From bf181b88ecd44214e39755861829ba185daea242 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 19 Aug 2025 15:16:25 +0800 Subject: [PATCH] :sparkles: Account bot basis --- DysonNetwork.Develop/AppDatabase.cs | 2 + DysonNetwork.Develop/Identity/BotAccount.cs | 46 ++++ .../Identity/BotAccountController.cs | 197 ++++++++++++++++++ .../Identity/BotAccountService.cs | 57 +++++ .../Startup/ServiceCollectionExtensions.cs | 1 + DysonNetwork.Pass/Account/Account.cs | 3 + DysonNetwork.Pass/Account/AccountService.cs | 45 ++-- .../Account/BotAccountReceiverGrpc.cs | 77 +++++++ DysonNetwork.Pass/Program.cs | 1 - .../Startup/ApplicationConfiguration.cs | 1 + DysonNetwork.Shared/Proto/GrpcClientHelper.cs | 35 ++-- DysonNetwork.Shared/Proto/develop.proto | 64 +++++- .../Registry/ServiceInjectionHelper.cs | 14 ++ 13 files changed, 512 insertions(+), 31 deletions(-) create mode 100644 DysonNetwork.Develop/Identity/BotAccount.cs create mode 100644 DysonNetwork.Develop/Identity/BotAccountController.cs create mode 100644 DysonNetwork.Develop/Identity/BotAccountService.cs create mode 100644 DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs diff --git a/DysonNetwork.Develop/AppDatabase.cs b/DysonNetwork.Develop/AppDatabase.cs index d90234d..a74cead 100644 --- a/DysonNetwork.Develop/AppDatabase.cs +++ b/DysonNetwork.Develop/AppDatabase.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using DysonNetwork.Develop.Identity; using DysonNetwork.Develop.Project; using Microsoft.EntityFrameworkCore; @@ -16,6 +17,7 @@ public class AppDatabase( public DbSet CustomApps { get; set; } = null!; public DbSet CustomAppSecrets { get; set; } = null!; + public DbSet BotAccounts { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/DysonNetwork.Develop/Identity/BotAccount.cs b/DysonNetwork.Develop/Identity/BotAccount.cs new file mode 100644 index 0000000..0e8d55e --- /dev/null +++ b/DysonNetwork.Develop/Identity/BotAccount.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using DysonNetwork.Develop.Project; +using DysonNetwork.Shared.Data; +using NodaTime; +using NodaTime.Serialization.Protobuf; + +namespace DysonNetwork.Develop.Identity; + +public class BotAccount : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + [MaxLength(1024)] public string Slug { get; set; } = null!; + + public bool IsActive { get; set; } = true; + + public Guid ProjectId { get; set; } + public DevProject Project { get; set; } = null!; + + public Shared.Proto.BotAccount ToProtoValue() + { + var proto = new Shared.Proto.BotAccount + { + Slug = Slug, + IsActive = IsActive, + AutomatedId = Id.ToString(), + CreatedAt = CreatedAt.ToTimestamp(), + UpdatedAt = UpdatedAt.ToTimestamp() + }; + + return proto; + } + + public static BotAccount FromProto(Shared.Proto.BotAccount proto) + { + var botAccount = new BotAccount + { + Id = Guid.Parse(proto.AutomatedId), + Slug = proto.Slug, + IsActive = proto.IsActive, + CreatedAt = proto.CreatedAt.ToInstant(), + UpdatedAt = proto.UpdatedAt.ToInstant() + }; + + return botAccount; + } +} \ No newline at end of file diff --git a/DysonNetwork.Develop/Identity/BotAccountController.cs b/DysonNetwork.Develop/Identity/BotAccountController.cs new file mode 100644 index 0000000..8e96826 --- /dev/null +++ b/DysonNetwork.Develop/Identity/BotAccountController.cs @@ -0,0 +1,197 @@ +using System.ComponentModel.DataAnnotations; +using DysonNetwork.Develop.Project; +using DysonNetwork.Shared.Proto; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DysonNetwork.Develop.Identity; + +[ApiController] +[Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")] +[Authorize] +public class BotAccountController( + BotAccountService botService, + DeveloperService developerService, + DevProjectService projectService, + ILogger logger +) + : ControllerBase +{ + public record BotRequest( + [Required] [MaxLength(1024)] string? Slug + ); + + public record UpdateBotRequest( + [MaxLength(1024)] string? Slug, + bool? IsActive + ) : BotRequest(Slug); + + [HttpGet] + public async Task ListBots( + [FromRoute] string pubName, + [FromRoute] Guid projectId) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); + + var developer = await developerService.GetDeveloperByName(pubName); + if (developer is null) + return NotFound("Developer not found"); + + if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), + PublisherMemberRole.Editor)) + return StatusCode(403, "You must be an editor of the developer to list bots"); + + var project = await projectService.GetProjectAsync(projectId, developer.Id); + if (project is null) + return NotFound("Project not found or you don't have access"); + + var bots = await botService.GetBotsByProjectAsync(projectId); + return Ok(bots); + } + + [HttpGet("{botId:guid}")] + public async Task GetBot( + [FromRoute] string pubName, + [FromRoute] Guid projectId, + [FromRoute] Guid botId) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); + + var developer = await developerService.GetDeveloperByName(pubName); + if (developer is null) + return NotFound("Developer not found"); + + if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), + PublisherMemberRole.Editor)) + return StatusCode(403, "You must be an editor of the developer to view bot details"); + + var project = await projectService.GetProjectAsync(projectId, developer.Id); + if (project is null) + return NotFound("Project not found or you don't have access"); + + var bot = await botService.GetBotByIdAsync(botId); + if (bot is null || bot.ProjectId != projectId) + return NotFound("Bot not found"); + + return Ok(bot); + } + + [HttpPost] + public async Task CreateBot( + [FromRoute] string pubName, + [FromRoute] Guid projectId, + [FromBody] BotRequest request + ) + { + if (string.IsNullOrWhiteSpace(request.Slug)) + return BadRequest("Name is required"); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); + + var developer = await developerService.GetDeveloperByName(pubName); + if (developer is null) + return NotFound("Developer not found"); + + if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), + PublisherMemberRole.Editor)) + return StatusCode(403, "You must be an editor of the developer to create a bot"); + + var project = await projectService.GetProjectAsync(projectId, developer.Id); + if (project is null) + return NotFound("Project not found or you don't have access"); + + try + { + var bot = await botService.CreateBotAsync(project, request.Slug); + return Ok(bot); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating bot account"); + return StatusCode(500, "An error occurred while creating the bot account"); + } + } + + [HttpPut("{botId:guid}")] + public async Task UpdateBot( + [FromRoute] string pubName, + [FromRoute] Guid projectId, + [FromRoute] Guid botId, + [FromBody] UpdateBotRequest request + ) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); + + var developer = await developerService.GetDeveloperByName(pubName); + if (developer is null) + return NotFound("Developer not found"); + + if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), + PublisherMemberRole.Editor)) + return StatusCode(403, "You must be an editor of the developer to update a bot"); + + var project = await projectService.GetProjectAsync(projectId, developer.Id); + if (project is null) + return NotFound("Project not found or you don't have access"); + + var bot = await botService.GetBotByIdAsync(botId); + if (bot is null || bot.ProjectId != projectId) + return NotFound("Bot not found"); + + try + { + var updatedBot = await botService.UpdateBotAsync( + bot, + request.Slug, + request.IsActive + ); + + return Ok(updatedBot); + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating bot account {BotId}", botId); + return StatusCode(500, "An error occurred while updating the bot account"); + } + } + + [HttpDelete("{botId:guid}")] + public async Task DeleteBot( + [FromRoute] string pubName, + [FromRoute] Guid projectId, + [FromRoute] Guid botId) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); + + var developer = await developerService.GetDeveloperByName(pubName); + if (developer is null) + return NotFound("Developer not found"); + + if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), + PublisherMemberRole.Editor)) + return StatusCode(403, "You must be an editor of the developer to delete a bot"); + + var project = await projectService.GetProjectAsync(projectId, developer.Id); + if (project is null) + return NotFound("Project not found or you don't have access"); + + var bot = await botService.GetBotByIdAsync(botId); + if (bot is null || bot.ProjectId != projectId) + return NotFound("Bot not found"); + + try + { + await botService.DeleteBotAsync(bot); + return NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting bot {BotId}", botId); + return StatusCode(500, "An error occurred while deleting the bot account"); + } + } +} \ No newline at end of file diff --git a/DysonNetwork.Develop/Identity/BotAccountService.cs b/DysonNetwork.Develop/Identity/BotAccountService.cs new file mode 100644 index 0000000..ee9a551 --- /dev/null +++ b/DysonNetwork.Develop/Identity/BotAccountService.cs @@ -0,0 +1,57 @@ +using DysonNetwork.Develop.Project; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Develop.Identity; + +public class BotAccountService(AppDatabase db, ILogger logger) +{ + public async Task GetBotByIdAsync(Guid id) + { + return await db.BotAccounts + .Include(b => b.Project) + .FirstOrDefaultAsync(b => b.Id == id); + } + + public async Task> GetBotsByProjectAsync(Guid projectId) + { + return await db.BotAccounts + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + } + + public async Task CreateBotAsync(DevProject project, string slug) + { + var bot = new BotAccount + { + Slug = slug, + ProjectId = project.Id, + Project = project, + IsActive = true + }; + + db.BotAccounts.Add(bot); + await db.SaveChangesAsync(); + + return bot; + } + + public async Task UpdateBotAsync(BotAccount bot, string? slug = null, bool? isActive = null) + { + if (slug != null) + bot.Slug = slug; + if (isActive.HasValue) + bot.IsActive = isActive.Value; + + bot.UpdatedAt = SystemClock.Instance.GetCurrentInstant(); + await db.SaveChangesAsync(); + + return bot; + } + + public async Task DeleteBotAsync(BotAccount bot) + { + db.BotAccounts.Remove(bot); + await db.SaveChangesAsync(); + } +} diff --git a/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs index faa5b7b..84f9023 100644 --- a/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs @@ -52,6 +52,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/DysonNetwork.Pass/Account/Account.cs b/DysonNetwork.Pass/Account/Account.cs index 158c1c2..5f378e1 100644 --- a/DysonNetwork.Pass/Account/Account.cs +++ b/DysonNetwork.Pass/Account/Account.cs @@ -20,6 +20,9 @@ public class Account : ModelBase [MaxLength(32)] public string Language { get; set; } = string.Empty; public Instant? ActivatedAt { get; set; } public bool IsSuperuser { get; set; } = false; + + // The ID is the BotAccount ID in the DysonNetwork.Develop + public Guid? AutomatedId { get; set; } public AccountProfile Profile { get; set; } = null!; public ICollection Contacts { get; set; } = new List(); diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index 777e864..4959b71 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -178,6 +178,29 @@ public class AccountService( ); } + public async Task CreateBotAccount(Account account, Guid automatedId) + { + var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); + if (dupeAutomateCount > 0) + throw new InvalidOperationException("Automated ID has already been used."); + + var dupeNameCount = await db.Accounts.Where(a => a.Name == account.Name).CountAsync(); + if (dupeNameCount > 0) + throw new InvalidOperationException("Account name has already been taken."); + + account.AutomatedId = automatedId; + account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); + account.IsSuperuser = false; + db.Accounts.Add(account); + await db.SaveChangesAsync(); + return account; + } + + public async Task GetBotAccount(Guid automatedId) + { + return await db.Accounts.FirstOrDefaultAsync(a => a.AutomatedId == automatedId); + } + public async Task RequestAccountDeletion(Account account) { var spell = await spells.CreateMagicSpell( @@ -665,21 +688,13 @@ public class AccountService( } } - /// - /// The maintenance method for server administrator. - /// To check every user has an account profile and to create them if it isn't having one. - /// - public async Task EnsureAccountProfileCreated() + public async Task DeleteAccount(Account account) { - var accountsId = await db.Accounts.Select(a => a.Id).ToListAsync(); - var existingId = await db.AccountProfiles.Select(p => p.AccountId).ToListAsync(); - var missingId = accountsId.Except(existingId).ToList(); - - if (missingId.Count != 0) - { - var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id }) - .ToList(); - await db.BulkInsertAsync(newProfiles); - } + await db.AuthSessions + .Where(s => s.AccountId == account.Id) + .ExecuteDeleteAsync(); + + db.Accounts.Remove(account); + await db.SaveChangesAsync(); } } \ No newline at end of file diff --git a/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs b/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs new file mode 100644 index 0000000..87c24bf --- /dev/null +++ b/DysonNetwork.Pass/Account/BotAccountReceiverGrpc.cs @@ -0,0 +1,77 @@ +using DysonNetwork.Shared.Proto; +using Grpc.Core; +using NodaTime; +using NodaTime.Serialization.Protobuf; + +namespace DysonNetwork.Pass.Account; + +public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts) + : BotAccountReceiverService.BotAccountReceiverServiceBase +{ + public override async Task CreateBotAccount( + CreateBotAccountRequest request, + ServerCallContext context + ) + { + var account = Account.FromProtoValue(request.Account); + account = await accounts.CreateBotAccount(account, Guid.Parse(request.AutomatedId)); + + return new CreateBotAccountResponse + { + Bot = new BotAccount + { + Account = account.ToProtoValue(), + AutomatedId = account.Id.ToString(), + CreatedAt = account.CreatedAt.ToTimestamp(), + UpdatedAt = account.UpdatedAt.ToTimestamp(), + IsActive = true + } + }; + } + + public override async Task UpdateBotAccount( + UpdateBotAccountRequest request, + 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")); + + 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(); + + return new UpdateBotAccountResponse + { + Bot = new BotAccount + { + Account = account.ToProtoValue(), + AutomatedId = account.Id.ToString(), + CreatedAt = account.CreatedAt.ToTimestamp(), + UpdatedAt = account.UpdatedAt.ToTimestamp(), + IsActive = true + } + }; + } + + public override async Task DeleteBotAccount( + DeleteBotAccountRequest request, + 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")); + + await accounts.DeleteAccount(account); + + return new DeleteBotAccountResponse(); + } +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Program.cs b/DysonNetwork.Pass/Program.cs index 34f3379..1b10786 100644 --- a/DysonNetwork.Pass/Program.cs +++ b/DysonNetwork.Pass/Program.cs @@ -1,5 +1,4 @@ using DysonNetwork.Pass; -using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Pages.Data; using DysonNetwork.Pass.Startup; using DysonNetwork.Shared.Http; diff --git a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs index 82f956a..38d299c 100644 --- a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs @@ -76,6 +76,7 @@ public static class ApplicationConfiguration app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcService(); + app.MapGrpcService(); return app; } diff --git a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs index 11589cf..3eab47f 100644 --- a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs +++ b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs @@ -32,12 +32,9 @@ public static class GrpcClientHelper private static async Task GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName) { var response = await etcdClient.GetAsync($"/services/{serviceName}"); - if (response.Kvs.Count == 0) - { - throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd."); - } - - return response.Kvs[0].Value.ToStringUtf8(); + return response.Kvs.Count == 0 + ? throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.") + : response.Kvs[0].Value.ToStringUtf8(); } public static async Task CreateAccountServiceClient( @@ -51,7 +48,21 @@ public static class GrpcClientHelper return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } - + + public static async Task + CreateBotAccountReceiverServiceClient( + IEtcdClient etcdClient, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass"); + return new BotAccountReceiverService.BotAccountReceiverServiceClient(CreateCallInvoker(url, clientCertPath, + clientKeyPath, + clientCertPassword)); + } + public static async Task CreateActionLogServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -75,7 +86,7 @@ public static class GrpcClientHelper return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } - + public static async Task CreatePermissionServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -99,7 +110,7 @@ public static class GrpcClientHelper return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } - + public static async Task CreateFileServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -111,7 +122,7 @@ public static class GrpcClientHelper return new FileService.FileServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } - + public static async Task CreateFileReferenceServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -123,7 +134,7 @@ public static class GrpcClientHelper return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } - + public static async Task CreatePublisherServiceClient( IEtcdClient etcdClient, string clientCertPath, @@ -147,4 +158,4 @@ public static class GrpcClientHelper return new CustomAppService.CustomAppServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } - } \ No newline at end of file +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Proto/develop.proto b/DysonNetwork.Shared/Proto/develop.proto index bb4067d..e94580e 100644 --- a/DysonNetwork.Shared/Proto/develop.proto +++ b/DysonNetwork.Shared/Proto/develop.proto @@ -5,6 +5,7 @@ package proto; option csharp_namespace = "DysonNetwork.Shared.Proto"; import "google/protobuf/timestamp.proto"; +import "account.proto"; import "file.proto"; message CustomAppOauthConfig { @@ -25,7 +26,7 @@ enum CustomAppStatus { SUSPENDED = 4; } - message CustomApp { +message CustomApp { string id = 1; string slug = 2; string name = 3; @@ -43,7 +44,7 @@ enum CustomAppStatus { google.protobuf.Timestamp created_at = 11; google.protobuf.Timestamp updated_at = 12; - } +} message CustomAppSecret { string id = 1; @@ -85,4 +86,61 @@ message CheckCustomAppSecretResponse { service CustomAppService { rpc GetCustomApp(GetCustomAppRequest) returns (GetCustomAppResponse); rpc CheckCustomAppSecret(CheckCustomAppSecretRequest) returns (CheckCustomAppSecretResponse); -} \ No newline at end of file +} + +// BotAccount represents a bot account in the system +// It extends the base Account with bot-specific fields +message BotAccount { + // Base account information + Account account = 1; + + // Bot-specific information + string slug = 2; // Unique identifier for the bot + bool is_active = 3; // Whether the bot is currently active + string automated_id = 5; // The bot ID + + // Timestamps + google.protobuf.Timestamp created_at = 6; + google.protobuf.Timestamp updated_at = 7; +} + +// Request/Response messages for BotAccount operations +message CreateBotAccountRequest { + Account account = 1; + string automated_id = 2; +} + +message CreateBotAccountResponse { + BotAccount bot = 1; // The created bot account +} + +message UpdateBotAccountRequest { + string automated_id = 1; // ID of the bot account to update + Account account = 2; // Updated account information +} + +message UpdateBotAccountResponse { + BotAccount bot = 1; // The updated bot account +} + +message DeleteBotAccountRequest { + string automated_id = 1; // ID of the bot account to delete + bool force = 2; // Whether to force delete (bypass soft delete) +} + +message DeleteBotAccountResponse { + bool success = 1; // Whether the deletion was successful +} + +// This service should be implemented by the Pass service to handle the creation, update, and deletion of bot accounts +service BotAccountReceiverService { + // Create a new bot account + rpc CreateBotAccount(CreateBotAccountRequest) returns (CreateBotAccountResponse); + + // Update an existing bot account + rpc UpdateBotAccount(UpdateBotAccountRequest) returns (UpdateBotAccountResponse); + + // Delete a bot account + rpc DeleteBotAccount(DeleteBotAccountRequest) returns (DeleteBotAccountResponse); +} + \ No newline at end of file diff --git a/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs b/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs index 8d56332..f1f9ade 100644 --- a/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs +++ b/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs @@ -43,6 +43,20 @@ public static class ServiceInjectionHelper }); services.AddSingleton(); + services.AddSingleton(sp => + { + var etcdClient = sp.GetRequiredService(); + var config = sp.GetRequiredService(); + var clientCertPath = config["Service:ClientCert"]!; + var clientKeyPath = config["Service:ClientKey"]!; + var clientCertPassword = config["Service:CertPassword"]; + + return GrpcClientHelper + .CreateBotAccountReceiverServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword) + .GetAwaiter() + .GetResult(); + }); + services.AddSingleton(sp => { var etcdClient = sp.GetRequiredService();