♻️ 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;
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Develop.Project; using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
using NodaTime;
using NodaTime.Serialization.Protobuf; using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Develop.Identity; namespace DysonNetwork.Develop.Identity;
@@ -15,6 +15,8 @@ public class BotAccount : ModelBase
public Guid ProjectId { get; set; } public Guid ProjectId { get; set; }
public DevProject Project { get; set; } = null!; public DevProject Project { get; set; } = null!;
[NotMapped] public AccountReference? Account { get; set; }
public Shared.Proto.BotAccount ToProtoValue() public Shared.Proto.BotAccount ToProtoValue()
{ {

View File

@@ -1,8 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Develop.Project; using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Develop.Identity; namespace DysonNetwork.Develop.Identity;
@@ -13,18 +16,61 @@ public class BotAccountController(
BotAccountService botService, BotAccountService botService,
DeveloperService developerService, DeveloperService developerService,
DevProjectService projectService, DevProjectService projectService,
ILogger<BotAccountController> logger ILogger<BotAccountController> logger,
AccountClientHelper accounts
) )
: ControllerBase : ControllerBase
{ {
public record BotRequest( public class CommonBotRequest
[Required] [MaxLength(1024)] string? Slug {
); [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(32)] public string? PictureId { get; set; }
[MaxLength(1024)] string? Slug, [MaxLength(32)] public string? BackgroundId { get; set; }
bool? IsActive }
) : BotRequest(Slug);
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] [HttpGet]
public async Task<IActionResult> ListBots( public async Task<IActionResult> ListBots(
@@ -47,7 +93,7 @@ public class BotAccountController(
return NotFound("Project not found or you don't have access"); return NotFound("Project not found or you don't have access");
var bots = await botService.GetBotsByProjectAsync(projectId); var bots = await botService.GetBotsByProjectAsync(projectId);
return Ok(bots); return Ok(await botService.LoadBotsAccountAsync(bots));
} }
[HttpGet("{botId:guid}")] [HttpGet("{botId:guid}")]
@@ -75,18 +121,16 @@ public class BotAccountController(
if (bot is null || bot.ProjectId != projectId) if (bot is null || bot.ProjectId != projectId)
return NotFound("Bot not found"); return NotFound("Bot not found");
return Ok(bot); return Ok(await botService.LoadBotAccountAsync(bot));
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> CreateBot( public async Task<IActionResult> CreateBot(
[FromRoute] string pubName, [FromRoute] string pubName,
[FromRoute] Guid projectId, [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) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized(); return Unauthorized();
@@ -102,9 +146,30 @@ public class BotAccountController(
if (project is null) if (project is null)
return NotFound("Project not found or you don't have access"); 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 try
{ {
var bot = await botService.CreateBotAsync(project, request.Slug); var bot = await botService.CreateBotAsync(project, createRequest.Slug, account);
return Ok(bot); return Ok(bot);
} }
catch (Exception ex) catch (Exception ex)
@@ -141,10 +206,29 @@ public class BotAccountController(
if (bot is null || bot.ProjectId != projectId) if (bot is null || bot.ProjectId != projectId)
return NotFound("Bot not found"); 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 try
{ {
var updatedBot = await botService.UpdateBotAsync( var updatedBot = await botService.UpdateBotAsync(
bot, bot,
botAccount,
request.Slug, request.Slug,
request.IsActive request.IsActive
); );

View File

@@ -1,13 +1,18 @@
using DysonNetwork.Develop.Project; using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Grpc.Core; using Grpc.Core;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf; using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Develop.Identity; 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) public async Task<BotAccount?> GetBotByIdAsync(Guid id)
{ {
@@ -23,39 +28,23 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco
.ToListAsync(); .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 // First, check if a bot with this slug already exists in this project
var existingBot = await db.BotAccounts var existingBot = await db.BotAccounts
.FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug); .FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug);
if (existingBot != null) if (existingBot != null)
{ {
throw new InvalidOperationException("A bot with this slug already exists in this project."); throw new InvalidOperationException("A bot with this slug already exists in this project.");
} }
var now = SystemClock.Instance.GetCurrentInstant();
try try
{ {
// First create the bot account in the Pass service
var createRequest = new CreateBotAccountRequest var createRequest = new CreateBotAccountRequest
{ {
AutomatedId = Guid.NewGuid().ToString(), AutomatedId = Guid.NewGuid().ToString(),
Account = new Account Account = 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 createResponse = await accountReceiver.CreateBotAccountAsync(createRequest);
@@ -80,7 +69,8 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco
} }
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists) 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) 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; var updated = false;
if (slug != null && bot.Slug != slug) if (slug != null && bot.Slug != slug)
@@ -100,7 +91,7 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco
bot.Slug = slug; bot.Slug = slug;
updated = true; updated = true;
} }
if (isActive.HasValue && bot.IsActive != isActive.Value) if (isActive.HasValue && bot.IsActive != isActive.Value)
{ {
bot.IsActive = isActive.Value; bot.IsActive = isActive.Value;
@@ -108,19 +99,14 @@ public class BotAccountService(AppDatabase db, BotAccountReceiverService.BotAcco
} }
if (!updated) return bot; if (!updated) return bot;
try try
{ {
// Update the bot account in the Pass service // Update the bot account in the Pass service
var updateRequest = new UpdateBotAccountRequest var updateRequest = new UpdateBotAccountRequest
{ {
AutomatedId = bot.Id.ToString(), AutomatedId = bot.Id.ToString(),
Account = new Shared.Proto.Account Account = account
{
Name = $"bot-{bot.Slug}",
Nick = $"Bot {bot.Slug}",
UpdatedAt = SystemClock.Instance.GetCurrentInstant().ToTimestamp()
}
}; };
var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest); 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 // Account not found in Pass service, continue with local deletion
} }
// Delete the local bot account // Delete the local bot account
db.BotAccounts.Remove(bot); db.BotAccounts.Remove(bot);
await db.SaveChangesAsync(); 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.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Develop.Project; using DysonNetwork.Develop.Project;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
@@ -11,7 +12,7 @@ public class Developer
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Guid PublisherId { get; set; } public Guid PublisherId { get; set; }
public List<DevProject> Projects { get; set; } = []; [JsonIgnore] public List<DevProject> Projects { get; set; } = [];
[NotMapped] public PublisherInfo? Publisher { get; set; } [NotMapped] public PublisherInfo? Publisher { get; set; }
} }

View File

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

View File

@@ -51,7 +51,8 @@ public class Account : ModelBase
Profile = Profile.ToProtoValue(), Profile = Profile.ToProtoValue(),
PerkSubscription = PerkSubscription?.ToProtoValue(), PerkSubscription = PerkSubscription?.ToProtoValue(),
CreatedAt = CreatedAt.ToTimestamp(), CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp() UpdatedAt = UpdatedAt.ToTimestamp(),
AutomatedId = AutomatedId?.ToString()
}; };
// Add contacts // Add contacts
@@ -81,6 +82,7 @@ public class Account : ModelBase
: null, : null,
CreatedAt = proto.CreatedAt.ToInstant(), CreatedAt = proto.CreatedAt.ToInstant(),
UpdatedAt = proto.UpdatedAt.ToInstant(), UpdatedAt = proto.UpdatedAt.ToInstant(),
AutomatedId = proto.AutomatedId is not null ? Guid.Parse(proto.AutomatedId) : null
}; };
account.Profile = AccountProfile.FromProtoValue(proto.Profile); account.Profile = AccountProfile.FromProtoValue(proto.Profile);
@@ -119,7 +121,7 @@ public abstract class Leveling
public class AccountProfile : ModelBase, IIdentifiedResource 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? FirstName { get; set; }
[MaxLength(256)] public string? MiddleName { get; set; } [MaxLength(256)] public string? MiddleName { get; set; }
[MaxLength(256)] public string? LastName { get; set; } [MaxLength(256)] public string? LastName { get; set; }

View File

@@ -30,6 +30,7 @@ public class AccountCurrentController(
{ {
[HttpGet] [HttpGet]
[ProducesResponseType<Account>(StatusCodes.Status200OK)] [ProducesResponseType<Account>(StatusCodes.Status200OK)]
[ProducesResponseType<ApiError>(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<Account>> GetCurrentIdentity() public async Task<ActionResult<Account>> GetCurrentIdentity()
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();

View File

@@ -6,6 +6,7 @@ using DysonNetwork.Pass.Email;
using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Localization;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Stream; using DysonNetwork.Shared.Stream;
using EFCore.BulkExtensions; using EFCore.BulkExtensions;
@@ -21,6 +22,8 @@ namespace DysonNetwork.Pass.Account;
public class AccountService( public class AccountService(
AppDatabase db, AppDatabase db,
MagicSpellService spells, MagicSpellService spells,
FileService.FileServiceClient files,
FileReferenceService.FileReferenceServiceClient fileRefs,
AccountUsernameService uname, AccountUsernameService uname,
EmailService mailer, EmailService mailer,
PusherService.PusherServiceClient pusher, PusherService.PusherServiceClient pusher,
@@ -182,7 +185,7 @@ public class AccountService(
); );
} }
public async Task<Account> CreateBotAccount(Account account, Guid automatedId) public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId, string? backgroundId)
{ {
var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync();
if (dupeAutomateCount > 0) if (dupeAutomateCount > 0)
@@ -195,8 +198,38 @@ public class AccountService(
account.AutomatedId = automatedId; account.AutomatedId = automatedId;
account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
account.IsSuperuser = false; 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); db.Accounts.Add(account);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return account; return account;
} }

View File

@@ -42,6 +42,26 @@ public class AccountServiceGrpc(
return account.ToProtoValue(); return account.ToProtoValue();
} }
public override async Task<Shared.Proto.Account> 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<GetAccountBatchResponse> GetAccountBatch(GetAccountBatchRequest request, public override async Task<GetAccountBatchResponse> GetAccountBatch(GetAccountBatchRequest request,
ServerCallContext context) ServerCallContext context)
{ {
@@ -56,7 +76,35 @@ public class AccountServiceGrpc(
.Where(a => accountIds.Contains(a.Id)) .Where(a => accountIds.Contains(a.Id))
.Include(a => a.Profile) .Include(a => a.Profile)
.ToListAsync(); .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<GetAccountBatchResponse> 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( var perks = await subscriptions.GetPerkSubscriptionsAsync(
accounts.Select(x => x.Id).ToList() accounts.Select(x => x.Id).ToList()
); );
@@ -76,7 +124,8 @@ public class AccountServiceGrpc(
return status.ToProtoValue(); return status.ToProtoValue();
} }
public override async Task<GetAccountStatusBatchResponse> GetAccountStatusBatch(GetAccountBatchRequest request, ServerCallContext context) public override async Task<GetAccountStatusBatchResponse> GetAccountStatusBatch(GetAccountBatchRequest request,
ServerCallContext context)
{ {
var accountIds = request.Id var accountIds = request.Id
.Select(id => Guid.TryParse(id, out var accountId) ? accountId : (Guid?)null) .Select(id => Guid.TryParse(id, out var accountId) ? accountId : (Guid?)null)
@@ -98,14 +147,14 @@ public class AccountServiceGrpc(
.Where(a => accountNames.Contains(a.Name)) .Where(a => accountNames.Contains(a.Name))
.Include(a => a.Profile) .Include(a => a.Profile)
.ToListAsync(); .ToListAsync();
var perks = await subscriptions.GetPerkSubscriptionsAsync( var perks = await subscriptions.GetPerkSubscriptionsAsync(
accounts.Select(x => x.Id).ToList() accounts.Select(x => x.Id).ToList()
); );
foreach (var account in accounts) foreach (var account in accounts)
if (perks.TryGetValue(account.Id, out var perk)) if (perks.TryGetValue(account.Id, out var perk))
account.PerkSubscription = perk?.ToReference(); account.PerkSubscription = perk?.ToReference();
var response = new GetAccountBatchResponse(); var response = new GetAccountBatchResponse();
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
return response; return response;

View File

@@ -1,3 +1,4 @@
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using Grpc.Core; using Grpc.Core;
using NodaTime; using NodaTime;
@@ -5,7 +6,12 @@ using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Pass.Account; 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 : BotAccountReceiverService.BotAccountReceiverServiceBase
{ {
public override async Task<CreateBotAccountResponse> CreateBotAccount( public override async Task<CreateBotAccountResponse> CreateBotAccount(
@@ -14,7 +20,12 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts)
) )
{ {
var account = Account.FromProtoValue(request.Account); 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 return new CreateBotAccountResponse
{ {
@@ -34,16 +45,44 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts)
ServerCallContext context ServerCallContext context
) )
{ {
var automatedId = Guid.Parse(request.AutomatedId); var account = Account.FromProtoValue(request.Account);
var account = await accounts.GetBotAccount(automatedId);
if (account is null) if (request.PictureId is not null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found")); {
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); db.Accounts.Update(account);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@@ -56,7 +95,7 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts)
CreatedAt = account.CreatedAt.ToTimestamp(), CreatedAt = account.CreatedAt.ToTimestamp(),
UpdatedAt = account.UpdatedAt.ToTimestamp(), UpdatedAt = account.UpdatedAt.ToTimestamp(),
IsActive = true IsActive = true
} }
}; };
} }
@@ -69,9 +108,9 @@ public class BotAccountReceiverGrpc(AppDatabase db, AccountService accounts)
var account = await accounts.GetBotAccount(automatedId); var account = await accounts.GetBotAccount(automatedId);
if (account is null) if (account is null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found")); throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found"));
await accounts.DeleteAccount(account); await accounts.DeleteAccount(account);
return new DeleteBotAccountResponse(); return new DeleteBotAccountResponse();
} }
} }

View File

@@ -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<AccountContactReference> Contacts { get; set; } = new();
public List<AccountBadgeReference> 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<ProfileLinkReference>? 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<string, object?> 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<int> 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
];
}

View File

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

View File

@@ -32,6 +32,8 @@ message Account {
google.protobuf.Timestamp created_at = 14; google.protobuf.Timestamp created_at = 14;
google.protobuf.Timestamp updated_at = 15; google.protobuf.Timestamp updated_at = 15;
optional string automated_id = 17;
} }
// Enum for status attitude // Enum for status attitude
@@ -246,7 +248,9 @@ message GetAccountStatusBatchResponse {
service AccountService { service AccountService {
// Account Operations // Account Operations
rpc GetAccount(GetAccountRequest) returns (Account) {} rpc GetAccount(GetAccountRequest) returns (Account) {}
rpc GetBotAccount(GetBotAccountRequest) returns (Account) {}
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {} rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {}
@@ -321,10 +325,18 @@ message GetAccountRequest {
string id = 1; // Account ID to retrieve string id = 1; // Account ID to retrieve
} }
message GetBotAccountRequest {
string automated_id = 1;
}
message GetAccountBatchRequest { message GetAccountBatchRequest {
repeated string id = 1; // Account ID to retrieve repeated string id = 1; // Account ID to retrieve
} }
message GetBotAccountBatchRequest {
repeated string automated_id = 1;
}
message LookupAccountBatchRequest { message LookupAccountBatchRequest {
repeated string names = 1; repeated string names = 1;
} }

View File

@@ -108,6 +108,8 @@ message BotAccount {
message CreateBotAccountRequest { message CreateBotAccountRequest {
Account account = 1; Account account = 1;
string automated_id = 2; string automated_id = 2;
optional string picture_id = 8;
optional string background_id = 9;
} }
message CreateBotAccountResponse { message CreateBotAccountResponse {
@@ -117,6 +119,8 @@ message CreateBotAccountResponse {
message UpdateBotAccountRequest { message UpdateBotAccountRequest {
string automated_id = 1; // ID of the bot account to update string automated_id = 1; // ID of the bot account to update
Account account = 2; // Updated account information Account account = 2; // Updated account information
optional string picture_id = 8;
optional string background_id = 9;
} }
message UpdateBotAccountResponse { message UpdateBotAccountResponse {

View File

@@ -11,6 +11,13 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
var response = await accounts.GetAccountAsync(request); var response = await accounts.GetAccountAsync(request);
return response; return response;
} }
public async Task<Account> GetBotAccount(Guid automatedId)
{
var request = new GetBotAccountRequest { AutomatedId = automatedId.ToString() };
var response = await accounts.GetBotAccountAsync(request);
return response;
}
public async Task<List<Account>> GetAccountBatch(List<Guid> ids) public async Task<List<Account>> GetAccountBatch(List<Guid> ids)
{ {
@@ -19,6 +26,14 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
var response = await accounts.GetAccountBatchAsync(request); var response = await accounts.GetAccountBatchAsync(request);
return response.Accounts.ToList(); return response.Accounts.ToList();
} }
public async Task<List<Account>> GetBotAccountBatch(List<Guid> 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<Dictionary<Guid, AccountStatusReference>> GetAccountStatusBatch(List<Guid> ids) public async Task<Dictionary<Guid, AccountStatusReference>> GetAccountStatusBatch(List<Guid> ids)
{ {