440 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			440 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.ComponentModel.DataAnnotations;
 | |
| using System.ComponentModel.DataAnnotations.Schema;
 | |
| using System.Text.Json.Serialization;
 | |
| using Microsoft.EntityFrameworkCore;
 | |
| using NodaTime;
 | |
| using NodaTime.Serialization.Protobuf;
 | |
| using OtpNet;
 | |
| 
 | |
| namespace DysonNetwork.Shared.Models;
 | |
| 
 | |
| [Index(nameof(Name), IsUnique = true)]
 | |
| public class SnAccount : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; } = Guid.NewGuid();
 | |
|     [MaxLength(256)] public string Name { get; set; } = string.Empty;
 | |
|     [MaxLength(256)] public string Nick { get; set; } = string.Empty;
 | |
|     [MaxLength(32)] public string Language { get; set; } = string.Empty;
 | |
|     [MaxLength(32)] public string Region { 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 SnAccountProfile Profile { get; set; } = null!;
 | |
|     public ICollection<SnAccountContact> Contacts { get; set; } = [];
 | |
|     public ICollection<SnAccountBadge> Badges { get; set; } = [];
 | |
| 
 | |
|     [JsonIgnore] public ICollection<SnAccountAuthFactor> AuthFactors { get; set; } = [];
 | |
|     [JsonIgnore] public ICollection<SnAccountConnection> Connections { get; set; } = [];
 | |
|     [JsonIgnore] public ICollection<SnAuthSession> Sessions { get; set; } = [];
 | |
|     [JsonIgnore] public ICollection<SnAuthChallenge> Challenges { get; set; } = [];
 | |
| 
 | |
|     [JsonIgnore] public ICollection<SnAccountRelationship> OutgoingRelationships { get; set; } = [];
 | |
|     [JsonIgnore] public ICollection<SnAccountRelationship> IncomingRelationships { get; set; } = [];
 | |
| 
 | |
|     [NotMapped] public SnSubscriptionReferenceObject? PerkSubscription { get; set; }
 | |
| 
 | |
|     public Proto.Account ToProtoValue()
 | |
|     {
 | |
|         var proto = new Proto.Account
 | |
|         {
 | |
|             Id = Id.ToString(),
 | |
|             Name = Name,
 | |
|             Nick = Nick,
 | |
|             Language = Language,
 | |
|             Region = Region,
 | |
|             ActivatedAt = ActivatedAt?.ToTimestamp(),
 | |
|             IsSuperuser = IsSuperuser,
 | |
|             Profile = Profile.ToProtoValue(),
 | |
|             PerkSubscription = PerkSubscription?.ToProtoValue(),
 | |
|             CreatedAt = CreatedAt.ToTimestamp(),
 | |
|             UpdatedAt = UpdatedAt.ToTimestamp(),
 | |
|             AutomatedId = AutomatedId?.ToString()
 | |
|         };
 | |
| 
 | |
|         // Add contacts
 | |
|         foreach (var contact in Contacts)
 | |
|             proto.Contacts.Add(contact.ToProtoValue());
 | |
| 
 | |
|         // Add badges
 | |
|         foreach (var badge in Badges)
 | |
|             proto.Badges.Add(badge.ToProtoValue());
 | |
| 
 | |
|         return proto;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     public static SnAccount FromProtoValue(Proto.Account proto)
 | |
|     {
 | |
|         var account = new SnAccount
 | |
|         {
 | |
|             Id = Guid.Parse(proto.Id),
 | |
|             Name = proto.Name,
 | |
|             Nick = proto.Nick,
 | |
|             Language = proto.Language,
 | |
|             Region = proto.Region,
 | |
|             ActivatedAt = proto.ActivatedAt?.ToInstant(),
 | |
|             IsSuperuser = proto.IsSuperuser,
 | |
|             PerkSubscription = proto.PerkSubscription is not null
 | |
|                 ? SnSubscriptionReferenceObject.FromProtoValue(proto.PerkSubscription)
 | |
|                 : null,
 | |
|             CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|             UpdatedAt = proto.UpdatedAt.ToInstant(),
 | |
|             AutomatedId = proto.AutomatedId is not null ? Guid.Parse(proto.AutomatedId) : null,
 | |
|             Profile = SnAccountProfile.FromProtoValue(proto.Profile)
 | |
|         };
 | |
| 
 | |
|         foreach (var contactProto in proto.Contacts)
 | |
|             account.Contacts.Add(SnAccountContact.FromProtoValue(contactProto));
 | |
| 
 | |
|         foreach (var badgeProto in proto.Badges)
 | |
|             account.Badges.Add(SnAccountBadge.FromProtoValue(badgeProto));
 | |
| 
 | |
|         return account;
 | |
|     }
 | |
| }
 | |
| 
 | |
| public abstract class Leveling
 | |
| {
 | |
|     private const int MaxLevel = 120;
 | |
|     private const double BaseExp = 100.0;
 | |
|     private const double K = 3.52; // tweak this for balance
 | |
| 
 | |
|     // Single level XP requirement (from L → L+1)
 | |
|     public static double ExpForLevel(int level)
 | |
|     {
 | |
|         if (level < 1) return 0;
 | |
|         return BaseExp + K * Math.Pow(level - 1, 2);
 | |
|     }
 | |
| 
 | |
|     // Total cumulative XP required to reach level L
 | |
|     public static double TotalExpForLevel(int level)
 | |
|     {
 | |
|         if (level < 1) return 0;
 | |
|         return BaseExp * level + K * ((level - 1) * level * (2 * level - 1)) / 6.0;
 | |
|     }
 | |
| 
 | |
|     // Get level from experience
 | |
|     public static int GetLevelFromExp(int xp)
 | |
|     {
 | |
|         if (xp < 0) return 0;
 | |
| 
 | |
|         int level = 0;
 | |
|         while (level < MaxLevel && TotalExpForLevel(level + 1) <= xp)
 | |
|         {
 | |
|             level++;
 | |
|         }
 | |
|         return level;
 | |
|     }
 | |
| 
 | |
|     // Progress to next level (0.0 ~ 1.0)
 | |
|     public static double GetProgressToNextLevel(int xp)
 | |
|     {
 | |
|         int currentLevel = GetLevelFromExp(xp);
 | |
|         if (currentLevel >= MaxLevel) return 1.0;
 | |
| 
 | |
|         double prevTotal = TotalExpForLevel(currentLevel);
 | |
|         double nextTotal = TotalExpForLevel(currentLevel + 1);
 | |
| 
 | |
|         return (xp - prevTotal) / (nextTotal - prevTotal);
 | |
|     }
 | |
| }
 | |
| 
 | |
| public class UsernameColor
 | |
| {
 | |
|     public string Type { get; set; } = "plain"; // "plain" | "gradient"
 | |
|     public string? Value { get; set; }          // e.g. "red" or "#ff6600"
 | |
|     public string? Direction { get; set; }      // e.g. "to right"
 | |
|     public List<string>? Colors { get; set; }   // e.g. ["#ff0000", "#00ff00"]
 | |
| 
 | |
|     public Proto.UsernameColor ToProtoValue()
 | |
|     {
 | |
|         var proto = new Proto.UsernameColor
 | |
|         {
 | |
|             Type = Type,
 | |
|             Value = Value,
 | |
|             Direction = Direction,
 | |
|         };
 | |
|         if (Colors is not null)
 | |
|         {
 | |
|             proto.Colors.AddRange(Colors);
 | |
|         }
 | |
|         return proto;
 | |
|     }
 | |
| 
 | |
|     public static UsernameColor FromProtoValue(Proto.UsernameColor proto)
 | |
|     {
 | |
|         return new UsernameColor
 | |
|         {
 | |
|             Type = proto.Type,
 | |
|             Value = proto.Value,
 | |
|             Direction = proto.Direction,
 | |
|             Colors = proto.Colors?.ToList()
 | |
|         };
 | |
|     }
 | |
| }
 | |
| 
 | |
| public class SnAccountProfile : ModelBase, IIdentifiedResource
 | |
| {
 | |
|     public Guid Id { get; set; } = Guid.NewGuid();
 | |
|     [MaxLength(256)] public string? FirstName { get; set; }
 | |
|     [MaxLength(256)] public string? MiddleName { get; set; }
 | |
|     [MaxLength(256)] public string? LastName { get; set; }
 | |
|     [MaxLength(4096)] public string? Bio { 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; }
 | |
|     [Column(TypeName = "jsonb")] public List<ProfileLink>? Links { get; set; }
 | |
|     [Column(TypeName = "jsonb")] public UsernameColor? UsernameColor { get; set; }
 | |
|     public Instant? Birthday { get; set; }
 | |
|     public Instant? LastSeenAt { get; set; }
 | |
| 
 | |
|     [Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
 | |
|     [Column(TypeName = "jsonb")] public SnAccountBadgeRef? ActiveBadge { get; set; }
 | |
| 
 | |
|     public int Experience { get; set; }
 | |
| 
 | |
|     [NotMapped]
 | |
|     public int Level => Leveling.GetLevelFromExp(Experience);
 | |
|     [NotMapped]
 | |
|     public double LevelingProgress => Leveling.GetProgressToNextLevel(Experience);
 | |
| 
 | |
|     public double SocialCredits { get; set; } = 100;
 | |
| 
 | |
|     [NotMapped]
 | |
|     public int SocialCreditsLevel => SocialCredits switch
 | |
|     {
 | |
|         < 100 => -1,
 | |
|         > 100 and < 200 => 0,
 | |
|         < 200 => 1,
 | |
|         _ => 2
 | |
|     };
 | |
| 
 | |
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; }
 | |
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; }
 | |
| 
 | |
|     public Guid AccountId { get; set; }
 | |
|     [JsonIgnore] public SnAccount Account { get; set; } = null!;
 | |
| 
 | |
|     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(),
 | |
|             ActiveBadge = ActiveBadge?.ToProtoValue(),
 | |
|             UsernameColor = UsernameColor?.ToProtoValue(),
 | |
|             CreatedAt = CreatedAt.ToTimestamp(),
 | |
|             UpdatedAt = UpdatedAt.ToTimestamp()
 | |
|         };
 | |
| 
 | |
|         return proto;
 | |
|     }
 | |
| 
 | |
|     public static SnAccountProfile FromProtoValue(Proto.AccountProfile proto)
 | |
|     {
 | |
|         var profile = new SnAccountProfile
 | |
|         {
 | |
|             Id = Guid.Parse(proto.Id),
 | |
|             FirstName = proto.FirstName,
 | |
|             LastName = proto.LastName,
 | |
|             MiddleName = proto.MiddleName,
 | |
|             Bio = proto.Bio,
 | |
|             Gender = proto.Gender,
 | |
|             Pronouns = proto.Pronouns,
 | |
|             TimeZone = proto.TimeZone,
 | |
|             Location = proto.Location,
 | |
|             Birthday = proto.Birthday?.ToInstant(),
 | |
|             LastSeenAt = proto.LastSeenAt?.ToInstant(),
 | |
|             Verification = proto.Verification is null ? null : SnVerificationMark.FromProtoValue(proto.Verification),
 | |
|             ActiveBadge = proto.ActiveBadge is null ? null : SnAccountBadgeRef.FromProtoValue(proto.ActiveBadge),
 | |
|             Experience = proto.Experience,
 | |
|             SocialCredits = proto.SocialCredits,
 | |
|             Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture),
 | |
|             Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background),
 | |
|             AccountId = Guid.Parse(proto.AccountId),
 | |
|             UsernameColor = proto.UsernameColor is not null ? UsernameColor.FromProtoValue(proto.UsernameColor) : null,
 | |
|             CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|             UpdatedAt = proto.UpdatedAt.ToInstant()
 | |
|         };
 | |
| 
 | |
|         return profile;
 | |
|     }
 | |
| 
 | |
|     public string ResourceIdentifier => $"account:profile:{Id}";
 | |
| }
 | |
| 
 | |
| public class ProfileLink
 | |
| {
 | |
|     public string Name { get; set; } = string.Empty;
 | |
|     public string Url { get; set; } = string.Empty;
 | |
| }
 | |
| 
 | |
| public class SnAccountContact : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; }
 | |
|     public AccountContactType Type { get; set; }
 | |
|     public Instant? VerifiedAt { get; set; }
 | |
|     public bool IsPrimary { get; set; } = false;
 | |
|     public bool IsPublic { get; set; } = false;
 | |
|     [MaxLength(1024)] public string Content { get; set; } = string.Empty;
 | |
| 
 | |
|     public Guid AccountId { get; set; }
 | |
|     [JsonIgnore] public SnAccount Account { get; set; } = null!;
 | |
| 
 | |
|     public Proto.AccountContact ToProtoValue()
 | |
|     {
 | |
|         var proto = new Proto.AccountContact
 | |
|         {
 | |
|             Id = Id.ToString(),
 | |
|             Type = Type switch
 | |
|             {
 | |
|                 AccountContactType.Email => Shared.Proto.AccountContactType.Email,
 | |
|                 AccountContactType.PhoneNumber => Shared.Proto.AccountContactType.PhoneNumber,
 | |
|                 AccountContactType.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 SnAccountContact FromProtoValue(Proto.AccountContact proto)
 | |
|     {
 | |
|         var contact = new SnAccountContact
 | |
|         {
 | |
|             Id = Guid.Parse(proto.Id),
 | |
|             AccountId = Guid.Parse(proto.AccountId),
 | |
|             Type = proto.Type switch
 | |
|             {
 | |
|                 Shared.Proto.AccountContactType.Email => AccountContactType.Email,
 | |
|                 Shared.Proto.AccountContactType.PhoneNumber => AccountContactType.PhoneNumber,
 | |
|                 Shared.Proto.AccountContactType.Address => AccountContactType.Address,
 | |
|                 _ => AccountContactType.Email
 | |
|             },
 | |
|             Content = proto.Content,
 | |
|             IsPrimary = proto.IsPrimary,
 | |
|             VerifiedAt = proto.VerifiedAt?.ToInstant(),
 | |
|             CreatedAt = proto.CreatedAt.ToInstant(),
 | |
|             UpdatedAt = proto.UpdatedAt.ToInstant()
 | |
|         };
 | |
| 
 | |
|         return contact;
 | |
|     }
 | |
| }
 | |
| 
 | |
| public enum AccountContactType
 | |
| {
 | |
|     Email,
 | |
|     PhoneNumber,
 | |
|     Address
 | |
| }
 | |
| 
 | |
| public class SnAccountAuthFactor : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; }
 | |
|     public AccountAuthFactorType Type { get; set; }
 | |
|     [JsonIgnore][MaxLength(8196)] public string? Secret { get; set; }
 | |
| 
 | |
|     [JsonIgnore]
 | |
|     [Column(TypeName = "jsonb")]
 | |
|     public Dictionary<string, object>? Config { get; set; } = [];
 | |
| 
 | |
|     /// <summary>
 | |
|     /// The trustworthy stands for how safe is this auth factor.
 | |
|     /// Basically, it affects how many steps it can complete in authentication.
 | |
|     /// Besides, users may need to use some high-trustworthy level auth factors when confirming some dangerous operations.
 | |
|     /// </summary>
 | |
|     public int Trustworthy { get; set; } = 1;
 | |
| 
 | |
|     public Instant? EnabledAt { get; set; }
 | |
|     public Instant? ExpiredAt { get; set; }
 | |
| 
 | |
|     public Guid AccountId { get; set; }
 | |
|     [JsonIgnore] public SnAccount Account { get; set; } = null!;
 | |
| 
 | |
|     public SnAccountAuthFactor HashSecret(int cost = 12)
 | |
|     {
 | |
|         if (Secret == null) return this;
 | |
|         Secret = BCrypt.Net.BCrypt.HashPassword(Secret, workFactor: cost);
 | |
|         return this;
 | |
|     }
 | |
| 
 | |
|     public bool VerifyPassword(string password)
 | |
|     {
 | |
|         if (Secret == null)
 | |
|             throw new InvalidOperationException("Auth factor with no secret cannot be verified with password.");
 | |
|         switch (Type)
 | |
|         {
 | |
|             case AccountAuthFactorType.Password:
 | |
|             case AccountAuthFactorType.PinCode:
 | |
|                 return BCrypt.Net.BCrypt.Verify(password, Secret);
 | |
|             case AccountAuthFactorType.TimedCode:
 | |
|                 var otp = new Totp(Base32Encoding.ToBytes(Secret));
 | |
|                 return otp.VerifyTotp(DateTime.UtcNow, password, out _, new VerificationWindow(previous: 5, future: 5));
 | |
|             case AccountAuthFactorType.EmailCode:
 | |
|             case AccountAuthFactorType.InAppCode:
 | |
|             default:
 | |
|                 throw new InvalidOperationException("Unsupported verification type, use CheckDeliveredCode instead.");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// <summary>
 | |
|     /// This dictionary will be returned to the client and should only be set when it just created.
 | |
|     /// Useful for passing the client some data to finishing setup and recovery code.
 | |
|     /// </summary>
 | |
|     [NotMapped]
 | |
|     public Dictionary<string, object>? CreatedResponse { get; set; }
 | |
| }
 | |
| 
 | |
| public enum AccountAuthFactorType
 | |
| {
 | |
|     Password,
 | |
|     EmailCode,
 | |
|     InAppCode,
 | |
|     TimedCode,
 | |
|     PinCode,
 | |
| }
 | |
| 
 | |
| public class SnAccountConnection : ModelBase
 | |
| {
 | |
|     public Guid Id { get; set; } = Guid.NewGuid();
 | |
|     [MaxLength(4096)] public string Provider { get; set; } = null!;
 | |
|     [MaxLength(8192)] public string ProvidedIdentifier { get; set; } = null!;
 | |
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } = [];
 | |
| 
 | |
|     [JsonIgnore][MaxLength(4096)] public string? AccessToken { get; set; }
 | |
|     [JsonIgnore][MaxLength(4096)] public string? RefreshToken { get; set; }
 | |
|     public Instant? LastUsedAt { get; set; }
 | |
| 
 | |
|     public Guid AccountId { get; set; }
 | |
|     public SnAccount Account { get; set; } = null!;
 | |
| }
 |