diff --git a/DysonNetwork.Pass/Account/AccountEventService.cs b/DysonNetwork.Pass/Account/AccountEventService.cs index 9a03a30..b47ce77 100644 --- a/DysonNetwork.Pass/Account/AccountEventService.cs +++ b/DysonNetwork.Pass/Account/AccountEventService.cs @@ -26,6 +26,7 @@ public class AccountEventService( { private static readonly Random Random = new(); private const string StatusCacheKey = "account:status:"; + private const string ActivityCacheKey = "account:activities:"; private async Task GetAccountIsConnected(Guid userId) { @@ -41,6 +42,12 @@ public class AccountEventService( cache.RemoveAsync(cacheKey); } + public void PurgeActivityCache(Guid userId) + { + var cacheKey = $"{ActivityCacheKey}{userId}"; + cache.RemoveAsync(cacheKey); + } + private async Task BroadcastStatusUpdate(SnAccountStatus status) { await nats.PublishAsync( @@ -434,4 +441,105 @@ public class AccountEventService( }; }).ToList(); } + + public async Task> GetActiveActivities(Guid userId) + { + var cacheKey = $"{ActivityCacheKey}{userId}"; + var cachedActivities = await cache.GetAsync>(cacheKey); + if (cachedActivities != null) + { + return cachedActivities; + } + + var now = SystemClock.Instance.GetCurrentInstant(); + var activities = await db.PresenceActivities + .Where(e => e.AccountId == userId && e.LeaseExpiresAt > now) + .ToListAsync(); + + await cache.SetWithGroupsAsync(cacheKey, activities, [$"{AccountService.AccountCachePrefix}{userId}"], TimeSpan.FromMinutes(1)); + return activities; + } + + public async Task SetActivity(SnPresenceActivity activity, int leaseMinutes) + { + if (leaseMinutes < 1 || leaseMinutes > 60) + throw new ArgumentException("Lease minutes must be between 1 and 60"); + + var now = SystemClock.Instance.GetCurrentInstant(); + activity.LeaseMinutes = leaseMinutes; + activity.LeaseExpiresAt = now + Duration.FromMinutes(leaseMinutes); + + db.PresenceActivities.Add(activity); + await db.SaveChangesAsync(); + + PurgeActivityCache(activity.AccountId); + + return activity; + } + + public async Task UpdateActivity(Guid activityId, Action update, int? leaseMinutes = null) + { + var activity = await db.PresenceActivities.FindAsync(activityId); + if (activity == null) + throw new KeyNotFoundException("Activity not found"); + + if (leaseMinutes.HasValue) + { + if (leaseMinutes.Value < 1 || leaseMinutes.Value > 60) + throw new ArgumentException("Lease minutes must be between 1 and 60"); + + activity.LeaseMinutes = leaseMinutes.Value; + activity.LeaseExpiresAt = SystemClock.Instance.GetCurrentInstant() + Duration.FromMinutes(leaseMinutes.Value); + } + + update(activity); + await db.SaveChangesAsync(); + + PurgeActivityCache(activity.AccountId); + + return activity; + } + + public async Task UpdateActivityByManualId(string manualId, Guid userId, Action update, int? leaseMinutes = null) + { + var activity = await db.PresenceActivities.FirstOrDefaultAsync(e => e.ManualId == manualId && e.AccountId == userId); + if (activity == null) + return null; + + if (leaseMinutes.HasValue) + { + if (leaseMinutes.Value < 1 || leaseMinutes.Value > 60) + throw new ArgumentException("Lease minutes must be between 1 and 60"); + + activity.LeaseMinutes = leaseMinutes.Value; + activity.LeaseExpiresAt = SystemClock.Instance.GetCurrentInstant() + Duration.FromMinutes(leaseMinutes.Value); + } + + update(activity); + await db.SaveChangesAsync(); + + PurgeActivityCache(activity.AccountId); + + return activity; + } + + public async Task DeleteActivityByManualId(string manualId, Guid userId) + { + var activity = await db.PresenceActivities.FirstOrDefaultAsync(e => e.ManualId == manualId && e.AccountId == userId); + if (activity == null) return false; + db.Remove(activity); + await db.SaveChangesAsync(); + PurgeActivityCache(activity.AccountId); + return true; + } + + public async Task DeleteActivity(Guid activityId) + { + var activity = await db.PresenceActivities.FindAsync(activityId); + if (activity == null) return false; + db.Remove(activity); + await db.SaveChangesAsync(); + PurgeActivityCache(activity.AccountId); + return true; + } } diff --git a/DysonNetwork.Pass/Account/PresenceActivityController.cs b/DysonNetwork.Pass/Account/PresenceActivityController.cs new file mode 100644 index 0000000..1c2695c --- /dev/null +++ b/DysonNetwork.Pass/Account/PresenceActivityController.cs @@ -0,0 +1,240 @@ +using DysonNetwork.Shared.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +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")] +[Authorize] +public class PresenceActivityController(AppDatabase db, AccountEventService service) : ControllerBase +{ + /// + /// Retrieves all active (non-expired) presence activities for the authenticated user. + /// + /// List of active presence activities + [HttpGet] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>> GetActivities() + { + if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); + var activities = await service.GetActiveActivities(currentUser.Id); + return Ok(activities); + } + + /// + /// Retrieves active presence activities for any user account (admin/debugging endpoint). + /// + /// The account ID to fetch activities for + /// List of active presence activities + [HttpGet("{accountId:guid}")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>> GetActivitiesByAccountId(Guid accountId) + { + var activities = await service.GetActiveActivities(accountId); + return Ok(activities); + } + + /// + /// Creates a new presence activity with lease expiration. + /// + /// Activity creation parameters + /// The created 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(); + + var activity = new SnPresenceActivity + { + Type = request.Type, + ManualId = request.ManualId, + Title = request.Title, + Subtitle = request.Subtitle, + Caption = request.Caption, + Meta = request.Meta, + AccountId = currentUser.Id, + }; + var result = await service.SetActivity(activity, request.LeaseMinutes); + return Ok(result); + } + + /// + /// Updates an existing presence activity using either its GUID or manual ID. + /// + /// System-generated GUID of the activity (optional) + /// User-defined manual ID 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] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> UpdateActivity( + [FromQuery] string? id, + [FromQuery] string? manualId, + [FromBody] UpdateActivityRequest request + ) + { + if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); + + var type = request.Type; + var title = request.Title; + var subtitle = request.Subtitle; + var caption = request.Caption; + var requestManualId = request.ManualId; + var requestMeta = request.Meta; + var leaseMinutes = request.LeaseMinutes; + + if (!string.IsNullOrWhiteSpace(manualId)) + { + var result = await service.UpdateActivityByManualId( + manualId, + currentUser.Id, + activity => + { + if (type.HasValue) activity.Type = type.Value; + if (title != null) activity.Title = title; + if (subtitle != null) activity.Subtitle = subtitle; + if (caption != null) activity.Caption = caption; + if (requestManualId != null) activity.ManualId = requestManualId; + if (requestMeta != null) activity.Meta = requestMeta; + }, + leaseMinutes); + + if (result == null) + return NotFound(); + + return Ok(result); + } + else if (!string.IsNullOrWhiteSpace(id) && Guid.TryParse(id, out var activityGuid)) + { + var result = await service.UpdateActivity( + activityGuid, + activity => + { + if (type.HasValue) activity.Type = type.Value; + if (title != null) activity.Title = title; + if (subtitle != null) activity.Subtitle = subtitle; + if (caption != null) activity.Caption = caption; + if (requestManualId != null) activity.ManualId = requestManualId; + if (requestMeta != null) activity.Meta = requestMeta; + }, + leaseMinutes); + + return Ok(result); + } + else + { + return BadRequest("Either 'id' (GUID) or 'manualId' must be provided"); + } + } + + /// + /// 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(); + } + else + { + if (!string.IsNullOrWhiteSpace(id) && Guid.TryParse(id, out var activityGuid)) + { + var deleted = await service.DeleteActivity(activityGuid); + if (!deleted) + return NotFound(); + + return NoContent(); + } + return BadRequest("Either 'id' (GUID) or 'manualId' must be provided"); + } + } + + /// + /// 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; } + + /// 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; + } + + /// + /// Request model for updating an existing presence activity. + /// All fields are optional and will only update if provided. + /// + public class UpdateActivityRequest + { + /// The type of presence activity (optional update) + public PresenceType? Type { get; set; } + + /// User-defined identifier update + public string? ManualId { get; set; } + + /// Title update + public string? Title { get; set; } + + /// Subtitle update + public string? Subtitle { get; set; } + + /// Caption update + public string? Caption { get; set; } + + /// Metadata update + public Dictionary? Meta { get; set; } + + /// Lease renewal in minutes + public int? LeaseMinutes { get; set; } + } +} diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs index 0bfdff0..10a4c43 100644 --- a/DysonNetwork.Pass/AppDatabase.cs +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -30,6 +30,7 @@ public class AppDatabase( public DbSet AccountRelationships { get; set; } = null!; public DbSet AccountStatuses { get; set; } = null!; public DbSet AccountCheckInResults { get; set; } = null!; + public DbSet PresenceActivities { get; set; } = null!; public DbSet Badges { get; set; } = null!; public DbSet ActionLogs { get; set; } = null!; public DbSet AbuseReports { get; set; } = null!; diff --git a/DysonNetwork.Pass/Migrations/20251101092348_AddPresenceActivity.Designer.cs b/DysonNetwork.Pass/Migrations/20251101092348_AddPresenceActivity.Designer.cs new file mode 100644 index 0000000..2933486 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20251101092348_AddPresenceActivity.Designer.cs @@ -0,0 +1,2690 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Pass; +using DysonNetwork.Shared.GeoIp; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20251101092348_AddPresenceActivity")] + partial class AddPresenceActivity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Resolution") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("resolution"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("ResourceIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("resource_identifier"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_abuse_reports"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_abuse_reports_account_id"); + + b.ToTable("abuse_reports", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("AutomatedId") + .HasColumnType("uuid") + .HasColumnName("automated_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsSuperuser") + .HasColumnType("boolean") + .HasColumnName("is_superuser"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("language"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_accounts_name"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Config") + .HasColumnType("jsonb") + .HasColumnName("config"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EnabledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("enabled_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Secret") + .HasMaxLength(8196) + .HasColumnType("character varying(8196)") + .HasColumnName("secret"); + + b.Property("Trustworthy") + .HasColumnType("integer") + .HasColumnName("trustworthy"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_auth_factors"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_auth_factors_account_id"); + + b.ToTable("account_auth_factors", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_badges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_badges_account_id"); + + b.ToTable("badges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccessToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("access_token"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("ProvidedIdentifier") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("provided_identifier"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("provider"); + + b.Property("RefreshToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("refresh_token"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_connections"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_connections_account_id"); + + b.ToTable("account_connections", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_account_contacts"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_contacts_account_id"); + + b.ToTable("account_contacts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActiveBadge") + .HasColumnType("jsonb") + .HasColumnName("active_badge"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone") + .HasColumnName("birthday"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("FirstName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("first_name"); + + b.Property("Gender") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("gender"); + + b.Property("LastName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("last_name"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property>("Links") + .HasColumnType("jsonb") + .HasColumnName("links"); + + b.Property("Location") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("location"); + + b.Property("MiddleName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("middle_name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Pronouns") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("pronouns"); + + b.Property("SocialCredits") + .HasColumnType("double precision") + .HasColumnName("social_credits"); + + b.Property("TimeZone") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("time_zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UsernameColor") + .HasColumnType("jsonb") + .HasColumnName("username_color"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_account_profiles"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_account_profiles_account_id"); + + b.ToTable("account_profiles", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("BlockedPermissions") + .HasColumnType("jsonb") + .HasColumnName("blocked_permissions"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_punishments"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_punishments_account_id"); + + b.ToTable("punishments", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => + { + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("RelatedId") + .HasColumnType("uuid") + .HasColumnName("related_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Status") + .HasColumnType("smallint") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("AccountId", "RelatedId") + .HasName("pk_account_relationships"); + + b.HasIndex("RelatedId") + .HasDatabaseName("ix_account_relationships_related_id"); + + b.ToTable("account_relationships", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cleared_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsAutomated") + .HasColumnType("boolean") + .HasColumnName("is_automated"); + + b.Property("IsInvisible") + .HasColumnType("boolean") + .HasColumnName("is_invisible"); + + b.Property("IsNotDisturb") + .HasColumnType("boolean") + .HasColumnName("is_not_disturb"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_statuses"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_statuses_account_id"); + + b.ToTable("account_statuses", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("action"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("jsonb") + .HasColumnName("location"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_action_logs"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_action_logs_account_id"); + + b.ToTable("action_logs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_api_keys"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_api_keys_account_id"); + + b.HasIndex("SessionId") + .HasDatabaseName("ix_api_keys_session_id"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthChallenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Audiences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("audiences"); + + b.Property>("BlacklistFactors") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("blacklist_factors"); + + b.Property("ClientId") + .HasColumnType("uuid") + .HasColumnName("client_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FailedAttempts") + .HasColumnType("integer") + .HasColumnName("failed_attempts"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("jsonb") + .HasColumnName("location"); + + b.Property("Nonce") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nonce"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("StepRemain") + .HasColumnType("integer") + .HasColumnName("step_remain"); + + b.Property("StepTotal") + .HasColumnType("integer") + .HasColumnName("step_total"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_auth_challenges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_challenges_account_id"); + + b.HasIndex("ClientId") + .HasDatabaseName("ix_auth_challenges_client_id"); + + b.ToTable("auth_challenges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_id"); + + b.Property("DeviceLabel") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_label"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_name"); + + b.Property("Platform") + .HasColumnType("integer") + .HasColumnName("platform"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_clients"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_clients_account_id"); + + b.ToTable("auth_clients", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("ChallengeId") + .HasColumnType("uuid") + .HasColumnName("challenge_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("LastGrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_granted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_sessions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_sessions_account_id"); + + b.HasIndex("ChallengeId") + .HasDatabaseName("ix_auth_sessions_challenge_id"); + + b.ToTable("auth_sessions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BackdatedFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("backdated_from"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); + + b.Property("RewardPoints") + .HasColumnType("numeric") + .HasColumnName("reward_points"); + + b.Property>("Tips") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("tips"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_check_in_results"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_check_in_results_account_id"); + + b.ToTable("account_check_in_results", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BonusMultiplier") + .HasColumnType("double precision") + .HasColumnName("bonus_multiplier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Delta") + .HasColumnType("bigint") + .HasColumnName("delta"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.Property("ReasonType") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_experience_records"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_experience_records_account_id"); + + b.ToTable("experience_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DrawDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("draw_date"); + + b.Property("DrawStatus") + .HasColumnType("integer") + .HasColumnName("draw_status"); + + b.Property>("MatchedRegionOneNumbers") + .HasColumnType("jsonb") + .HasColumnName("matched_region_one_numbers"); + + b.Property("MatchedRegionTwoNumber") + .HasColumnType("integer") + .HasColumnName("matched_region_two_number"); + + b.Property("Multiplier") + .HasColumnType("integer") + .HasColumnName("multiplier"); + + b.Property>("RegionOneNumbers") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("region_one_numbers"); + + b.Property("RegionTwoNumber") + .HasColumnType("integer") + .HasColumnName("region_two_number"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_lotteries"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_lotteries_account_id"); + + b.ToTable("lotteries", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnLotteryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DrawDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("draw_date"); + + b.Property("TotalPrizeAmount") + .HasColumnType("bigint") + .HasColumnName("total_prize_amount"); + + b.Property("TotalPrizesAwarded") + .HasColumnType("integer") + .HasColumnName("total_prizes_awarded"); + + b.Property("TotalTickets") + .HasColumnType("integer") + .HasColumnName("total_tickets"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property>("WinningRegionOneNumbers") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("winning_region_one_numbers"); + + b.Property("WinningRegionTwoNumber") + .HasColumnType("integer") + .HasColumnName("winning_region_two_number"); + + b.HasKey("Id") + .HasName("pk_lottery_records"); + + b.ToTable("lottery_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Spell") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("spell"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_magic_spells"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_magic_spells_account_id"); + + b.HasIndex("Spell") + .IsUnique() + .HasDatabaseName("ix_magic_spells_spell"); + + b.ToTable("magic_spells", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => + { + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Actor") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("GroupId", "Actor") + .HasName("pk_permission_group_members"); + + b.ToTable("permission_group_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("area"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_permission_nodes"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permission_nodes_group_id"); + + b.HasIndex("Key", "Area", "Actor") + .HasDatabaseName("ix_permission_nodes_key_area_actor"); + + b.ToTable("permission_nodes", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPresenceActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LeaseExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lease_expires_at"); + + b.Property("LeaseMinutes") + .HasColumnType("integer") + .HasColumnName("lease_minutes"); + + b.Property("ManualId") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("manual_id"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Subtitle") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subtitle"); + + b.Property("Title") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_presence_activities"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_presence_activities_account_id"); + + b.ToTable("presence_activities", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("IsCommunity") + .HasColumnType("boolean") + .HasColumnName("is_community"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_realms"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_realms_slug"); + + b.ToTable("realms", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => + { + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("LeaveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("leave_at"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("RealmId", "AccountId") + .HasName("pk_realm_members"); + + b.ToTable("realm_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Delta") + .HasColumnType("double precision") + .HasColumnName("delta"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.Property("ReasonType") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_social_credit_records"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_social_credit_records_account_id"); + + b.ToTable("social_credit_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallets"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallets_account_id"); + + b.ToTable("wallets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletCoupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Code") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DiscountAmount") + .HasColumnType("numeric") + .HasColumnName("discount_amount"); + + b.Property("DiscountRate") + .HasColumnType("double precision") + .HasColumnName("discount_rate"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Identifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("MaxUsage") + .HasColumnType("integer") + .HasColumnName("max_usage"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_coupons"); + + b.ToTable("wallet_coupons", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatorAccountId") + .HasColumnType("uuid") + .HasColumnName("creator_account_id"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Message") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("SplitType") + .HasColumnType("integer") + .HasColumnName("split_type"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TotalAmount") + .HasColumnType("numeric") + .HasColumnName("total_amount"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_funds"); + + b.HasIndex("CreatorAccountId") + .HasDatabaseName("ix_wallet_funds_creator_account_id"); + + b.ToTable("wallet_funds", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFundRecipient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("FundId") + .HasColumnType("uuid") + .HasColumnName("fund_id"); + + b.Property("IsReceived") + .HasColumnType("boolean") + .HasColumnName("is_received"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("received_at"); + + b.Property("RecipientAccountId") + .HasColumnType("uuid") + .HasColumnName("recipient_account_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_fund_recipients"); + + b.HasIndex("FundId") + .HasDatabaseName("ix_wallet_fund_recipients_fund_id"); + + b.HasIndex("RecipientAccountId") + .HasDatabaseName("ix_wallet_fund_recipients_recipient_account_id"); + + b.ToTable("wallet_fund_recipients", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FinalPrice") + .HasColumnType("numeric") + .HasColumnName("final_price"); + + b.Property("GiftCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("gift_code"); + + b.Property("GifterId") + .HasColumnType("uuid") + .HasColumnName("gifter_id"); + + b.Property("IsOpenGift") + .HasColumnType("boolean") + .HasColumnName("is_open_gift"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("message"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RecipientId") + .HasColumnType("uuid") + .HasColumnName("recipient_id"); + + b.Property("RedeemedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("redeemed_at"); + + b.Property("RedeemerId") + .HasColumnType("uuid") + .HasColumnName("redeemer_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("SubscriptionId") + .HasColumnType("uuid") + .HasColumnName("subscription_id"); + + b.Property("SubscriptionIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subscription_identifier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_gifts"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_gifts_coupon_id"); + + b.HasIndex("GiftCode") + .HasDatabaseName("ix_wallet_gifts_gift_code"); + + b.HasIndex("GifterId") + .HasDatabaseName("ix_wallet_gifts_gifter_id"); + + b.HasIndex("RecipientId") + .HasDatabaseName("ix_wallet_gifts_recipient_id"); + + b.HasIndex("RedeemerId") + .HasDatabaseName("ix_wallet_gifts_redeemer_id"); + + b.HasIndex("SubscriptionId") + .IsUnique() + .HasDatabaseName("ix_wallet_gifts_subscription_id"); + + b.ToTable("wallet_gifts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("ProductIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("product_identifier"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TransactionId") + .HasColumnType("uuid") + .HasColumnName("transaction_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_orders"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_orders_payee_wallet_id"); + + b.HasIndex("TransactionId") + .HasDatabaseName("ix_payment_orders_transaction_id"); + + b.ToTable("payment_orders", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WalletId") + .HasColumnType("uuid") + .HasColumnName("wallet_id"); + + b.HasKey("Id") + .HasName("pk_wallet_pockets"); + + b.HasIndex("WalletId") + .HasDatabaseName("ix_wallet_pockets_wallet_id"); + + b.ToTable("wallet_pockets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + b.Property("BegunAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("begun_at"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsFreeTrial") + .HasColumnType("boolean") + .HasColumnName("is_free_trial"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RenewalAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("renewal_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallet_subscriptions_account_id"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_subscriptions_coupon_id"); + + b.HasIndex("Identifier") + .HasDatabaseName("ix_wallet_subscriptions_identifier"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wallet_subscriptions_status"); + + b.HasIndex("AccountId", "Identifier") + .HasDatabaseName("ix_wallet_subscriptions_account_id_identifier"); + + b.HasIndex("AccountId", "IsActive") + .HasDatabaseName("ix_wallet_subscriptions_account_id_is_active"); + + b.ToTable("wallet_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("PayerWalletId") + .HasColumnType("uuid") + .HasColumnName("payer_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_transactions"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_transactions_payee_wallet_id"); + + b.HasIndex("PayerWalletId") + .HasDatabaseName("ix_payment_transactions_payer_wallet_id"); + + b.ToTable("payment_transactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_abuse_reports_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Connections") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_connections_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Shared.Models.SnAccountProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_punishments_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Related") + .WithMany("IncomingRelationships") + .HasForeignKey("RelatedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_related_id"); + + b.Navigation("Account"); + + b.Navigation("Related"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountStatus", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_auth_sessions_session_id"); + + b.Navigation("Account"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthChallenge", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .HasConstraintName("fk_auth_challenges_auth_clients_client_id"); + + b.Navigation("Account"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_clients_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.Navigation("Account"); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_experience_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lotteries_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPresenceActivity", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_presence_activities_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnRealm", "Realm") + .WithMany("Members") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_members_realms_realm_id"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_social_credit_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "CreatorAccount") + .WithMany() + .HasForeignKey("CreatorAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_funds_accounts_creator_account_id"); + + b.Navigation("CreatorAccount"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFundRecipient", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWalletFund", "Fund") + .WithMany("Recipients") + .HasForeignKey("FundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_fund_recipients_wallet_funds_fund_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "RecipientAccount") + .WithMany() + .HasForeignKey("RecipientAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_fund_recipients_accounts_recipient_account_id"); + + b.Navigation("Fund"); + + b.Navigation("RecipientAccount"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_gifts_wallet_coupons_coupon_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Gifter") + .WithMany() + .HasForeignKey("GifterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_gifts_accounts_gifter_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .HasConstraintName("fk_wallet_gifts_accounts_recipient_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Redeemer") + .WithMany() + .HasForeignKey("RedeemerId") + .HasConstraintName("fk_wallet_gifts_accounts_redeemer_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletSubscription", "Subscription") + .WithOne("Gift") + .HasForeignKey("DysonNetwork.Shared.Models.SnWalletGift", "SubscriptionId") + .HasConstraintName("fk_wallet_gifts_wallet_subscriptions_subscription_id"); + + b.Navigation("Coupon"); + + b.Navigation("Gifter"); + + b.Navigation("Recipient"); + + b.Navigation("Redeemer"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletTransaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", b => + { + b.Navigation("AuthFactors"); + + b.Navigation("Badges"); + + b.Navigation("Challenges"); + + b.Navigation("Connections"); + + b.Navigation("Contacts"); + + b.Navigation("IncomingRelationships"); + + b.Navigation("OutgoingRelationships"); + + b.Navigation("Profile") + .IsRequired(); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.Navigation("Pockets"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b => + { + b.Navigation("Recipients"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.Navigation("Gift"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Pass/Migrations/20251101092348_AddPresenceActivity.cs b/DysonNetwork.Pass/Migrations/20251101092348_AddPresenceActivity.cs new file mode 100644 index 0000000..6f59e07 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20251101092348_AddPresenceActivity.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + /// + public partial class AddPresenceActivity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "background_id", + table: "realms"); + + migrationBuilder.DropColumn( + name: "picture_id", + table: "realms"); + + migrationBuilder.CreateTable( + name: "presence_activities", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + type = table.Column(type: "integer", nullable: false), + manual_id = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + title = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + subtitle = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + caption = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), + meta = table.Column>(type: "jsonb", nullable: true), + lease_minutes = table.Column(type: "integer", nullable: false), + lease_expires_at = table.Column(type: "timestamp with time zone", nullable: false), + account_id = table.Column(type: "uuid", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + deleted_at = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_presence_activities", x => x.id); + table.ForeignKey( + name: "fk_presence_activities_accounts_account_id", + column: x => x.account_id, + principalTable: "accounts", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_presence_activities_account_id", + table: "presence_activities", + column: "account_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "presence_activities"); + + migrationBuilder.AddColumn( + name: "background_id", + table: "realms", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "picture_id", + table: "realms", + type: "character varying(32)", + maxLength: 32, + nullable: true); + } + } +} diff --git a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs index a663138..dd327d6 100644 --- a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs @@ -22,7 +22,7 @@ namespace DysonNetwork.Pass.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("ProductVersion", "9.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -1363,6 +1363,74 @@ namespace DysonNetwork.Pass.Migrations b.ToTable("permission_nodes", (string)null); }); + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPresenceActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LeaseExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lease_expires_at"); + + b.Property("LeaseMinutes") + .HasColumnType("integer") + .HasColumnName("lease_minutes"); + + b.Property("ManualId") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("manual_id"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Subtitle") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subtitle"); + + b.Property("Title") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_presence_activities"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_presence_activities_account_id"); + + b.ToTable("presence_activities", (string)null); + }); + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => { b.Property("Id") @@ -1378,11 +1446,6 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("jsonb") .HasColumnName("background"); - b.Property("BackgroundId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("background_id"); - b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasColumnName("created_at"); @@ -1415,11 +1478,6 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("jsonb") .HasColumnName("picture"); - b.Property("PictureId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("picture_id"); - b.Property("Slug") .IsRequired() .HasMaxLength(1024) @@ -2389,6 +2447,18 @@ namespace DysonNetwork.Pass.Migrations b.Navigation("Group"); }); + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPresenceActivity", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_presence_activities_accounts_account_id"); + + b.Navigation("Account"); + }); + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => { b.HasOne("DysonNetwork.Shared.Models.SnRealm", "Realm") diff --git a/DysonNetwork.Shared/Models/AccountEvent.cs b/DysonNetwork.Shared/Models/AccountEvent.cs index 23bd9fb..25b0641 100644 --- a/DysonNetwork.Shared/Models/AccountEvent.cs +++ b/DysonNetwork.Shared/Models/AccountEvent.cs @@ -10,21 +10,31 @@ public enum StatusAttitude { Positive, Negative, - Neutral + Neutral, } public class SnAccountStatus : ModelBase { public Guid Id { get; set; } = Guid.NewGuid(); public StatusAttitude Attitude { get; set; } - [NotMapped] public bool IsOnline { get; set; } - [NotMapped] public bool IsCustomized { get; set; } = true; + + [NotMapped] + public bool IsOnline { get; set; } + + [NotMapped] + public bool IsCustomized { get; set; } = true; public bool IsInvisible { get; set; } public bool IsNotDisturb { get; set; } - [MaxLength(1024)] public string? Label { get; set; } - [Column(TypeName = "jsonb")] public Dictionary? Meta { get; set; } + + [MaxLength(1024)] + public string? Label { get; set; } + + [Column(TypeName = "jsonb")] + public Dictionary? Meta { get; set; } public Instant? ClearedAt { get; set; } - [MaxLength(4096)] public string? AppIdentifier { get; set; } + + [MaxLength(4096)] + public string? AppIdentifier { get; set; } /// /// Indicates this status is created based on running process or rich presence @@ -44,7 +54,7 @@ public class SnAccountStatus : ModelBase StatusAttitude.Positive => Shared.Proto.StatusAttitude.Positive, StatusAttitude.Negative => Shared.Proto.StatusAttitude.Negative, StatusAttitude.Neutral => Shared.Proto.StatusAttitude.Neutral, - _ => Shared.Proto.StatusAttitude.Unspecified + _ => Shared.Proto.StatusAttitude.Unspecified, }, IsOnline = IsOnline, IsCustomized = IsCustomized, @@ -53,7 +63,7 @@ public class SnAccountStatus : ModelBase Label = Label ?? string.Empty, Meta = GrpcTypeHelper.ConvertObjectToByteString(Meta), ClearedAt = ClearedAt?.ToTimestamp(), - AccountId = AccountId.ToString() + AccountId = AccountId.ToString(), }; return proto; @@ -69,7 +79,7 @@ public class SnAccountStatus : ModelBase Shared.Proto.StatusAttitude.Positive => StatusAttitude.Positive, Shared.Proto.StatusAttitude.Negative => StatusAttitude.Negative, Shared.Proto.StatusAttitude.Neutral => StatusAttitude.Neutral, - _ => StatusAttitude.Neutral + _ => StatusAttitude.Neutral, }, IsOnline = proto.IsOnline, IsCustomized = proto.IsCustomized, @@ -78,7 +88,7 @@ public class SnAccountStatus : ModelBase Label = proto.Label, Meta = GrpcTypeHelper.ConvertByteStringToObject>(proto.Meta), ClearedAt = proto.ClearedAt?.ToInstant(), - AccountId = Guid.Parse(proto.AccountId) + AccountId = Guid.Parse(proto.AccountId), }; return status; @@ -92,7 +102,7 @@ public enum CheckInResultLevel Normal, Better, Best, - Special + Special, } public class SnCheckInResult : ModelBase @@ -101,7 +111,9 @@ public class SnCheckInResult : ModelBase public CheckInResultLevel Level { get; set; } public decimal? RewardPoints { get; set; } public int? RewardExperience { get; set; } - [Column(TypeName = "jsonb")] public ICollection Tips { get; set; } = new List(); + + [Column(TypeName = "jsonb")] + public ICollection Tips { get; set; } = new List(); public Guid AccountId { get; set; } public SnAccount Account { get; set; } = null!; @@ -125,3 +137,38 @@ public class DailyEventResponse public SnCheckInResult? CheckInResult { get; set; } public ICollection Statuses { get; set; } = new List(); } + +public enum PresenceType +{ + Unknown, + Gaming, + Music, + Workout, +} + +public class SnPresenceActivity : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public PresenceType Type { get; set; } = PresenceType.Unknown; + + [MaxLength(4096)] + public string? ManualId { get; set; } + + [MaxLength(4096)] + public string? Title { get; set; } + + [MaxLength(4096)] + public string? Subtitle { get; set; } + + [MaxLength(4096)] + public string? Caption { get; set; } + + [Column(TypeName = "jsonb")] + public Dictionary? Meta { get; set; } + + public int LeaseMinutes { get; set; } = 5; // Lease period in minutes (1-60) + public Instant LeaseExpiresAt { get; set; } + + public Guid AccountId { get; set; } + public SnAccount Account { get; set; } = null!; +} diff --git a/PRESENCE_ACTIVITY_API.md b/PRESENCE_ACTIVITY_API.md new file mode 100644 index 0000000..4596d68 --- /dev/null +++ b/PRESENCE_ACTIVITY_API.md @@ -0,0 +1,324 @@ +# Presence Activity API Documentation + +## Overview + +The Presence Activity API allows users to manage their current activities (e.g., gaming, music, workouts) with automatic expiration through a lease-based system. Activities can be created, updated, and deleted, with support for flexible metadata and both system-generated and user-defined identifiers. + +This service is handled by the DysonNetwork.Pass, when using with the gateway, replace the `/api` with the `/pass` + +## Key Features + +- **Lease-Based Expiration**: Activities automatically expire within 1-60 minutes unless renewed +- **Flexible Identity**: Support for both autogenerated GUIDs and user-defined ManualIds +- **Extensible Metadata**: JSON-stored metadata dictionary for custom developer data +- **Soft Deletion**: Activities are soft-deleted and filtered automatically +- **Performance Optimized**: Cached active activities with 1-minute expiration +- **Authentication Required**: All endpoints require valid user authentication + +## API Endpoints + +### Base URL: `/api/activities` + +### Authentication +All endpoints require `[Authorize]` header. User context is automatically extracted. + +--- + +## Get Active Activities + +Retrieve all currently active (non-expired) presence activities for the authenticated user. + +**Endpoint:** `GET /api/activities` + +**Response:** +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "type": "Gaming", + "manualId": "game-session-1", + "title": "Playing Cyberpunk 2077", + "subtitle": "Night City Exploration", + "caption": "Missions completed: 15", + "meta": { + "appName": "Cyberpunk 2077", + "platform": "Steam", + "customProperty": "additional data" + }, + "leaseMinutes": 10, + "leaseExpiresAt": "2024-01-15T14:30:00Z", + "accountId": "user-guid", + "createdAt": "2024-01-15T14:25:00Z", + "updatedAt": "2024-01-15T14:25:00Z", + "deletedAt": null + } +] +``` + +**Common Response Codes:** +- `200 OK` - Success, returns array of active activities +- `401 Unauthorized` - Invalid or missing authentication + +--- + +## Create New Activity + +Create a new presence activity with a configurable lease period. + +**Endpoint:** `POST /api/activities` + +**Request Body:** +```json +{ + "type": "Gaming", + "manualId": "my-game-session", + "title": "Playing Cyberpunk 2077", + "subtitle": "Night City Mission", + "caption": "Currently exploring downtown", + "meta": { + "appName": "Cyberpunk 2077", + "platform": "Steam", + "difficulty": "Hard", + "mods": ["mod1", "mod2"] + }, + "leaseMinutes": 15 +} +``` + +**Response:** Returns the created `SnPresenceActivity` object with populated fields. + +**Field Details:** +- `type`: PresenceType enum (Unknown, Gaming, Music, Workout) +- `manualId`: Optional user-defined string identifier +- `title`, `subtitle`, `caption`: Display strings (max 4096 chars each) +- `meta`: Optional `Dictionary` for custom data +- `leaseMinutes`: 1-60 minutes (default: 5) + +**Response Codes:** +- `200 OK` - Activity created successfully +- `400 Bad Request` - Invalid lease minutes or malformed data +- `401 Unauthorized` - Invalid authentication + +--- + +## Update Activity + +Update an existing activity using either its GUID or ManualId. Only provided fields are updated. + +**Endpoint:** `PUT /api/activities` + +**Query Parameters:** (one required) +- `id` - System-generated GUID (string) +- `manualId` - User-defined identifier (string) + +**Request Body:** (all fields optional) +```json +{ + "title": "Updated: Playing Cyberpunk 2077", + "meta": { + "appName": "Cyberpunk 2077", + "platform": "Steam", + "newProperty": "updated data" + }, + "leaseMinutes": 20 +} +``` + +**Response:** Returns the updated `SnPresenceActivity` object. + +**Response Codes:** +- `200 OK` - Activity updated successfully +- `400 Bad Request` - Missing or invalid ID parameters +- `401 Unauthorized` - Invalid authentication +- `404 Not Found` - Activity not found or doesn't belong to user + +**Example cURL:** +```bash +# Update by ManualId +curl -X PUT "/api/activities?manualId=my-game-session" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"leaseMinutes": 20}' + +# Update by GUID +curl -X PUT "/api/activities?id=550e8400-e29b-41d4-a716-446655440000" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"title": "Updated Title"}' +``` + +--- + +## Delete Activity + +Soft-delete an activity using either GUID or ManualId. + +**Endpoint:** `DELETE /api/activities` + +**Query Parameters:** (one required) +- `id` - System-generated GUID (string) +- `manualId` - User-defined identifier (string) + +**Request Body:** None + +**Response:** No content (204) + +**Response Codes:** +- `204 No Content` - Activity deleted successfully +- `400 Bad Request` - Missing or invalid ID parameters +- `401 Unauthorized` - Invalid authentication +- `404 Not Found` - Activity not found or doesn't belong to user + +**Example cURL:** +```bash +# Delete by ManualId +curl -X DELETE "/api/activities?manualId=my-game-session" \ + -H "Authorization: Bearer " +``` + +--- + +## Additional Endpoint + +### Get Activities by Account ID + +**Endpoint:** `GET /api/activities/{accountId:guid}` + +For administrative or debugging purposes. Returns activities for the specified account ID, regardless of authentication. + +--- + +## Data Models + +### PresenceType Enum +```csharp +public enum PresenceType +{ + Unknown, + Gaming, + Music, + Workout +} +``` + +### SnPresenceActivity +```csharp +public class SnPresenceActivity : ModelBase +{ + public Guid Id { get; set; } // System-generated GUID + public PresenceType Type { get; set; } + public string? ManualId { get; set; } // User-defined ID + public string? Title { get; set; } + public string? Subtitle { get; set; } + public string? Caption { get; set; } + public Dictionary? Meta { get; set; } // JSON metadata + public int LeaseMinutes { get; set; } // Lease duration + public Instant LeaseExpiresAt { get; set; } // Expiration timestamp + + // Inherited from ModelBase + public Guid AccountId { get; set; } + public Instant CreatedAt { get; set; } + public Instant UpdatedAt { get; set; } + public Instant? DeletedAt { get; set; } // Soft deletion +} +``` + +## Behavior & Constraints + +### Lease Expiration +- Activities automatically expire when `SystemClock.Instance.GetCurrentInstant() > LeaseExpiresAt` +- Expiry is checked in database queries, so expired activities are filtered out of GET operations +- Clients must periodically update/renew leases to keep activities active + +### ID Flexibility +- **ManualId**: User-defined string, unique within a user's activities +- **GUID**: System-generated, always unique, returned in API responses +- Both can be used interchangeably for updates and deletion + +### Performance Optimizations +- Active activities are cached for 1 minute to handle frequent updates +- Cache is invalidated on create/update/delete operations +- Database queries filter expired activities automatically + +### Security +- All operations are scoped to the authenticated user's account +- Users can only manage their own activities +- Invalid or expired authentication tokens return 401 Unauthorized + +### Data Storage +- Activities are stored in PostgreSQL with JSONB metadata support +- Soft deletion uses timestamp rather than hard removal +- EF Core middleware automatically handles CreatedAt/UpdatedAt timestamps + +## Usage Examples + +### Gaming Session Management +```javascript +// Start gaming session +const activity = await fetch('/api/activities', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'Gaming', + manualId: 'game-session-1', + title: 'Playing Cyberpunk 2077', + meta: { appId: 'cyberpunk2077', mods: ['photorealistic'] }, + leaseMinutes: 15 + }) +}); + +// Update progress (extend lease) +await fetch('/api/activities?manualId=game-session-1', { + method: 'PUT', + body: JSON.stringify({ + title: 'Playing Cyberpunk 2077 - Level 25', + leaseMinutes: 15 + }) +}); + +// End session +await fetch('/api/activities?manualId=game-session-1', { + method: 'DELETE' +}); +``` + +### Metadata Extension +```javascript +// Rich metadata support +const activity = { + type: 'Music', + manualId: 'spotify-session', + title: 'Listening to Electronic', + meta: { + spotifyTrackId: '1Je1IMUlBXcx1FzbcXRuWw', + artist: 'Purity Ring', + album: 'Shrines', + duration: 240000, // milliseconds + custom: { userRating: 5, genre: 'Electronic' } + }, + leaseMinutes: 30 +}; +``` + +## Error Handling + +Common error responses follow REST API conventions: + +```json +{ + "type": "Microsoft.AspNetCore.Mvc.ValidationProblemDetails", + "title": "One or more validation errors occurred.", + "status": 400, + "errors": { + "leaseMinutes": ["Lease minutes must be between 1 and 60."] + } +} +``` + +## Implementation Notes + +- Built with ASP.NET Core and Entity Framework Core +- Uses NodaTime for precise timestamp handling +- PostgreSQL JSONB for flexible metadata storage +- Integration with existing authentication and caching systems +- Follows established project patterns for soft deletion and audit trails