From 5d7429a416de53ee3a877091aee9e79b2f9f060f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 23 Aug 2025 13:00:30 +0800 Subject: [PATCH] :recycle: Refind bot account --- DysonNetwork.Develop/Identity/BotAccount.cs | 4 +- .../Identity/BotAccountController.cs | 112 ++++++- .../Identity/BotAccountService.cs | 71 ++-- DysonNetwork.Develop/Identity/Developer.cs | 3 +- DysonNetwork.Develop/appsettings.json | 2 +- DysonNetwork.Pass/Account/Account.cs | 6 +- .../Account/AccountCurrentController.cs | 1 + DysonNetwork.Pass/Account/AccountService.cs | 35 +- .../Account/AccountServiceGrpc.cs | 57 +++- .../Account/BotAccountReceiverGrpc.cs | 67 +++- DysonNetwork.Shared/Data/Account.cs | 309 ++++++++++++++++++ DysonNetwork.Shared/Data/Subscription.cs | 64 ++++ DysonNetwork.Shared/Proto/account.proto | 12 + DysonNetwork.Shared/Proto/develop.proto | 4 + .../Registry/AccountClientHelper.cs | 15 + 15 files changed, 691 insertions(+), 71 deletions(-) create mode 100644 DysonNetwork.Shared/Data/Account.cs create mode 100644 DysonNetwork.Shared/Data/Subscription.cs diff --git a/DysonNetwork.Develop/Identity/BotAccount.cs b/DysonNetwork.Develop/Identity/BotAccount.cs index 0e8d55e..bbd12bc 100644 --- a/DysonNetwork.Develop/Identity/BotAccount.cs +++ b/DysonNetwork.Develop/Identity/BotAccount.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using DysonNetwork.Develop.Project; using DysonNetwork.Shared.Data; -using NodaTime; using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Develop.Identity; @@ -15,6 +15,8 @@ public class BotAccount : ModelBase public Guid ProjectId { get; set; } public DevProject Project { get; set; } = null!; + + [NotMapped] public AccountReference? Account { get; set; } public Shared.Proto.BotAccount ToProtoValue() { diff --git a/DysonNetwork.Develop/Identity/BotAccountController.cs b/DysonNetwork.Develop/Identity/BotAccountController.cs index 8e96826..f08b76e 100644 --- a/DysonNetwork.Develop/Identity/BotAccountController.cs +++ b/DysonNetwork.Develop/Identity/BotAccountController.cs @@ -1,8 +1,11 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Develop.Project; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Registry; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NodaTime; +using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Develop.Identity; @@ -13,18 +16,61 @@ public class BotAccountController( BotAccountService botService, DeveloperService developerService, DevProjectService projectService, - ILogger logger + ILogger logger, + AccountClientHelper accounts ) : ControllerBase { - public record BotRequest( - [Required] [MaxLength(1024)] string? Slug - ); + public class CommonBotRequest + { + [MaxLength(256)] public string? FirstName { get; set; } + [MaxLength(256)] public string? MiddleName { get; set; } + [MaxLength(256)] public string? LastName { get; set; } + [MaxLength(1024)] public string? Gender { get; set; } + [MaxLength(1024)] public string? Pronouns { get; set; } + [MaxLength(1024)] public string? TimeZone { get; set; } + [MaxLength(1024)] public string? Location { get; set; } + [MaxLength(4096)] public string? Bio { get; set; } + public Instant? Birthday { get; set; } - public record UpdateBotRequest( - [MaxLength(1024)] string? Slug, - bool? IsActive - ) : BotRequest(Slug); + [MaxLength(32)] public string? PictureId { get; set; } + [MaxLength(32)] public string? BackgroundId { get; set; } + } + + public class BotCreateRequest : CommonBotRequest + { + [Required] + [MinLength(2)] + [MaxLength(256)] + [RegularExpression(@"^[A-Za-z0-9_-]+$", + ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") + ] + public string Name { get; set; } = string.Empty; + + [Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty; + + [Required] [MaxLength(1024)] public string Slug { get; set; } = string.Empty; + + [MaxLength(128)] public string Language { get; set; } = "en-us"; + } + + public class UpdateBotRequest : CommonBotRequest + { + [MinLength(2)] + [MaxLength(256)] + [RegularExpression(@"^[A-Za-z0-9_-]+$", + ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") + ] + public string? Name { get; set; } = string.Empty; + + [MaxLength(256)] public string? Nick { get; set; } = string.Empty; + + [Required] [MaxLength(1024)] public string Slug { get; set; } = string.Empty; + + [MaxLength(128)] public string? Language { get; set; } + + public bool? IsActive { get; set; } + } [HttpGet] public async Task ListBots( @@ -47,7 +93,7 @@ public class BotAccountController( return NotFound("Project not found or you don't have access"); var bots = await botService.GetBotsByProjectAsync(projectId); - return Ok(bots); + return Ok(await botService.LoadBotsAccountAsync(bots)); } [HttpGet("{botId:guid}")] @@ -75,18 +121,16 @@ public class BotAccountController( if (bot is null || bot.ProjectId != projectId) return NotFound("Bot not found"); - return Ok(bot); + return Ok(await botService.LoadBotAccountAsync(bot)); } [HttpPost] public async Task CreateBot( [FromRoute] string pubName, [FromRoute] Guid projectId, - [FromBody] BotRequest request + [FromBody] BotCreateRequest createRequest ) { - if (string.IsNullOrWhiteSpace(request.Slug)) - return BadRequest("Name is required"); if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); @@ -102,9 +146,30 @@ public class BotAccountController( if (project is null) return NotFound("Project not found or you don't have access"); + var account = new Account() + { + Name = createRequest.Name, + Nick = createRequest.Nick, + Language = createRequest.Language, + Profile = new AccountProfile() + { + Bio = createRequest.Bio, + Gender = createRequest.Gender, + FirstName = createRequest.FirstName, + MiddleName = createRequest.MiddleName, + LastName = createRequest.LastName, + TimeZone = createRequest.TimeZone, + Pronouns = createRequest.Pronouns, + Location = createRequest.Location, + Birthday = createRequest.Birthday?.ToTimestamp(), + Picture = new CloudFile() { Id = createRequest.PictureId }, + Background = new CloudFile() { Id = createRequest.BackgroundId } + } + }; + try { - var bot = await botService.CreateBotAsync(project, request.Slug); + var bot = await botService.CreateBotAsync(project, createRequest.Slug, account); return Ok(bot); } catch (Exception ex) @@ -141,10 +206,29 @@ public class BotAccountController( if (bot is null || bot.ProjectId != projectId) return NotFound("Bot not found"); + var botAccount = await accounts.GetBotAccount(bot.Id); + + if (request.Name is not null) botAccount.Name = request.Name; + if (request.Nick is not null) botAccount.Nick = request.Nick; + if (request.Language is not null) botAccount.Language = request.Language; + if (request.Bio is not null) botAccount.Profile.Bio = request.Bio; + if (request.Gender is not null) botAccount.Profile.Gender = request.Gender; + if (request.FirstName is not null) botAccount.Profile.FirstName = request.FirstName; + if (request.MiddleName is not null) botAccount.Profile.MiddleName = request.MiddleName; + if (request.LastName is not null) botAccount.Profile.LastName = request.LastName; + if (request.TimeZone is not null) botAccount.Profile.TimeZone = request.TimeZone; + if (request.Pronouns is not null) botAccount.Profile.Pronouns = request.Pronouns; + if (request.Location is not null) botAccount.Profile.Location = request.Location; + if (request.Birthday is not null) botAccount.Profile.Birthday = request.Birthday?.ToTimestamp(); + if (request.PictureId is not null) botAccount.Profile.Picture = new CloudFile() { Id = request.PictureId }; + if (request.BackgroundId is not null) + botAccount.Profile.Background = new CloudFile() { Id = request.BackgroundId }; + try { var updatedBot = await botService.UpdateBotAsync( bot, + botAccount, request.Slug, request.IsActive ); diff --git a/DysonNetwork.Develop/Identity/BotAccountService.cs b/DysonNetwork.Develop/Identity/BotAccountService.cs index 410171c..2e3e082 100644 --- a/DysonNetwork.Develop/Identity/BotAccountService.cs +++ b/DysonNetwork.Develop/Identity/BotAccountService.cs @@ -1,13 +1,18 @@ using DysonNetwork.Develop.Project; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Registry; using Grpc.Core; using Microsoft.EntityFrameworkCore; -using NodaTime; using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Develop.Identity; -public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver) +public class BotAccountService( + AppDatabase db, + BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver, + AccountClientHelper accounts +) { public async Task GetBotByIdAsync(Guid id) { @@ -23,39 +28,23 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco .ToListAsync(); } - public async Task CreateBotAsync(DevProject project, string slug) + public async Task CreateBotAsync(DevProject project, string slug, Account account) { // First, check if a bot with this slug already exists in this project var existingBot = await db.BotAccounts .FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug); - + if (existingBot != null) { throw new InvalidOperationException("A bot with this slug already exists in this project."); } - var now = SystemClock.Instance.GetCurrentInstant(); - try { - // First create the bot account in the Pass service var createRequest = new CreateBotAccountRequest { AutomatedId = Guid.NewGuid().ToString(), - Account = new Account - { - Name = slug, - Nick = $"Bot {slug}", - Language = "en", - Profile = new AccountProfile - { - Id = Guid.NewGuid().ToString(), - CreatedAt = now.ToTimestamp(), - UpdatedAt = now.ToTimestamp() - }, - CreatedAt = now.ToTimestamp(), - UpdatedAt = now.ToTimestamp() - } + Account = account }; var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest); @@ -80,7 +69,8 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco } catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists) { - throw new InvalidOperationException("A bot account with this ID already exists in the authentication service.", ex); + throw new InvalidOperationException( + "A bot account with this ID already exists in the authentication service.", ex); } catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument) { @@ -92,7 +82,8 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco } } - public async Task UpdateBotAsync(BotAccount bot, string? slug = null, bool? isActive = null) + public async Task UpdateBotAsync(BotAccount bot, Account account, string? slug = null, + bool? isActive = null) { var updated = false; if (slug != null && bot.Slug != slug) @@ -100,7 +91,7 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco bot.Slug = slug; updated = true; } - + if (isActive.HasValue && bot.IsActive != isActive.Value) { bot.IsActive = isActive.Value; @@ -108,19 +99,14 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco } if (!updated) return bot; - + try { // Update the bot account in the Pass service var updateRequest = new UpdateBotAccountRequest { AutomatedId = bot.Id.ToString(), - Account = new Shared.Proto.Account - { - Name = $"bot-{bot.Slug}", - Nick = $"Bot {bot.Slug}", - UpdatedAt = SystemClock.Instance.GetCurrentInstant().ToTimestamp() - } + Account = account }; var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest); @@ -160,9 +146,28 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco { // Account not found in Pass service, continue with local deletion } - + // Delete the local bot account db.BotAccounts.Remove(bot); await db.SaveChangesAsync(); } -} + + public async Task LoadBotAccountAsync(BotAccount bot) => + (await LoadBotsAccountAsync([bot])).FirstOrDefault(); + + public async Task> LoadBotsAccountAsync(IEnumerable bots) + { + bots = bots.ToList(); + var automatedIds = bots.Select(b => b.Id).ToList(); + var data = await accounts.GetBotAccountBatch(automatedIds); + + foreach (var bot in bots) + { + bot.Account = data + .Select(AccountReference.FromProtoValue) + .FirstOrDefault(e => e.AutomatedId == bot.Id); + } + + return bots as List ?? []; + } +} \ No newline at end of file diff --git a/DysonNetwork.Develop/Identity/Developer.cs b/DysonNetwork.Develop/Identity/Developer.cs index f8f5b2c..ef618aa 100644 --- a/DysonNetwork.Develop/Identity/Developer.cs +++ b/DysonNetwork.Develop/Identity/Developer.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; using DysonNetwork.Develop.Project; using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Data; @@ -11,7 +12,7 @@ public class Developer public Guid Id { get; set; } = Guid.NewGuid(); public Guid PublisherId { get; set; } - public List Projects { get; set; } = []; + [JsonIgnore] public List Projects { get; set; } = []; [NotMapped] public PublisherInfo? Publisher { get; set; } } diff --git a/DysonNetwork.Develop/appsettings.json b/DysonNetwork.Develop/appsettings.json index 77eb08b..1d407a2 100644 --- a/DysonNetwork.Develop/appsettings.json +++ b/DysonNetwork.Develop/appsettings.json @@ -24,7 +24,7 @@ }, "Service": { "Name": "DysonNetwork.Develop", - "Url": "https://localhost:7099", + "Url": "https://localhost:7192", "ClientCert": "../Certificates/client.crt", "ClientKey": "../Certificates/client.key" } diff --git a/DysonNetwork.Pass/Account/Account.cs b/DysonNetwork.Pass/Account/Account.cs index dab201a..eab1d2b 100644 --- a/DysonNetwork.Pass/Account/Account.cs +++ b/DysonNetwork.Pass/Account/Account.cs @@ -51,7 +51,8 @@ public class Account : ModelBase Profile = Profile.ToProtoValue(), PerkSubscription = PerkSubscription?.ToProtoValue(), CreatedAt = CreatedAt.ToTimestamp(), - UpdatedAt = UpdatedAt.ToTimestamp() + UpdatedAt = UpdatedAt.ToTimestamp(), + AutomatedId = AutomatedId?.ToString() }; // Add contacts @@ -81,6 +82,7 @@ public class Account : ModelBase : null, CreatedAt = proto.CreatedAt.ToInstant(), UpdatedAt = proto.UpdatedAt.ToInstant(), + AutomatedId = proto.AutomatedId is not null ? Guid.Parse(proto.AutomatedId) : null }; account.Profile = AccountProfile.FromProtoValue(proto.Profile); @@ -119,7 +121,7 @@ public abstract class Leveling public class AccountProfile : ModelBase, IIdentifiedResource { - public Guid Id { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); [MaxLength(256)] public string? FirstName { get; set; } [MaxLength(256)] public string? MiddleName { get; set; } [MaxLength(256)] public string? LastName { get; set; } diff --git a/DysonNetwork.Pass/Account/AccountCurrentController.cs b/DysonNetwork.Pass/Account/AccountCurrentController.cs index ae27229..3bd3cc7 100644 --- a/DysonNetwork.Pass/Account/AccountCurrentController.cs +++ b/DysonNetwork.Pass/Account/AccountCurrentController.cs @@ -30,6 +30,7 @@ public class AccountCurrentController( { [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> GetCurrentIdentity() { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index 704796a..edcd9d7 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -6,6 +6,7 @@ using DysonNetwork.Pass.Email; using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Permission; using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Stream; using EFCore.BulkExtensions; @@ -21,6 +22,8 @@ namespace DysonNetwork.Pass.Account; public class AccountService( AppDatabase db, MagicSpellService spells, + FileService.FileServiceClient files, + FileReferenceService.FileReferenceServiceClient fileRefs, AccountUsernameService uname, EmailService mailer, PusherService.PusherServiceClient pusher, @@ -182,7 +185,7 @@ public class AccountService( ); } - public async Task CreateBotAccount(Account account, Guid automatedId) + public async Task CreateBotAccount(Account account, Guid automatedId, string? pictureId, string? backgroundId) { var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); if (dupeAutomateCount > 0) @@ -195,8 +198,38 @@ public class AccountService( account.AutomatedId = automatedId; account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); account.IsSuperuser = false; + + if (!string.IsNullOrEmpty(pictureId)) + { + var file = await files.GetFileAsync(new GetFileRequest { Id = pictureId }); + await fileRefs.CreateReferenceAsync( + new CreateReferenceRequest + { + ResourceId = account.Profile.ResourceIdentifier, + FileId = pictureId, + Usage = "profile.picture" + } + ); + account.Profile.Picture = CloudFileReferenceObject.FromProtoValue(file); + } + + if (!string.IsNullOrEmpty(backgroundId)) + { + var file = await files.GetFileAsync(new GetFileRequest { Id = backgroundId }); + await fileRefs.CreateReferenceAsync( + new CreateReferenceRequest + { + ResourceId = account.Profile.ResourceIdentifier, + FileId = backgroundId, + Usage = "profile.background" + } + ); + account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file); + } + db.Accounts.Add(account); await db.SaveChangesAsync(); + return account; } diff --git a/DysonNetwork.Pass/Account/AccountServiceGrpc.cs b/DysonNetwork.Pass/Account/AccountServiceGrpc.cs index 0daa9de..f63be80 100644 --- a/DysonNetwork.Pass/Account/AccountServiceGrpc.cs +++ b/DysonNetwork.Pass/Account/AccountServiceGrpc.cs @@ -42,6 +42,26 @@ public class AccountServiceGrpc( return account.ToProtoValue(); } + public override async Task GetBotAccount(GetBotAccountRequest request, + ServerCallContext context) + { + if (!Guid.TryParse(request.AutomatedId, out var automatedId)) + throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid automated ID format")); + + var account = await _db.Accounts + .AsNoTracking() + .Include(a => a.Profile) + .FirstOrDefaultAsync(a => a.AutomatedId == automatedId); + + if (account == null) + throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account with automated ID {request.AutomatedId} not found")); + + var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id); + account.PerkSubscription = perk?.ToReference(); + + return account.ToProtoValue(); + } + public override async Task GetAccountBatch(GetAccountBatchRequest request, ServerCallContext context) { @@ -56,7 +76,35 @@ public class AccountServiceGrpc( .Where(a => accountIds.Contains(a.Id)) .Include(a => a.Profile) .ToListAsync(); - + + var perks = await subscriptions.GetPerkSubscriptionsAsync( + accounts.Select(x => x.Id).ToList() + ); + foreach (var account in accounts) + if (perks.TryGetValue(account.Id, out var perk)) + account.PerkSubscription = perk?.ToReference(); + + var response = new GetAccountBatchResponse(); + response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); + return response; + } + + + public override async Task GetBotAccountBatch(GetBotAccountBatchRequest request, + ServerCallContext context) + { + var automatedIds = request.AutomatedId + .Select(id => Guid.TryParse(id, out var automatedId) ? automatedId : (Guid?)null) + .Where(id => id.HasValue) + .Select(id => id!.Value) + .ToList(); + + var accounts = await _db.Accounts + .AsNoTracking() + .Where(a => a.AutomatedId != null && automatedIds.Contains(a.AutomatedId.Value)) + .Include(a => a.Profile) + .ToListAsync(); + var perks = await subscriptions.GetPerkSubscriptionsAsync( accounts.Select(x => x.Id).ToList() ); @@ -76,7 +124,8 @@ public class AccountServiceGrpc( return status.ToProtoValue(); } - public override async Task GetAccountStatusBatch(GetAccountBatchRequest request, ServerCallContext context) + public override async Task GetAccountStatusBatch(GetAccountBatchRequest request, + ServerCallContext context) { var accountIds = request.Id .Select(id => Guid.TryParse(id, out var accountId) ? accountId : (Guid?)null) @@ -98,14 +147,14 @@ public class AccountServiceGrpc( .Where(a => accountNames.Contains(a.Name)) .Include(a => a.Profile) .ToListAsync(); - + var perks = await subscriptions.GetPerkSubscriptionsAsync( accounts.Select(x => x.Id).ToList() ); foreach (var account in accounts) if (perks.TryGetValue(account.Id, out var perk)) account.PerkSubscription = perk?.ToReference(); - + var response = new GetAccountBatchResponse(); response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); return response; diff --git a/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs b/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs index 87c24bf..243810a 100644 --- a/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs +++ b/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs @@ -1,3 +1,4 @@ +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; using Grpc.Core; using NodaTime; @@ -5,7 +6,12 @@ using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Pass.Account; -public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts) +public class BotAccountReceiverGrpc( + AppDatabase db, + AccountService accounts, + FileService.FileServiceClient files, + FileReferenceService.FileReferenceServiceClient fileRefs +) : BotAccountReceiverService.BotAccountReceiverServiceBase { public override async Task CreateBotAccount( @@ -14,7 +20,12 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts) ) { var account = Account.FromProtoValue(request.Account); - account = await accounts.CreateBotAccount(account, Guid.Parse(request.AutomatedId)); + account = await accounts.CreateBotAccount( + account, + Guid.Parse(request.AutomatedId), + request.PictureId, + request.BackgroundId + ); return new CreateBotAccountResponse { @@ -34,16 +45,44 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts) ServerCallContext context ) { - var automatedId = Guid.Parse(request.AutomatedId); - var account = await accounts.GetBotAccount(automatedId); - if (account is null) - throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found")); + var account = Account.FromProtoValue(request.Account); + + if (request.PictureId is not null) + { + var file = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); + if (account.Profile.Picture is not null) + await fileRefs.DeleteResourceReferencesAsync( + new DeleteResourceReferencesRequest { ResourceId = account.Profile.ResourceIdentifier } + ); + await fileRefs.CreateReferenceAsync( + new CreateReferenceRequest + { + ResourceId = account.Profile.ResourceIdentifier, + FileId = request.PictureId, + Usage = "profile.picture" + } + ); + account.Profile.Picture = CloudFileReferenceObject.FromProtoValue(file); + } + + if (request.BackgroundId is not null) + { + var file = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); + if (account.Profile.Background is not null) + await fileRefs.DeleteResourceReferencesAsync( + new DeleteResourceReferencesRequest { ResourceId = account.Profile.ResourceIdentifier } + ); + await fileRefs.CreateReferenceAsync( + new CreateReferenceRequest + { + ResourceId = account.Profile.ResourceIdentifier, + FileId = request.BackgroundId, + Usage = "profile.background" + } + ); + account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file); + } - account.Name = request.Account.Name; - account.Nick = request.Account.Nick; - account.Profile = AccountProfile.FromProtoValue(request.Account.Profile); - account.Language = request.Account.Language; - db.Accounts.Update(account); await db.SaveChangesAsync(); @@ -56,7 +95,7 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts) CreatedAt = account.CreatedAt.ToTimestamp(), UpdatedAt = account.UpdatedAt.ToTimestamp(), IsActive = true - } + } }; } @@ -69,9 +108,9 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts) var account = await accounts.GetBotAccount(automatedId); if (account is null) throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found")); - + await accounts.DeleteAccount(account); - + return new DeleteBotAccountResponse(); } } \ No newline at end of file diff --git a/DysonNetwork.Shared/Data/Account.cs b/DysonNetwork.Shared/Data/Account.cs new file mode 100644 index 0000000..cb70a70 --- /dev/null +++ b/DysonNetwork.Shared/Data/Account.cs @@ -0,0 +1,309 @@ +using DysonNetwork.Shared.Proto; +using NodaTime; +using NodaTime.Serialization.Protobuf; + +namespace DysonNetwork.Shared.Data; + +public class AccountReference +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Nick { get; set; } = string.Empty; + public string Language { get; set; } = string.Empty; + public Instant? ActivatedAt { get; set; } + public bool IsSuperuser { get; set; } + public Guid? AutomatedId { get; set; } + public AccountProfileReference Profile { get; set; } = null!; + public List Contacts { get; set; } = new(); + public List Badges { get; set; } = new(); + public SubscriptionReference? PerkSubscription { get; set; } + public Instant CreatedAt { get; set; } + public Instant UpdatedAt { get; set; } + + public Proto.Account ToProtoValue() + { + var proto = new Proto.Account + { + Id = Id.ToString(), + Name = Name, + Nick = Nick, + Language = Language, + ActivatedAt = ActivatedAt?.ToTimestamp(), + IsSuperuser = IsSuperuser, + Profile = Profile.ToProtoValue(), + PerkSubscription = PerkSubscription?.ToProtoValue(), + CreatedAt = CreatedAt.ToTimestamp(), + UpdatedAt = UpdatedAt.ToTimestamp() + }; + + foreach (var contact in Contacts) + proto.Contacts.Add(contact.ToProtoValue()); + + foreach (var badge in Badges) + proto.Badges.Add(badge.ToProtoValue()); + + return proto; + } + + public static AccountReference FromProtoValue(Proto.Account proto) + { + var account = new AccountReference + { + Id = Guid.Parse(proto.Id), + Name = proto.Name, + Nick = proto.Nick, + Language = proto.Language, + ActivatedAt = proto.ActivatedAt?.ToInstant(), + IsSuperuser = proto.IsSuperuser, + AutomatedId = string.IsNullOrEmpty(proto.AutomatedId) ? null : Guid.Parse(proto.AutomatedId), + PerkSubscription = proto.PerkSubscription != null + ? SubscriptionReference.FromProtoValue(proto.PerkSubscription) + : null, + CreatedAt = proto.CreatedAt.ToInstant(), + UpdatedAt = proto.UpdatedAt.ToInstant() + }; + + account.Profile = AccountProfileReference.FromProtoValue(proto.Profile); + + foreach (var contactProto in proto.Contacts) + account.Contacts.Add(AccountContactReference.FromProtoValue(contactProto)); + + foreach (var badgeProto in proto.Badges) + account.Badges.Add(AccountBadgeReference.FromProtoValue(badgeProto)); + + return account; + } +} + +public class AccountProfileReference +{ + public Guid Id { get; set; } + public string? FirstName { get; set; } + public string? MiddleName { get; set; } + public string? LastName { get; set; } + public string? Bio { get; set; } + public string? Gender { get; set; } + public string? Pronouns { get; set; } + public string? TimeZone { get; set; } + public string? Location { get; set; } + public List? Links { get; set; } + public Instant? Birthday { get; set; } + public Instant? LastSeenAt { get; set; } + public VerificationMark? Verification { get; set; } + public int Experience { get; set; } + public int Level => Leveling.ExperiencePerLevel.Count(xp => Experience >= xp) - 1; + public double SocialCredits { get; set; } = 100; + + public int SocialCreditsLevel => SocialCredits switch + { + < 100 => -1, + > 100 and < 200 => 0, + < 200 => 1, + _ => 2 + }; + + public double LevelingProgress => Level >= Leveling.ExperiencePerLevel.Count - 1 + ? 100 + : (Experience - Leveling.ExperiencePerLevel[Level]) * 100.0 / + (Leveling.ExperiencePerLevel[Level + 1] - Leveling.ExperiencePerLevel[Level]); + + public CloudFileReferenceObject? Picture { get; set; } + public CloudFileReferenceObject? Background { get; set; } + public Guid AccountId { get; set; } + + public Proto.AccountProfile ToProtoValue() + { + var proto = new Proto.AccountProfile + { + Id = Id.ToString(), + FirstName = FirstName ?? string.Empty, + MiddleName = MiddleName ?? string.Empty, + LastName = LastName ?? string.Empty, + Bio = Bio ?? string.Empty, + Gender = Gender ?? string.Empty, + Pronouns = Pronouns ?? string.Empty, + TimeZone = TimeZone ?? string.Empty, + Location = Location ?? string.Empty, + Birthday = Birthday?.ToTimestamp(), + LastSeenAt = LastSeenAt?.ToTimestamp(), + Experience = Experience, + Level = Level, + LevelingProgress = LevelingProgress, + SocialCredits = SocialCredits, + SocialCreditsLevel = SocialCreditsLevel, + Picture = Picture?.ToProtoValue(), + Background = Background?.ToProtoValue(), + AccountId = AccountId.ToString(), + Verification = Verification?.ToProtoValue(), + }; + + return proto; + } + + public static AccountProfileReference FromProtoValue(Proto.AccountProfile proto) + { + return new AccountProfileReference + { + Id = Guid.Parse(proto.Id), + FirstName = string.IsNullOrEmpty(proto.FirstName) ? null : proto.FirstName, + MiddleName = string.IsNullOrEmpty(proto.MiddleName) ? null : proto.MiddleName, + LastName = string.IsNullOrEmpty(proto.LastName) ? null : proto.LastName, + Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio, + Gender = string.IsNullOrEmpty(proto.Gender) ? null : proto.Gender, + Pronouns = string.IsNullOrEmpty(proto.Pronouns) ? null : proto.Pronouns, + TimeZone = string.IsNullOrEmpty(proto.TimeZone) ? null : proto.TimeZone, + Location = string.IsNullOrEmpty(proto.Location) ? null : proto.Location, + Birthday = proto.Birthday?.ToInstant(), + LastSeenAt = proto.LastSeenAt?.ToInstant(), + Experience = proto.Experience, + SocialCredits = proto.SocialCredits, + Picture = proto.Picture != null ? CloudFileReferenceObject.FromProtoValue(proto.Picture) : null, + Background = proto.Background != null ? CloudFileReferenceObject.FromProtoValue(proto.Background) : null, + AccountId = Guid.Parse(proto.AccountId), + Verification = proto.Verification != null ? VerificationMark.FromProtoValue(proto.Verification) : null, + }; + } +} + +public class AccountContactReference : ModelBase +{ + public Guid Id { get; set; } + public AccountContactReferenceType Type { get; set; } + public Instant? VerifiedAt { get; set; } + public bool IsPrimary { get; set; } = false; + public bool IsPublic { get; set; } = false; + public string Content { get; set; } = string.Empty; + + public Guid AccountId { get; set; } + + public Shared.Proto.AccountContact ToProtoValue() + { + var proto = new Shared.Proto.AccountContact + { + Id = Id.ToString(), + Type = Type switch + { + AccountContactReferenceType.Email => Shared.Proto.AccountContactType.Email, + AccountContactReferenceType.PhoneNumber => Shared.Proto.AccountContactType.PhoneNumber, + AccountContactReferenceType.Address => Shared.Proto.AccountContactType.Address, + _ => Shared.Proto.AccountContactType.Unspecified + }, + Content = Content, + IsPrimary = IsPrimary, + VerifiedAt = VerifiedAt?.ToTimestamp(), + AccountId = AccountId.ToString(), + CreatedAt = CreatedAt.ToTimestamp(), + UpdatedAt = UpdatedAt.ToTimestamp() + }; + + return proto; + } + + public static AccountContactReference FromProtoValue(Shared.Proto.AccountContact proto) + { + var contact = new AccountContactReference + { + Id = Guid.Parse(proto.Id), + AccountId = Guid.Parse(proto.AccountId), + Type = proto.Type switch + { + Shared.Proto.AccountContactType.Email => AccountContactReferenceType.Email, + Shared.Proto.AccountContactType.PhoneNumber => AccountContactReferenceType.PhoneNumber, + Shared.Proto.AccountContactType.Address => AccountContactReferenceType.Address, + _ => AccountContactReferenceType.Email + }, + Content = proto.Content, + IsPrimary = proto.IsPrimary, + VerifiedAt = proto.VerifiedAt?.ToInstant(), + CreatedAt = proto.CreatedAt.ToInstant(), + UpdatedAt = proto.UpdatedAt.ToInstant() + }; + + return contact; + } +} + +public enum AccountContactReferenceType +{ + Email, + PhoneNumber, + Address +} + +public class AccountBadgeReference : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Type { get; set; } = null!; + public string? Label { get; set; } + public string? Caption { get; set; } + public Dictionary Meta { get; set; } = new(); + public Instant? ActivatedAt { get; set; } + public Instant? ExpiredAt { get; set; } + + public Guid AccountId { get; set; } + + public AccountBadge ToProtoValue() + { + var proto = new AccountBadge + { + Id = Id.ToString(), + Type = Type, + Label = Label ?? string.Empty, + Caption = Caption ?? string.Empty, + ActivatedAt = ActivatedAt?.ToTimestamp(), + ExpiredAt = ExpiredAt?.ToTimestamp(), + AccountId = AccountId.ToString(), + CreatedAt = CreatedAt.ToTimestamp(), + UpdatedAt = UpdatedAt.ToTimestamp() + }; + proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta)); + + return proto; + } + + public static AccountBadgeReference FromProtoValue(AccountBadge proto) + { + var badge = new AccountBadgeReference + { + Id = Guid.Parse(proto.Id), + AccountId = Guid.Parse(proto.AccountId), + Type = proto.Type, + Label = proto.Label, + Caption = proto.Caption, + ActivatedAt = proto.ActivatedAt?.ToInstant(), + ExpiredAt = proto.ExpiredAt?.ToInstant(), + CreatedAt = proto.CreatedAt.ToInstant(), + UpdatedAt = proto.UpdatedAt.ToInstant() + }; + + return badge; + } +} + +public class ProfileLinkReference +{ + public string Name { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; +} + +public static class Leveling +{ + public static readonly List ExperiencePerLevel = + [ + 0, // Level 0 + 100, // Level 1 + 250, // Level 2 + 500, // Level 3 + 1000, // Level 4 + 2000, // Level 5 + 4000, // Level 6 + 8000, // Level 7 + 16000, // Level 8 + 32000, // Level 9 + 64000, // Level 10 + 128000, // Level 11 + 256000, // Level 12 + 512000, // Level 13 + 1024000 + ]; +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Data/Subscription.cs b/DysonNetwork.Shared/Data/Subscription.cs new file mode 100644 index 0000000..0dcea37 --- /dev/null +++ b/DysonNetwork.Shared/Data/Subscription.cs @@ -0,0 +1,64 @@ +using NodaTime; +using NodaTime.Serialization.Protobuf; + +namespace DysonNetwork.Shared.Data; + +public class SubscriptionReference +{ + public Guid Id { get; set; } + public string Identifier { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public bool IsActive { get; set; } + public bool IsAvailable { get; set; } + public Instant BegunAt { get; set; } + public Instant? EndedAt { get; set; } + public Instant? RenewalAt { get; set; } + public SubscriptionReferenceStatus Status { get; set; } + + public static SubscriptionReference FromProtoValue(Proto.SubscriptionReferenceObject proto) + { + return new SubscriptionReference + { + Id = Guid.Parse(proto.Id), + Identifier = proto.Identifier, + DisplayName = proto.DisplayName, + IsActive = proto.IsActive, + IsAvailable = proto.IsAvailable, + BegunAt = proto.BegunAt.ToInstant(), + EndedAt = proto.EndedAt?.ToInstant(), + RenewalAt = proto.RenewalAt?.ToInstant(), + Status = (SubscriptionReferenceStatus)proto.Status + }; + } + + public Proto.SubscriptionReferenceObject ToProtoValue() + { + return new Proto.SubscriptionReferenceObject + { + Id = Id.ToString(), + Identifier = Identifier, + DisplayName = DisplayName, + IsActive = IsActive, + IsAvailable = IsAvailable, + BegunAt = BegunAt.ToTimestamp(), + EndedAt = EndedAt?.ToTimestamp(), + RenewalAt = RenewalAt?.ToTimestamp(), + Status = Status switch + { + SubscriptionReferenceStatus.Unpaid => Proto.SubscriptionStatus.Unpaid, + SubscriptionReferenceStatus.Active => Proto.SubscriptionStatus.Active, + SubscriptionReferenceStatus.Expired => Proto.SubscriptionStatus.Expired, + SubscriptionReferenceStatus.Cancelled => Proto.SubscriptionStatus.Cancelled, + _ => Proto.SubscriptionStatus.Unpaid + } + }; + } +} + +public enum SubscriptionReferenceStatus +{ + Unpaid = 0, + Active = 1, + Expired = 2, + Cancelled = 3 +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Proto/account.proto b/DysonNetwork.Shared/Proto/account.proto index 2de26ee..0dc5637 100644 --- a/DysonNetwork.Shared/Proto/account.proto +++ b/DysonNetwork.Shared/Proto/account.proto @@ -32,6 +32,8 @@ message Account { google.protobuf.Timestamp created_at = 14; google.protobuf.Timestamp updated_at = 15; + + optional string automated_id = 17; } // Enum for status attitude @@ -246,7 +248,9 @@ message GetAccountStatusBatchResponse { service AccountService { // Account Operations rpc GetAccount(GetAccountRequest) returns (Account) {} + rpc GetBotAccount(GetBotAccountRequest) returns (Account) {} rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} + rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {} @@ -321,10 +325,18 @@ message GetAccountRequest { string id = 1; // Account ID to retrieve } +message GetBotAccountRequest { + string automated_id = 1; +} + message GetAccountBatchRequest { repeated string id = 1; // Account ID to retrieve } +message GetBotAccountBatchRequest { + repeated string automated_id = 1; +} + message LookupAccountBatchRequest { repeated string names = 1; } diff --git a/DysonNetwork.Shared/Proto/develop.proto b/DysonNetwork.Shared/Proto/develop.proto index e94580e..ff3f0aa 100644 --- a/DysonNetwork.Shared/Proto/develop.proto +++ b/DysonNetwork.Shared/Proto/develop.proto @@ -108,6 +108,8 @@ message BotAccount { message CreateBotAccountRequest { Account account = 1; string automated_id = 2; + optional string picture_id = 8; + optional string background_id = 9; } message CreateBotAccountResponse { @@ -117,6 +119,8 @@ message CreateBotAccountResponse { message UpdateBotAccountRequest { string automated_id = 1; // ID of the bot account to update Account account = 2; // Updated account information + optional string picture_id = 8; + optional string background_id = 9; } message UpdateBotAccountResponse { diff --git a/DysonNetwork.Shared/Registry/AccountClientHelper.cs b/DysonNetwork.Shared/Registry/AccountClientHelper.cs index d816add..edbc356 100644 --- a/DysonNetwork.Shared/Registry/AccountClientHelper.cs +++ b/DysonNetwork.Shared/Registry/AccountClientHelper.cs @@ -11,6 +11,13 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts) var response = await accounts.GetAccountAsync(request); return response; } + + public async Task GetBotAccount(Guid automatedId) + { + var request = new GetBotAccountRequest { AutomatedId = automatedId.ToString() }; + var response = await accounts.GetBotAccountAsync(request); + return response; + } public async Task> GetAccountBatch(List ids) { @@ -19,6 +26,14 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts) var response = await accounts.GetAccountBatchAsync(request); return response.Accounts.ToList(); } + + public async Task> GetBotAccountBatch(List automatedIds) + { + var request = new GetBotAccountBatchRequest(); + request.AutomatedId.AddRange(automatedIds.Select(id => id.ToString())); + var response = await accounts.GetBotAccountBatchAsync(request); + return response.Accounts.ToList(); + } public async Task> GetAccountStatusBatch(List ids) {