Compare commits

...

2 Commits

Author SHA1 Message Date
LittleSheep
7f7b47fb1c Invoke bot reciever service in Bot 2025-08-19 15:48:19 +08:00
LittleSheep
bf181b88ec Account bot basis 2025-08-19 15:16:35 +08:00
13 changed files with 623 additions and 31 deletions

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using DysonNetwork.Develop.Identity; using DysonNetwork.Develop.Identity;
using DysonNetwork.Develop.Project; using DysonNetwork.Develop.Project;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -16,6 +17,7 @@ public class AppDatabase(
public DbSet<CustomApp> CustomApps { get; set; } = null!; public DbSet<CustomApp> CustomApps { get; set; } = null!;
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!; public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
public DbSet<BotAccount> BotAccounts { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

View File

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

View File

@@ -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<BotAccountController> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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");
}
}
}

View File

@@ -0,0 +1,168 @@
using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Proto;
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 async Task<BotAccount?> GetBotByIdAsync(Guid id)
{
return await db.BotAccounts
.Include(b => b.Project)
.FirstOrDefaultAsync(b => b.Id == id);
}
public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId)
{
return await db.BotAccounts
.Where(b => b.ProjectId == projectId)
.ToListAsync();
}
public async Task<BotAccount> CreateBotAsync(DevProject project, string slug)
{
// 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()
}
};
var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest);
var botAccount = createResponse.Bot;
// Then create the local bot account
var bot = new BotAccount
{
Id = Guid.Parse(botAccount.AutomatedId),
Slug = slug,
ProjectId = project.Id,
Project = project,
IsActive = botAccount.IsActive,
CreatedAt = botAccount.CreatedAt.ToInstant(),
UpdatedAt = botAccount.UpdatedAt.ToInstant()
};
db.BotAccounts.Add(bot);
await db.SaveChangesAsync();
return bot;
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
{
throw new InvalidOperationException("A bot account with this ID already exists in the authentication service.", ex);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
throw new ArgumentException($"Invalid bot account data: {ex.Status.Detail}", ex);
}
catch (RpcException ex)
{
throw new Exception($"Failed to create bot account: {ex.Status.Detail}", ex);
}
}
public async Task<BotAccount> UpdateBotAsync(BotAccount bot, string? slug = null, bool? isActive = null)
{
var updated = false;
if (slug != null && bot.Slug != slug)
{
bot.Slug = slug;
updated = true;
}
if (isActive.HasValue && bot.IsActive != isActive.Value)
{
bot.IsActive = isActive.Value;
updated = true;
}
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()
}
};
var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest);
var updatedBot = updateResponse.Bot;
// Update local bot account
bot.UpdatedAt = updatedBot.UpdatedAt.ToInstant();
bot.IsActive = updatedBot.IsActive;
await db.SaveChangesAsync();
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
throw new Exception("Bot account not found in the authentication service", ex);
}
catch (RpcException ex)
{
throw new Exception($"Failed to update bot account: {ex.Status.Detail}", ex);
}
return bot;
}
public async Task DeleteBotAsync(BotAccount bot)
{
try
{
// Delete the bot account from the Pass service
var deleteRequest = new DeleteBotAccountRequest
{
AutomatedId = bot.Id.ToString(),
Force = false
};
await accountReceiver.DeleteBotAccountAsync(deleteRequest);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
// Account not found in Pass service, continue with local deletion
}
// Delete the local bot account
db.BotAccounts.Remove(bot);
await db.SaveChangesAsync();
}
}

View File

@@ -52,6 +52,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<DeveloperService>(); services.AddScoped<DeveloperService>();
services.AddScoped<CustomAppService>(); services.AddScoped<CustomAppService>();
services.AddScoped<DevProjectService>(); services.AddScoped<DevProjectService>();
services.AddScoped<BotAccountService>();
return services; return services;
} }

View File

@@ -20,6 +20,9 @@ public class Account : ModelBase
[MaxLength(32)] public string Language { get; set; } = string.Empty; [MaxLength(32)] public string Language { get; set; } = string.Empty;
public Instant? ActivatedAt { get; set; } public Instant? ActivatedAt { get; set; }
public bool IsSuperuser { get; set; } = false; 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 AccountProfile Profile { get; set; } = null!;
public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>(); public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>();

View File

@@ -178,6 +178,29 @@ public class AccountService(
); );
} }
public async Task<Account> 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<Account?> GetBotAccount(Guid automatedId)
{
return await db.Accounts.FirstOrDefaultAsync(a => a.AutomatedId == automatedId);
}
public async Task RequestAccountDeletion(Account account) public async Task RequestAccountDeletion(Account account)
{ {
var spell = await spells.CreateMagicSpell( var spell = await spells.CreateMagicSpell(
@@ -665,21 +688,13 @@ public class AccountService(
} }
} }
/// <summary> public async Task DeleteAccount(Account account)
/// The maintenance method for server administrator.
/// To check every user has an account profile and to create them if it isn't having one.
/// </summary>
public async Task EnsureAccountProfileCreated()
{ {
var accountsId = await db.Accounts.Select(a => a.Id).ToListAsync(); await db.AuthSessions
var existingId = await db.AccountProfiles.Select(p => p.AccountId).ToListAsync(); .Where(s => s.AccountId == account.Id)
var missingId = accountsId.Except(existingId).ToList(); .ExecuteDeleteAsync();
if (missingId.Count != 0) db.Accounts.Remove(account);
{ await db.SaveChangesAsync();
var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id })
.ToList();
await db.BulkInsertAsync(newProfiles);
}
} }
} }

View File

@@ -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<CreateBotAccountResponse> 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<UpdateBotAccountResponse> 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<DeleteBotAccountResponse> 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();
}
}

View File

@@ -1,5 +1,4 @@
using DysonNetwork.Pass; using DysonNetwork.Pass;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Pages.Data; using DysonNetwork.Pass.Pages.Data;
using DysonNetwork.Pass.Startup; using DysonNetwork.Pass.Startup;
using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Http;

View File

@@ -76,6 +76,7 @@ public static class ApplicationConfiguration
app.MapGrpcService<AuthServiceGrpc>(); app.MapGrpcService<AuthServiceGrpc>();
app.MapGrpcService<ActionLogServiceGrpc>(); app.MapGrpcService<ActionLogServiceGrpc>();
app.MapGrpcService<PermissionServiceGrpc>(); app.MapGrpcService<PermissionServiceGrpc>();
app.MapGrpcService<BotAccountReceiverGrpc>();
return app; return app;
} }

View File

@@ -32,12 +32,9 @@ public static class GrpcClientHelper
private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName) private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName)
{ {
var response = await etcdClient.GetAsync($"/services/{serviceName}"); var response = await etcdClient.GetAsync($"/services/{serviceName}");
if (response.Kvs.Count == 0) return response.Kvs.Count == 0
{ ? throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.")
throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd."); : response.Kvs[0].Value.ToStringUtf8();
}
return response.Kvs[0].Value.ToStringUtf8();
} }
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient( public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
@@ -51,7 +48,21 @@ public static class GrpcClientHelper
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword)); clientCertPassword));
} }
public static async Task<BotAccountReceiverService.BotAccountReceiverServiceClient>
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<ActionLogService.ActionLogServiceClient> CreateActionLogServiceClient( public static async Task<ActionLogService.ActionLogServiceClient> CreateActionLogServiceClient(
IEtcdClient etcdClient, IEtcdClient etcdClient,
string clientCertPath, string clientCertPath,
@@ -75,7 +86,7 @@ public static class GrpcClientHelper
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword)); clientCertPassword));
} }
public static async Task<PermissionService.PermissionServiceClient> CreatePermissionServiceClient( public static async Task<PermissionService.PermissionServiceClient> CreatePermissionServiceClient(
IEtcdClient etcdClient, IEtcdClient etcdClient,
string clientCertPath, string clientCertPath,
@@ -99,7 +110,7 @@ public static class GrpcClientHelper
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword)); clientCertPassword));
} }
public static async Task<FileService.FileServiceClient> CreateFileServiceClient( public static async Task<FileService.FileServiceClient> CreateFileServiceClient(
IEtcdClient etcdClient, IEtcdClient etcdClient,
string clientCertPath, string clientCertPath,
@@ -111,7 +122,7 @@ public static class GrpcClientHelper
return new FileService.FileServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, return new FileService.FileServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword)); clientCertPassword));
} }
public static async Task<FileReferenceService.FileReferenceServiceClient> CreateFileReferenceServiceClient( public static async Task<FileReferenceService.FileReferenceServiceClient> CreateFileReferenceServiceClient(
IEtcdClient etcdClient, IEtcdClient etcdClient,
string clientCertPath, string clientCertPath,
@@ -123,7 +134,7 @@ public static class GrpcClientHelper
return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, return new FileReferenceService.FileReferenceServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword)); clientCertPassword));
} }
public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient( public static async Task<PublisherService.PublisherServiceClient> CreatePublisherServiceClient(
IEtcdClient etcdClient, IEtcdClient etcdClient,
string clientCertPath, string clientCertPath,
@@ -147,4 +158,4 @@ public static class GrpcClientHelper
return new CustomAppService.CustomAppServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, return new CustomAppService.CustomAppServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword)); clientCertPassword));
} }
} }

View File

@@ -5,6 +5,7 @@ package proto;
option csharp_namespace = "DysonNetwork.Shared.Proto"; option csharp_namespace = "DysonNetwork.Shared.Proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "account.proto";
import "file.proto"; import "file.proto";
message CustomAppOauthConfig { message CustomAppOauthConfig {
@@ -25,7 +26,7 @@ enum CustomAppStatus {
SUSPENDED = 4; SUSPENDED = 4;
} }
message CustomApp { message CustomApp {
string id = 1; string id = 1;
string slug = 2; string slug = 2;
string name = 3; string name = 3;
@@ -43,7 +44,7 @@ enum CustomAppStatus {
google.protobuf.Timestamp created_at = 11; google.protobuf.Timestamp created_at = 11;
google.protobuf.Timestamp updated_at = 12; google.protobuf.Timestamp updated_at = 12;
} }
message CustomAppSecret { message CustomAppSecret {
string id = 1; string id = 1;
@@ -85,4 +86,61 @@ message CheckCustomAppSecretResponse {
service CustomAppService { service CustomAppService {
rpc GetCustomApp(GetCustomAppRequest) returns (GetCustomAppResponse); rpc GetCustomApp(GetCustomAppRequest) returns (GetCustomAppResponse);
rpc CheckCustomAppSecret(CheckCustomAppSecretRequest) returns (CheckCustomAppSecretResponse); rpc CheckCustomAppSecret(CheckCustomAppSecretRequest) returns (CheckCustomAppSecretResponse);
} }
// 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);
}

View File

@@ -43,6 +43,20 @@ public static class ServiceInjectionHelper
}); });
services.AddSingleton<AccountClientHelper>(); services.AddSingleton<AccountClientHelper>();
services.AddSingleton<BotAccountReceiverService.BotAccountReceiverServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
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<ActionLogService.ActionLogServiceClient>(sp => services.AddSingleton<ActionLogService.ActionLogServiceClient>(sp =>
{ {
var etcdClient = sp.GetRequiredService<IEtcdClient>(); var etcdClient = sp.GetRequiredService<IEtcdClient>();