✨ Add account statuses
This commit is contained in:
		| @@ -1,5 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| @@ -15,6 +16,7 @@ public class AccountController( | ||||
|     FileService fs, | ||||
|     AuthService auth, | ||||
|     AccountService accounts, | ||||
|     AccountEventService events, | ||||
|     MagicSpellService spells | ||||
| ) : ControllerBase | ||||
| { | ||||
| @@ -203,6 +205,62 @@ public class AccountController( | ||||
|         return profile; | ||||
|     } | ||||
|  | ||||
|     public class StatusRequest | ||||
|     { | ||||
|         public StatusAttitude Attitude { get; set; } | ||||
|         public bool IsInvisible { get; set; } | ||||
|         public bool IsNotDisturb { get; set; } | ||||
|         [MaxLength(1024)] public string? Label { get; set; } | ||||
|         public Instant? ClearedAt { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me/statuses")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Status>> GetCurrentStatus() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var status = await events.GetStatus(currentUser.Id); | ||||
|         return Ok(status); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("me/statuses")] | ||||
|     [Authorize] | ||||
|     [RequiredPermission("global", "accounts.statuses.create")] | ||||
|     public async Task<ActionResult<Status>> CreateStatus([FromBody] StatusRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var status = new Status | ||||
|         { | ||||
|             AccountId = currentUser.Id, | ||||
|             Attitude = request.Attitude, | ||||
|             IsInvisible = request.IsInvisible, | ||||
|             IsNotDisturb = request.IsNotDisturb, | ||||
|             Label = request.Label, | ||||
|             ClearedAt = request.ClearedAt | ||||
|         }; | ||||
|  | ||||
|         return await events.CreateStatus(currentUser, status); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("me/statuses")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> DeleteStatus() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
|             .Where(s => s.AccountId == currentUser.Id) | ||||
|             .Where(s => s.ClearedAt == null || s.ClearedAt > now) | ||||
|             .OrderByDescending(s => s.CreatedAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (status is null) return NotFound(); | ||||
|          | ||||
|         await events.ClearStatus(currentUser, status); | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("search")] | ||||
|     public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20) | ||||
|     { | ||||
|   | ||||
							
								
								
									
										79
									
								
								DysonNetwork.Sphere/Account/AccountEventService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								DysonNetwork.Sphere/Account/AccountEventService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| using DysonNetwork.Sphere.Activity; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Caching.Memory; | ||||
| using NodaTime; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
|  | ||||
| public class AccountEventService(AppDatabase db, ActivityService act, WebSocketService ws, IMemoryCache cache) | ||||
| { | ||||
|     private const string StatusCacheKey = "account_status_"; | ||||
|  | ||||
|     public async Task<Status> GetStatus(long userId) | ||||
|     { | ||||
|         var cacheKey = $"{StatusCacheKey}{userId}"; | ||||
|         if (cache.TryGetValue(cacheKey, out Status? cachedStatus)) | ||||
|             return cachedStatus!; | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
|             .Where(e => e.AccountId == userId) | ||||
|             .Where(e => e.ClearedAt == null || e.ClearedAt > now) | ||||
|             .OrderByDescending(e => e.CreatedAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (status is not null) | ||||
|         { | ||||
|             cache.Set(cacheKey, status, TimeSpan.FromMinutes(5)); | ||||
|             return status; | ||||
|         } | ||||
|  | ||||
|         var isOnline = ws.GetAccountIsConnected(userId); | ||||
|         if (isOnline) | ||||
|         { | ||||
|             return new Status | ||||
|             { | ||||
|                 Attitude = StatusAttitude.Neutral, | ||||
|                 IsOnline = true, | ||||
|                 Label = "Online", | ||||
|                 AccountId = userId, | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return new Status | ||||
|         { | ||||
|             Attitude = StatusAttitude.Neutral, | ||||
|             IsOnline = false, | ||||
|             Label = "Offline", | ||||
|             AccountId = userId, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public async Task<Status> CreateStatus(Account user, Status status) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         await db.AccountStatuses | ||||
|             .Where(x => x.AccountId == user.Id && (x.ClearedAt == null || x.ClearedAt > now)) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(x => x.ClearedAt, now)); | ||||
|  | ||||
|         db.AccountStatuses.Add(status); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         await act.CreateActivity( | ||||
|             user, | ||||
|             "accounts.status", | ||||
|             $"account.statuses/{status.Id}", | ||||
|             ActivityVisibility.Friends | ||||
|         ); | ||||
|  | ||||
|         return status; | ||||
|     } | ||||
|  | ||||
|     public async Task ClearStatus(Account user, Status status) | ||||
|     { | ||||
|         status.ClearedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|         db.Update(status); | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								DysonNetwork.Sphere/Account/Status.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								DysonNetwork.Sphere/Account/Status.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
|  | ||||
| public enum StatusAttitude | ||||
| { | ||||
|     Positive, | ||||
|     Negative, | ||||
|     Neutral | ||||
| } | ||||
|  | ||||
| public class Status : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     public StatusAttitude Attitude { get; set; } | ||||
|     [NotMapped] public bool IsOnline { get; set; } | ||||
|     public bool IsInvisible { get; set; } | ||||
|     public bool IsNotDisturb { get; set; } | ||||
|     [MaxLength(1024)] public string? Label { get; set; } | ||||
|     public Instant? ClearedAt { get; set; } | ||||
|      | ||||
|     public long AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
| } | ||||
| @@ -30,6 +30,7 @@ public class AppDatabase( | ||||
|     public DbSet<Account.AccountContact> AccountContacts { get; set; } | ||||
|     public DbSet<Account.AccountAuthFactor> AccountAuthFactors { get; set; } | ||||
|     public DbSet<Account.Relationship> AccountRelationships { get; set; } | ||||
|     public DbSet<Account.Status> AccountStatuses { get; set; } | ||||
|     public DbSet<Account.Notification> Notifications { get; set; } | ||||
|     public DbSet<Account.NotificationPushSubscription> NotificationPushSubscriptions { get; set; } | ||||
|  | ||||
| @@ -87,7 +88,8 @@ public class AppDatabase( | ||||
|                         PermissionService.NewPermissionNode("group:default", "global", "files.create", true), | ||||
|                         PermissionService.NewPermissionNode("group:default", "global", "chat.create", true), | ||||
|                         PermissionService.NewPermissionNode("group:default", "global", "chat.messages.create", true), | ||||
|                         PermissionService.NewPermissionNode("group:default", "global", "chat.realtime.create", true) | ||||
|                         PermissionService.NewPermissionNode("group:default", "global", "chat.realtime.create", true), | ||||
|                         PermissionService.NewPermissionNode("group:default", "global", "accounts.statuses.create", true) | ||||
|                     } | ||||
|                 }); | ||||
|                 await context.SaveChangesAsync(cancellationToken); | ||||
|   | ||||
| @@ -41,6 +41,11 @@ public class WebSocketService | ||||
|         ActiveConnections.TryRemove(key, out _); | ||||
|     } | ||||
|  | ||||
|     public bool GetAccountIsConnected(long accountId) | ||||
|     { | ||||
|         return ActiveConnections.Any(c => c.Key.AccountId == accountId); | ||||
|     } | ||||
|  | ||||
|     public void SendPacketToAccount(long userId, WebSocketPacket packet) | ||||
|     { | ||||
|         var connections = ActiveConnections.Where(c => c.Key.AccountId == userId); | ||||
|   | ||||
							
								
								
									
										2500
									
								
								DysonNetwork.Sphere/Migrations/20250506172100_AddAccountStatuses.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2500
									
								
								DysonNetwork.Sphere/Migrations/20250506172100_AddAccountStatuses.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,54 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddAccountStatuses : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "account_statuses", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     attitude = table.Column<int>(type: "integer", nullable: false), | ||||
|                     is_invisible = table.Column<bool>(type: "boolean", nullable: false), | ||||
|                     is_not_disturb = table.Column<bool>(type: "boolean", nullable: false), | ||||
|                     label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), | ||||
|                     cleared_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||
|                     account_id = table.Column<long>(type: "bigint", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_account_statuses", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_account_statuses_accounts_account_id", | ||||
|                         column: x => x.account_id, | ||||
|                         principalTable: "accounts", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_account_statuses_account_id", | ||||
|                 table: "account_statuses", | ||||
|                 column: "account_id"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "account_statuses"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -454,6 +454,59 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                     b.ToTable("account_relationships", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<int>("Attitude") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("attitude"); | ||||
|  | ||||
|                     b.Property<Instant?>("ClearedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("cleared_at"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<bool>("IsInvisible") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_invisible"); | ||||
|  | ||||
|                     b.Property<bool>("IsNotDisturb") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_not_disturb"); | ||||
|  | ||||
|                     b.Property<string>("Label") | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("label"); | ||||
|  | ||||
|                     b.Property<Instant>("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.Sphere.Activity.Activity", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
| @@ -1920,6 +1973,18 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                     b.Navigation("Related"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_statuses_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Activity.Activity", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|   | ||||
| @@ -131,6 +131,7 @@ builder.Services.AddScoped<WebSocketService>(); | ||||
| builder.Services.AddScoped<EmailService>(); | ||||
| builder.Services.AddScoped<PermissionService>(); | ||||
| builder.Services.AddScoped<AccountService>(); | ||||
| builder.Services.AddScoped<AccountEventService>(); | ||||
| builder.Services.AddScoped<RelationshipService>(); | ||||
| builder.Services.AddScoped<MagicSpellService>(); | ||||
| builder.Services.AddScoped<NotificationService>(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user