using Microsoft.Extensions.Localization; using MongoDB.Entities; using PrivaPub.ClientModels; using PrivaPub.ClientModels.User; using PrivaPub.ClientModels.User.Avatar; using PrivaPub.Models.User; using PrivaPub.Resources; using PrivaPub.Services.Federation; using PrivaPub.StaticServices; namespace PrivaPub.Services.ClientToServer.Private { public interface IPrivateAvatarUsersService { Task UpdateAvatar(UpdateAvatarForm form); Task InsertAvatar(InsertAvatarForm form); Task GetRootAvatars(string rootUserId, CancellationToken token); } public class PrivateAvatarUsersService : IPrivateAvatarUsersService { readonly ILocalActorService _localActors; readonly DbEntities _dbEntities; readonly IStringLocalizer _localizer; readonly ILogger _logger; public PrivateAvatarUsersService(ILocalActorService localActors, DbEntities dbEntities, IStringLocalizer localizer, ILogger logger) { _localActors = localActors; _dbEntities = dbEntities; _localizer = localizer; _logger = logger; } public async Task InsertAvatar(InsertAvatarForm form) { var result = new WebResult(); try { var userName = form.UserName?.Trim().ToLowerInvariant(); if (string.IsNullOrEmpty(userName) || !userName.All(c => char.IsAsciiLetterOrDigit(c) || c == '_')) return result.Invalidate(_localizer["The username may contain only letters, digits and underscores."]); if (await _localActors.IsUserNameTaken(userName, default)) return result.Invalidate(_localizer["The username '{0}' is already take.", userName]); if (!await IsValidRootUser(form.RootId)) return result.Invalidate(_localizer["You can't do this action because of your account status."]); var (privateKey, publicKey) = Keys.NewKeyPair(); var newAvatar = new Avatar { Name = form.Name, UserName = userName, Biography = form.Biography, Fields = form.Fields, PersonalNote = form.PersonalNote, PrivateKey = privateKey, PublicKey = publicKey, Domain = _localActors.BaseAddress }; if (form.Settings is { IsDefault: false }) newAvatar.Settings = ToSettings(form.Settings); var actor = _localActors.FromAvatar(newAvatar); newAvatar.Url = actor.Uri; newAvatar.InboxURL = actor.Inbox; newAvatar.OutboxURL = actor.Outbox; await DB.Default.SaveAsync(newAvatar); await DB.Default.SaveAsync(new RootToAvatar { RootId = form.RootId, AvatarId = newAvatar.ID }); result.Data = ToView(newAvatar); return result; } catch (Exception ex) { _logger.LogError(ex, $"{nameof(PrivateAvatarUsersService)}.{nameof(InsertAvatar)}"); return result.Invalidate(_localizer["Error: {0}", ex.Message], exception: ex); } } public async Task UpdateAvatar(UpdateAvatarForm form) { var result = new WebResult(); try { if (!await IsValidRootUser(form.RootId)) return result.Invalidate(_localizer["You can't do this action because of your account status."]); var owns = await _dbEntities.RootToAvatars .Match(ra => ra.RootId == form.RootId && ra.AvatarId == form.AvatarId) .ExecuteAnyAsync(); if (!owns) return result.Invalidate(_localizer["Avatar not found."], StatusCodes.Status404NotFound); var avatar = await _dbEntities.Avatars.MatchID(form.AvatarId).ExecuteFirstAsync(); if (avatar == default || avatar.DeletionAt.HasValue) return result.Invalidate(_localizer["Avatar not found."], StatusCodes.Status404NotFound); avatar.Name = form.Name; avatar.Biography = form.Biography; avatar.Fields = form.Fields ?? new(); avatar.PersonalNote = form.PersonalNote; if (form.Settings != default) avatar.Settings = ToSettings(form.Settings); avatar.UpdatedAt = DateTime.UtcNow; await DB.Default.SaveAsync(avatar); result.Data = ToView(avatar); return result; } catch (Exception ex) { _logger.LogError(ex, $"{nameof(PrivateAvatarUsersService)}.{nameof(UpdateAvatar)}"); return result.Invalidate(_localizer["Error: {0}", ex.Message], exception: ex); } } public async Task GetRootAvatars(string rootUserId, CancellationToken token) { var result = new WebResult(); try { var avatarIds = (await _dbEntities.RootToAvatars.Match(ra => ra.RootId == rootUserId).ExecuteAsync(token)) .Select(ra => ra.AvatarId) .ToList(); var avatars = avatarIds.Count == 0 ? new List() : await _dbEntities.Avatars.Match(a => avatarIds.Contains(a.ID) && !a.DeletionAt.HasValue).ExecuteAsync(token); result.Data = avatars.Select(ToView).ToList(); return result; } catch (Exception ex) { _logger.LogError(ex, $"{nameof(PrivateAvatarUsersService)}.{nameof(GetRootAvatars)}"); return result.Invalidate(_localizer["Error: {0}", ex.Message], exception: ex); } } async ValueTask IsValidRootUser(string rootUserId) => !string.IsNullOrEmpty(rootUserId) && await _dbEntities.RootUsers.MatchID(rootUserId) .Match(ru => !ru.DeletedAt.HasValue && !ru.IsBanned) .ExecuteAnyAsync(); static AvatarSettings ToSettings(ViewAvatarSettings settings) => new() { IsDefault = settings.IsDefault, DarkThemeIndexColour = settings.DarkThemeIndexColour, IconsThemeIndexColour = settings.IconsThemeIndexColour, LanguageCode = settings.LanguageCode, LightThemeIndexColour = settings.LightThemeIndexColour, PreferSystemTheming = settings.PreferSystemTheming, ThemeIsDarkGray = settings.ThemeIsDarkGray, ThemeIsDarkMode = settings.ThemeIsDarkMode, ThemeIsLightGray = settings.ThemeIsLightGray }; ViewAvatar ToView(Avatar avatar) => new() { Id = avatar.ID, Url = avatar.Url, Handle = _localActors.FromAvatar(avatar).Handle, Name = avatar.Name, UserName = avatar.UserName, Biography = avatar.Biography, Fields = avatar.Fields, PictureURL = avatar.PictureURL, ThumbnailURL = avatar.ThumbnailURL, SharedPersonalContacts = avatar.SharedPersonalContacts, AccountState = (ViewAvatarState)(int)avatar.AccountState, ServerType = ViewAvatarServer.PrivaPub, Settings = new() { IsDefault = avatar.Settings.IsDefault, LanguageCode = avatar.Settings.LanguageCode, IconsThemeIndexColour = avatar.Settings.IconsThemeIndexColour, LightThemeIndexColour = avatar.Settings.LightThemeIndexColour, DarkThemeIndexColour = avatar.Settings.DarkThemeIndexColour, PreferSystemTheming = avatar.Settings.PreferSystemTheming, ThemeIsDarkMode = avatar.Settings.ThemeIsDarkMode, ThemeIsDarkGray = avatar.Settings.ThemeIsDarkGray, ThemeIsLightGray = avatar.Settings.ThemeIsLightGray }, UpdatedAt = avatar.UpdatedAt, CreatedAt = avatar.CreatedAt }; } }