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