using Discord; using LiteDB; using Campus_live_rewrite.api.server.Classes.db_class; using static Campus_live_rewrite.api.server.Classes.db_class.EventDBClasses; namespace Campus_live_rewrite.api.dbs { public class EventDB { public static LiteDatabase EventDBFile = new LiteDatabase(Environment.CurrentDirectory + "/Data/DBs/Events.db"); private static ILiteCollection Events => EventDBFile.GetCollection("events"); private static ulong GetNextId() { var last = Events.Query() .OrderByDescending(x => x.PlayerEventId) .FirstOrDefault(); return last == null ? 1UL : last.PlayerEventId + 1; } public static FullPlayerEvent? GetFullEventById(ulong eventId) { return Events.FindById(eventId); } /// Apply an edit to a stored event and persist it. Null when the event is unknown. public static FullPlayerEvent? Mutate(ulong eventId, Action edit) { var ev = Events.FindById(eventId); if (ev == null) return null; edit(ev); Events.Update(ev); return ev; } public static FullPlayerEvent? SetEventName(ulong eventId, string NewName) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.Name = NewName; Events.Update(ev); return ev; } public static FullPlayerEvent? SetEventImageName(ulong eventId, string NewImageName) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.ImageName = NewImageName; Events.Update(ev); return ev; } public static FullPlayerEvent? SetEventDescription(ulong eventId, string newDescription) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.Description = newDescription; Events.Update(ev); return ev; } public static FullPlayerEvent? SetEventTime(ulong eventId, DateTime start, DateTime end) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.StartTime = start; ev.EndTime = end; Events.Update(ev); return ev; } public static FullPlayerEvent? SetEventAccessibility(ulong eventId, EventDBClasses.PlayerEventAccessibility accessibility) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.Accessibility = accessibility; Events.Update(ev); return ev; } public static FullPlayerEvent? SetEventTags(ulong eventId, List tags) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.Tags = (tags ?? new List()) .Where(t => !string.IsNullOrWhiteSpace(t)) .Select(t => new EventDBClasses.FullTag { Tag = t.Trim(), Type = EventDBClasses.TagType.Unknown1 }) .ToList(); Events.Update(ev); return ev; } public static List SearchEventByQuery(string? query) { if (string.IsNullOrWhiteSpace(query)) { return Events.Query() .OrderBy(e => e.StartTime) .ToList(); } query = query.Trim(); bool isTagSearch = query.StartsWith("#") || query.StartsWith("%23"); if (isTagSearch) { string tag = query .Replace("%23", "") .TrimStart('#') .ToLower(); return Events .Find(Query.EQ("Tags.Tag", tag)) .OrderBy(e => e.StartTime) .ToList(); } string nameQuery = query.ToLower(); return Events.Query() .Where(e => e.Name != null && e.Name.ToLower().Contains(nameQuery)) .OrderBy(e => e.StartTime) .ToList(); } /// /// Events scheduled in a given room. /api/playerevents/v1/room/{RoomId} used to answer /// a hardcoded "[]" (marked "todo db"), so a room never showed the events booked in it. /// public static List GetEventsForRoom(ulong roomId) { return Events.Query() .Where(e => e.RoomId == roomId) .OrderBy(e => e.StartTime) .ToList(); } /// /// Events happening right now - started, not yet ended. /api/playerevents/v1/searchlive /// was another hardcoded "[]", so the "live now" list was always empty however many /// events were running. /// public static List GetLiveEvents() { var now = DateTime.UtcNow; return Events.Query() .ToList() .Where(e => e.StartTime.ToUniversalTime() <= now && e.EndTime.ToUniversalTime() >= now) .OrderBy(e => e.StartTime) .ToList(); } /// /// Records a player's RSVP and keeps AttendeeCount in step with it. One response per /// player per event - responding again replaces the previous answer. /// public static FullPlayerEvent? SetResponse(ulong eventId, ulong playerId, PlayerEventResponseType type) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.Responses ??= new List(); ev.Responses.RemoveAll(r => r.PlayerId == playerId); if (type != PlayerEventResponseType.None) { ulong nextId = ev.Responses.Count == 0 ? 1UL : ev.Responses.Max(r => r.PlayerEventResponseId) + 1; ev.Responses.Add(new FullEventResponses { PlayerEventResponseId = nextId, PlayerEventId = eventId, PlayerId = playerId, CreatedAt = DateTime.UtcNow, Type = type }); } // AttendeeCount was never maintained - it sat at 0 no matter how many people // responded, so every event advertised itself as empty. ev.AttendeeCount = ev.Responses.Count(r => r.Type == PlayerEventResponseType.Yes); Events.Update(ev); return ev; } /// /// Whether a player may enter an event's room, and why not when they may not. /// /// Rules: /// - the CREATOR may join at any time, including before it starts (they have to be /// able to set the room up). /// - anyone INVITED may join at any time. An invite is a response row: the invite /// flow writes Pending, and Yes / Interested / No all mean the player is already /// part of the event, so any response row counts. /// - everyone else may only join once it has actually started, and not after it has /// ended. /// /// Returns true when entry is allowed; `reason` explains a refusal for the log. /// public static bool CanPlayerJoin(FullPlayerEvent ev, ulong playerId, out string reason) { reason = ""; if (ev.CreatorPlayerId == playerId) return true; bool invited = ev.Responses?.Any(r => r.PlayerId == playerId) == true; if (invited) return true; var now = DateTime.UtcNow; // Doors open a few minutes early so people can be in the room when it starts. if (now < ev.StartTime.ToUniversalTime().AddMinutes(-Campus_live_rewrite.api.server.ConfigCampus.EventEarlyJoinMinutes)) { reason = $"event has not started (starts {ev.StartTime:u}, not invited)"; return false; } if (now > ev.EndTime.ToUniversalTime()) { reason = $"event already ended ({ev.EndTime:u})"; return false; } return true; } /// Marks an event live / not live. Used by roominstance/{id}/inprogress. public static FullPlayerEvent? SetBroadcastingInstance(ulong eventId, ulong roomInstanceId, bool inProgress) { var ev = Events.FindById(eventId); if (ev == null) return null; ev.BroadcastingRoomInstanceId = inProgress ? roomInstanceId : 0; ev.State = inProgress ? 1 : 0; Events.Update(ev); return ev; } /// The event currently broadcasting to a given room instance, if any. public static FullPlayerEvent? GetEventForRoomInstance(ulong roomInstanceId) { if (roomInstanceId == 0) return null; return Events.Query() .Where(e => e.BroadcastingRoomInstanceId == roomInstanceId) .FirstOrDefault(); } /// Removes an event outright. Used by the admin panel for moderation. public static bool DeleteEvent(ulong eventId) => Events.Delete(eventId); public static FullPlayerEvent CreateEvent(ulong creatorPlayerId, EventDBClasses.CreateEventRequest request) { var ev = new FullPlayerEvent { PlayerEventId = GetNextId(), CreatorPlayerId = creatorPlayerId, RoomId = request.RoomId, SubRoomId = request.SubRoomId, ClubId = request.ClubId, Name = request.Name, Description = request.Description, ImageName = request.ImageName, Tags = request.Tags?.Select(t => new EventDBClasses.FullTag { Tag = t, Type = EventDBClasses.TagType.Unknown1 }).ToList(), StartTime = request.StartTime, EndTime = request.EndTime, AttendeeCount = 0, State = 0, Accessibility = request.Accessibility, IsMultiInstance = request.IsMultiInstance, SupportMultiInstanceRoomChat = request.SupportMultiInstanceRoomChat, DefaultBroadcastPermissions = (EventDBClasses.BroadcastPermissionsEnum)request.DefaultBroadcastPermissions, CanRequestBroadcastPermissions = (EventDBClasses.BroadcastPermissionsEnum)request.CanRequestBroadcastPermissions, Responses = new List() }; Events.Insert(ev); return ev; } } }