using DysonNetwork.Shared.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Pass.Account; /// /// Controller for managing user presence activities with lease-based expiration. /// Supports both user-defined manual IDs and autogenerated GUIDs for activity management. /// [ApiController] [Route("/api/activities")] public class PresenceActivityController(AppDatabase db, AccountEventService service) : ControllerBase { /// /// Retrieves active (non-expired) presence activities for the authenticated user. /// Optionally includes expired activities if includeExpired is true. /// /// Whether to include expired activities /// The number of activities to skip for pagination /// The maximum number of activities to return /// List of presence activities [HttpGet] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task>> GetActivities( [FromQuery] bool includeExpired = false, [FromQuery] int offset = 0, [FromQuery] int take = 20 ) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); List activities; if (includeExpired) { (activities, var total) = await service.GetAllActivities(currentUser.Id, offset, take); Response.Headers["X-Total"] = total.ToString(); } else { activities = await service.GetActiveActivities(currentUser.Id); } return Ok(activities); } /// /// Retrieves active presence activities for any user account (admin/debugging endpoint). /// /// List of active presence activities [HttpGet("{identifier}")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetActivitiesByAccountId( string identifier ) { var account = Guid.TryParse(identifier, out var identifierGuid) ? await db.Accounts.FirstOrDefaultAsync(a => a.Id == identifierGuid) : await db.Accounts.FirstOrDefaultAsync(a => a.Name == identifier); if (account is null) return NotFound(); var activities = await service.GetActiveActivities(account.Id); return Ok(activities); } /// /// Creates or updates a presence activity with lease expiration. /// If an activity with the same 'manualId' exists, it will be updated. /// Otherwise, a new activity will be created. /// /// Activity creation or update parameters /// The created or updated activity [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> SetActivity( [FromBody] SetActivityRequest request ) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); if (!string.IsNullOrWhiteSpace(request.ManualId)) { var result = await service.UpdateActivityByManualId( request.ManualId, currentUser.Id, activity => { if (request.Type.HasValue) activity.Type = request.Type.Value; activity.Title = request.Title; activity.Subtitle = request.Subtitle; activity.Caption = request.Caption; activity.LargeImage = request.LargeImage; activity.SmallImage = request.SmallImage; activity.TitleUrl = request.TitleUrl; activity.SubtitleUrl = request.SubtitleUrl; activity.Meta = request.Meta; }, request.LeaseMinutes ); if (result != null) { return Ok(result); } } if (!request.Type.HasValue) return BadRequest("Type is required when creating a new activity"); var newActivity = new SnPresenceActivity { AccountId = currentUser.Id, Type = request.Type.Value, ManualId = request.ManualId, Title = request.Title, Subtitle = request.Subtitle, Caption = request.Caption, LargeImage = request.LargeImage, SmallImage = request.SmallImage, TitleUrl = request.TitleUrl, SubtitleUrl = request.SubtitleUrl, Meta = request.Meta }; var createResult = await service.SetActivity(newActivity, request.LeaseMinutes); return Ok(createResult); } /// /// Updates an existing presence activity using either its GUID or manual ID. /// /// System-generated GUID of the activity (optional) /// Update parameters (only provided fields are updated) /// The updated activity /// One of 'id' or 'manualId' must be provided and non-empty. [HttpPut("{id:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> UpdateActivity( [FromRoute] Guid id, [FromBody] SetActivityRequest request ) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var result = await service.UpdateActivity( id, currentUser.Id, activity => { if (request.Type.HasValue) activity.Type = request.Type.Value; if (request.Title != null) activity.Title = request.Title; if (request.Subtitle != null) activity.Subtitle = request.Subtitle; if (request.Caption != null) activity.Caption = request.Caption; if (request.ManualId != null) activity.ManualId = request.ManualId; if (request.Meta != null) activity.Meta = request.Meta; }, request.LeaseMinutes ); return Ok(result); } /// /// Deletes a presence activity using either its GUID or manual ID. /// /// System-generated GUID of the activity (optional) /// User-defined manual ID of the activity (optional) /// NoContent on success /// One of 'id' or 'manualId' must be provided and non-empty. Soft-deletes the activity. [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task DeleteActivityById( [FromQuery] string? id, [FromQuery] string? manualId ) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); if (!string.IsNullOrWhiteSpace(manualId)) { var deleted = await service.DeleteActivityByManualId(manualId, currentUser.Id); if (!deleted) return NotFound(); return NoContent(); } if (string.IsNullOrWhiteSpace(id) || !Guid.TryParse(id, out var activityGuid)) return BadRequest("Either 'id' (GUID) or 'manualId' must be provided"); { var deleted = await service.DeleteActivity(activityGuid, currentUser.Id); if (!deleted) return NotFound(); return NoContent(); } } /// /// Request model for creating a new presence activity. /// public class SetActivityRequest { /// The type of presence activity (e.g., Gaming, Music, Workout) public PresenceType? Type { get; set; } /// User-defined identifier for the activity (optional, for easy reference) public string? ManualId { get; set; } /// Main title of the activity public string? Title { get; set; } /// Secondary subtitle of the activity public string? Subtitle { get; set; } /// Additional caption/description public string? Caption { get; set; } /// Large image URL or base64 string public string? LargeImage { get; set; } /// Small image URL or base64 string public string? SmallImage { get; set; } /// Title URL public string? TitleUrl { get; set; } /// Subtitle URL public string? SubtitleUrl { get; set; } /// Extensible metadata dictionary for custom developer data public Dictionary? Meta { get; set; } /// Lease duration in minutes (1-60, default: 5) public int LeaseMinutes { get; set; } = 5; } }