♻️ Refind bot account

This commit is contained in:
2025-08-23 13:00:30 +08:00
parent fb7e52d6f3
commit 5d7429a416
15 changed files with 691 additions and 71 deletions

View File

@@ -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()
{

View File

@@ -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<BotAccountController> logger
ILogger<BotAccountController> 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<IActionResult> 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<IActionResult> 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
);

View File

@@ -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<BotAccount?> GetBotByIdAsync(Guid id)
{
@@ -23,39 +28,23 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco
.ToListAsync();
}
public async Task<BotAccount> CreateBotAsync(DevProject project, string slug)
public async Task<BotAccount> 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<BotAccount> UpdateBotAsync(BotAccount bot, string? slug = null, bool? isActive = null)
public async Task<BotAccount> 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<BotAccount?> LoadBotAccountAsync(BotAccount bot) =>
(await LoadBotsAccountAsync([bot])).FirstOrDefault();
public async Task<List<BotAccount>> LoadBotsAccountAsync(IEnumerable<BotAccount> 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<BotAccount> ?? [];
}
}

View File

@@ -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<DevProject> Projects { get; set; } = [];
[JsonIgnore] public List<DevProject> Projects { get; set; } = [];
[NotMapped] public PublisherInfo? Publisher { get; set; }
}

View File

@@ -24,7 +24,7 @@
},
"Service": {
"Name": "DysonNetwork.Develop",
"Url": "https://localhost:7099",
"Url": "https://localhost:7192",
"ClientCert": "../Certificates/client.crt",
"ClientKey": "../Certificates/client.key"
}