♻️ Centralized data models (wip)

This commit is contained in:
2025-09-27 14:09:28 +08:00
parent 51b6f7309e
commit e70d8371f8
206 changed files with 1352 additions and 2128 deletions

View File

@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum AbuseReportType
{
Copyright,
Harassment,
Impersonation,
OffensiveContent,
Spam,
PrivacyViolation,
IllegalContent,
Other
}
public class SnAbuseReport : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(4096)] public string ResourceIdentifier { get; set; } = null!;
public AbuseReportType Type { get; set; }
[MaxLength(8192)] public string Reason { get; set; } = null!;
public Instant? ResolvedAt { get; set; }
[MaxLength(8192)] public string? Resolution { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
}

View File

@@ -0,0 +1,402 @@
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<AccountContact> Contacts { get; set; } = [];
public ICollection<SnAccountBadge> Badges { get; set; } = [];
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = [];
[JsonIgnore] public ICollection<AccountConnection> 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 SubscriptionReferenceObject? 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
? SubscriptionReferenceObject.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(AccountContact.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 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; }
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(),
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),
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 AccountContact : 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 AccountContact FromProtoValue(Proto.AccountContact proto)
{
var contact = new AccountContact
{
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 AccountAuthFactor : 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 AccountAuthFactor 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 AccountConnection : 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!;
}

View File

@@ -0,0 +1,127 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Shared.Proto;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public enum StatusAttitude
{
Positive,
Negative,
Neutral
}
public class SnAccountStatus : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public StatusAttitude Attitude { get; set; }
[NotMapped] public bool IsOnline { get; set; }
[NotMapped] public bool IsCustomized { get; set; } = true;
public bool IsInvisible { get; set; }
public bool IsNotDisturb { get; set; }
[MaxLength(1024)] public string? Label { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
public Instant? ClearedAt { get; set; }
[MaxLength(4096)] public string? AppIdentifier { get; set; }
/// <summary>
/// Indicates this status is created based on running process or rich presence
/// </summary>
public bool IsAutomated { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public AccountStatus ToProtoValue()
{
var proto = new AccountStatus
{
Id = Id.ToString(),
Attitude = Attitude switch
{
StatusAttitude.Positive => Shared.Proto.StatusAttitude.Positive,
StatusAttitude.Negative => Shared.Proto.StatusAttitude.Negative,
StatusAttitude.Neutral => Shared.Proto.StatusAttitude.Neutral,
_ => Shared.Proto.StatusAttitude.Unspecified
},
IsOnline = IsOnline,
IsCustomized = IsCustomized,
IsInvisible = IsInvisible,
IsNotDisturb = IsNotDisturb,
Label = Label ?? string.Empty,
Meta = GrpcTypeHelper.ConvertObjectToByteString(Meta),
ClearedAt = ClearedAt?.ToTimestamp(),
AccountId = AccountId.ToString()
};
return proto;
}
public static SnAccountStatus FromProtoValue(AccountStatus proto)
{
var status = new SnAccountStatus
{
Id = Guid.Parse(proto.Id),
Attitude = proto.Attitude switch
{
Shared.Proto.StatusAttitude.Positive => StatusAttitude.Positive,
Shared.Proto.StatusAttitude.Negative => StatusAttitude.Negative,
Shared.Proto.StatusAttitude.Neutral => StatusAttitude.Neutral,
_ => StatusAttitude.Neutral
},
IsOnline = proto.IsOnline,
IsCustomized = proto.IsCustomized,
IsInvisible = proto.IsInvisible,
IsNotDisturb = proto.IsNotDisturb,
Label = proto.Label,
Meta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(proto.Meta),
ClearedAt = proto.ClearedAt?.ToInstant(),
AccountId = Guid.Parse(proto.AccountId)
};
return status;
}
}
public enum CheckInResultLevel
{
Worst,
Worse,
Normal,
Better,
Best,
Special
}
public class SnCheckInResult : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public CheckInResultLevel Level { get; set; }
public decimal? RewardPoints { get; set; }
public int? RewardExperience { get; set; }
[Column(TypeName = "jsonb")] public ICollection<CheckInFortuneTip> Tips { get; set; } = new List<CheckInFortuneTip>();
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Instant? BackdatedFrom { get; set; }
}
public class CheckInFortuneTip
{
public bool IsPositive { get; set; }
public string Title { get; set; } = null!;
public string Content { get; set; } = null!;
}
/// <summary>
/// This method should not be mapped. Used to generate the daily event calendar.
/// </summary>
public class DailyEventResponse
{
public Instant Date { get; set; }
public SnCheckInResult? CheckInResult { get; set; }
public ICollection<SnAccountStatus> Statuses { get; set; } = new List<SnAccountStatus>();
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Shared.Proto;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SnActionLog : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(4096)] public string Action { get; set; } = null!;
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
[MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(128)] public string? IpAddress { get; set; }
[Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Guid? SessionId { get; set; }
public ActionLog ToProtoValue()
{
var protoLog = new ActionLog
{
Id = Id.ToString(),
Action = Action,
UserAgent = UserAgent ?? string.Empty,
IpAddress = IpAddress ?? string.Empty,
Location = Location?.ToString() ?? string.Empty,
AccountId = AccountId.ToString(),
CreatedAt = CreatedAt.ToTimestamp()
};
// Convert Meta dictionary to Struct
protoLog.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta));
if (SessionId.HasValue)
protoLog.SessionId = SessionId.Value.ToString();
return protoLog;
}
}

View File

@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public interface IActivity
{
public SnActivity ToActivity();
}
[NotMapped]
public class SnActivity : ModelBase
{
public Guid Id { get; set; }
[MaxLength(1024)] public string Type { get; set; } = null!;
[MaxLength(4096)] public string ResourceIdentifier { get; set; } = null!;
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
public object? Data { get; set; }
// Outdated fields, for backward compability
public int Visibility => 0;
public static SnActivity Empty()
{
var now = SystemClock.Instance.GetCurrentInstant();
return new SnActivity
{
CreatedAt = now,
UpdatedAt = now,
Id = Guid.NewGuid(),
Type = "empty",
ResourceIdentifier = "none"
};
}
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SnApiKey : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Label { get; set; } = null!;
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Guid SessionId { get; set; }
public SnAuthSession Session { get; set; } = null!;
[NotMapped]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Key { get; set; }
public Proto.ApiKey ToProtoValue()
{
return new Proto.ApiKey
{
Id = Id.ToString(),
Label = Label,
AccountId = AccountId.ToString(),
SessionId = SessionId.ToString(),
Key = Key,
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
}
public static SnApiKey FromProtoValue(Proto.ApiKey proto)
{
return new SnApiKey
{
Id = Guid.Parse(proto.Id),
AccountId = Guid.Parse(proto.AccountId),
SessionId = Guid.Parse(proto.SessionId),
Label = proto.Label,
Key = proto.Key,
CreatedAt = proto.CreatedAt.ToInstant(),
UpdatedAt = proto.UpdatedAt.ToInstant()
};
}
}

View File

@@ -0,0 +1,131 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.GeoIp;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SnAuthSession : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Instant? LastGrantedAt { get; set; }
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
// When the challenge is null, indicates the session is for an API Key
public Guid? ChallengeId { get; set; }
public SnAuthChallenge? Challenge { get; set; } = null!;
// Indicates the session is for an OIDC connection
public Guid? AppId { get; set; }
public Proto.AuthSession ToProtoValue() => new()
{
Id = Id.ToString(),
LastGrantedAt = LastGrantedAt?.ToTimestamp(),
ExpiredAt = ExpiredAt?.ToTimestamp(),
AccountId = AccountId.ToString(),
Account = Account.ToProtoValue(),
ChallengeId = ChallengeId.ToString(),
Challenge = Challenge?.ToProtoValue(),
AppId = AppId?.ToString()
};
}
public enum ChallengeType
{
Login,
OAuth, // Trying to authorize other platforms
Oidc // Trying to connect other platforms
}
public enum ClientPlatform
{
Unidentified,
Web,
Ios,
Android,
MacOs,
Windows,
Linux
}
public class SnAuthChallenge : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Instant? ExpiredAt { get; set; }
public int StepRemain { get; set; }
public int StepTotal { get; set; }
public int FailedAttempts { get; set; }
public ChallengeType Type { get; set; } = ChallengeType.Login;
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
[MaxLength(128)] public string? IpAddress { get; set; }
[MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(1024)] public string? Nonce { get; set; }
[Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
public Guid? ClientId { get; set; }
public SnAuthClient? Client { get; set; } = null!;
public SnAuthChallenge Normalize()
{
if (StepRemain == 0 && BlacklistFactors.Count == 0) StepRemain = StepTotal;
return this;
}
public Proto.AuthChallenge ToProtoValue() => new()
{
Id = Id.ToString(),
ExpiredAt = ExpiredAt?.ToTimestamp(),
StepRemain = StepRemain,
StepTotal = StepTotal,
FailedAttempts = FailedAttempts,
Type = (Proto.ChallengeType)Type,
BlacklistFactors = { BlacklistFactors.Select(x => x.ToString()) },
Audiences = { Audiences },
Scopes = { Scopes },
IpAddress = IpAddress,
UserAgent = UserAgent,
DeviceId = Client!.DeviceId,
Nonce = Nonce,
AccountId = AccountId.ToString()
};
}
public class SnAuthClient : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public ClientPlatform Platform { get; set; } = ClientPlatform.Unidentified;
[MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
[MaxLength(1024)] public string? DeviceLabel { get; set; }
[MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
}
public class SnAuthClientWithChallenge : SnAuthClient
{
public List<SnAuthChallenge> Challenges { get; set; } = [];
public static SnAuthClientWithChallenge FromClient(SnAuthClient client)
{
return new SnAuthClientWithChallenge
{
Id = client.Id,
Platform = client.Platform,
DeviceName = client.DeviceName,
DeviceLabel = client.DeviceLabel,
DeviceId = client.DeviceId,
AccountId = client.AccountId,
};
}
}

View File

@@ -0,0 +1,122 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using Google.Protobuf.WellKnownTypes;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SnAccountBadge : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Type { get; set; } = null!;
[MaxLength(1024)] public string? Label { get; set; }
[MaxLength(4096)] public string? Caption { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object?> Meta { get; set; } = new();
public Instant? ActivatedAt { get; set; }
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public SnAccount Account { get; set; } = null!;
public SnAccountBadgeRef ToReference()
{
return new SnAccountBadgeRef
{
Id = Id,
Type = Type,
Label = Label,
Caption = Caption,
Meta = Meta,
ActivatedAt = ActivatedAt,
ExpiredAt = ExpiredAt,
AccountId = AccountId,
};
}
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 SnAccountBadge FromProtoValue(AccountBadge proto)
{
var badge = new SnAccountBadge
{
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 SnAccountBadgeRef : ModelBase
{
public Guid Id { get; set; }
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 BadgeReferenceObject ToProtoValue()
{
var proto = new BadgeReferenceObject
{
Id = Id.ToString(),
Type = Type,
Label = Label ?? string.Empty,
Caption = Caption ?? string.Empty,
ActivatedAt = ActivatedAt?.ToTimestamp(),
ExpiredAt = ExpiredAt?.ToTimestamp(),
AccountId = AccountId.ToString()
};
proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta!));
return proto;
}
public static SnAccountBadgeRef FromProtoValue(BadgeReferenceObject proto)
{
var badge = new SnAccountBadgeRef
{
Id = Guid.Parse(proto.Id),
Type = proto.Type,
Label = proto.Label,
Caption = proto.Caption,
Meta = GrpcTypeHelper.ConvertFromValueMap(proto.Meta).ToDictionary(),
ActivatedAt = proto.ActivatedAt?.ToInstant(),
ExpiredAt = proto.ExpiredAt?.ToInstant(),
AccountId = Guid.Parse(proto.AccountId)
};
return badge;
}
}

View File

@@ -0,0 +1,53 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Shared.Data;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SnBotAccount : 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 SnDevProject Project { get; set; } = null!;
[NotMapped] public SnAccount? Account { get; set; }
/// <summary>
/// This developer field is to serve the transparent info for user to know which developer
/// published this robot. Not for relationships usage.
/// </summary>
[NotMapped] public SnDeveloper? Developer { get; set; }
public Proto.BotAccount ToProtoValue()
{
var proto = new Proto.BotAccount
{
Slug = Slug,
IsActive = IsActive,
AutomatedId = Id.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
return proto;
}
public static SnBotAccount FromProto(Proto.BotAccount proto)
{
var botAccount = new SnBotAccount
{
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,79 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class SnChatMessage : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Type { get; set; } = null!;
[MaxLength(4096)] public string? Content { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
[Column(TypeName = "jsonb")] public List<Guid>? MembersMentioned { get; set; }
[MaxLength(36)] public string Nonce { get; set; } = null!;
public Instant? EditedAt { get; set; }
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
public ICollection<SnChatMessageReaction> Reactions { get; set; } = new List<SnChatMessageReaction>();
public Guid? RepliedMessageId { get; set; }
public SnChatMessage? RepliedMessage { get; set; }
public Guid? ForwardedMessageId { get; set; }
public SnChatMessage? ForwardedMessage { get; set; }
public Guid SenderId { get; set; }
public SnChatMember Sender { get; set; } = null!;
public Guid ChatRoomId { get; set; }
[JsonIgnore] public SnChatRoom ChatRoom { get; set; } = null!;
public string ResourceIdentifier => $"message:{Id}";
/// <summary>
/// Creates a shallow clone of this message for sync operations
/// </summary>
/// <returns>A new Message instance with copied properties</returns>
public SnChatMessage Clone()
{
return new SnChatMessage
{
Id = Id,
Type = Type,
Content = Content,
Meta = Meta,
MembersMentioned = MembersMentioned,
Nonce = Nonce,
EditedAt = EditedAt,
Attachments = Attachments,
RepliedMessageId = RepliedMessageId,
ForwardedMessageId = ForwardedMessageId,
SenderId = SenderId,
Sender = Sender,
ChatRoomId = ChatRoomId,
ChatRoom = ChatRoom,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt
};
}
}
public enum MessageReactionAttitude
{
Positive,
Neutral,
Negative,
}
public class SnChatMessageReaction : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid MessageId { get; set; }
[JsonIgnore] public SnChatMessage Message { get; set; } = null!;
public Guid SenderId { get; set; }
public SnChatMember Sender { get; set; } = null!;
[MaxLength(256)] public string Symbol { get; set; } = null!;
public MessageReactionAttitude Attitude { get; set; }
}

View File

@@ -0,0 +1,144 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum ChatRoomType
{
Group,
DirectMessage
}
public class SnChatRoom : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; }
[MaxLength(1024)] public string? Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
public ChatRoomType Type { get; set; }
public bool IsCommunity { get; set; }
public bool IsPublic { get; set; }
// Outdated fields, for backward compability
[MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; }
[JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>();
public Guid? RealmId { get; set; }
public SnRealm? Realm { get; set; }
[NotMapped]
[JsonPropertyName("members")]
public ICollection<ChatMemberTransmissionObject> DirectMembers { get; set; } =
new List<ChatMemberTransmissionObject>();
public string ResourceIdentifier => $"chatroom:{Id}";
}
public abstract class ChatMemberRole
{
public const int Owner = 100;
public const int Moderator = 50;
public const int Member = 0;
}
public enum ChatMemberNotify
{
All,
Mentions,
None
}
public enum ChatTimeoutCauseType
{
ByModerator = 0,
BySlowMode = 1,
}
public class ChatTimeoutCause
{
public ChatTimeoutCauseType Type { get; set; }
public Guid? SenderId { get; set; }
}
public class SnChatMember : ModelBase
{
public Guid Id { get; set; }
public Guid ChatRoomId { get; set; }
public SnChatRoom ChatRoom { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public SnAccount? Account { get; set; }
[NotMapped] public SnAccountStatus? Status { get; set; }
[MaxLength(1024)] public string? Nick { get; set; }
public int Role { get; set; } = ChatMemberRole.Member;
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
public Instant? LastReadAt { get; set; }
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
public bool IsBot { get; set; } = false;
/// <summary>
/// The break time is the user doesn't receive any message from this member for a while.
/// Expect mentioned him or her.
/// </summary>
public Instant? BreakUntil { get; set; }
/// <summary>
/// The timeout is the user can't send any message.
/// Set by the moderator of the chat room.
/// </summary>
public Instant? TimeoutUntil { get; set; }
/// <summary>
/// The timeout cause is the reason why the user is timeout.
/// </summary>
[Column(TypeName = "jsonb")] public ChatTimeoutCause? TimeoutCause { get; set; }
}
public class ChatMemberTransmissionObject : ModelBase
{
public Guid Id { get; set; }
public Guid ChatRoomId { get; set; }
public Guid AccountId { get; set; }
[NotMapped] public SnAccount Account { get; set; } = null!;
[MaxLength(1024)] public string? Nick { get; set; }
public int Role { get; set; } = ChatMemberRole.Member;
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
public bool IsBot { get; set; } = false;
public Instant? BreakUntil { get; set; }
public Instant? TimeoutUntil { get; set; }
public ChatTimeoutCause? TimeoutCause { get; set; }
public static ChatMemberTransmissionObject FromEntity(SnChatMember member)
{
return new ChatMemberTransmissionObject
{
Id = member.Id,
ChatRoomId = member.ChatRoomId,
AccountId = member.AccountId,
Account = member.Account!,
Nick = member.Nick,
Role = member.Role,
Notify = member.Notify,
JoinedAt = member.JoinedAt,
LeaveAt = member.LeaveAt,
IsBot = member.IsBot,
BreakUntil = member.BreakUntil,
TimeoutUntil = member.TimeoutUntil,
TimeoutCause = member.TimeoutCause,
CreatedAt = member.CreatedAt,
UpdatedAt = member.UpdatedAt,
DeletedAt = member.DeletedAt
};
}
}

View File

@@ -0,0 +1,141 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SnCloudFile : ModelBase, ICloudFile, IIdentifiedResource
{
/// The id generated by TuS, basically just UUID remove the dash lines
[MaxLength(32)]
public string Id { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty);
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
[MaxLength(4096)] public string? Description { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object?>? FileMeta { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object?>? UserMeta { get; set; }
[Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = [];
[MaxLength(256)] public string? MimeType { get; set; }
[MaxLength(256)] public string? Hash { get; set; }
public Instant? ExpiredAt { get; set; }
public long Size { get; set; }
public Instant? UploadedAt { get; set; }
public bool HasCompression { get; set; } = false;
public bool HasThumbnail { get; set; } = false;
public bool IsEncrypted { get; set; } = false;
public FilePool? Pool { get; set; }
public Guid? PoolId { get; set; }
[JsonIgnore] public SnFileBundle? Bundle { get; set; }
public Guid? BundleId { get; set; }
/// <summary>
/// The field is set to true if the recycling job plans to delete the file.
/// Due to the unstable of the recycling job, this doesn't really delete the file until a human verifies it.
/// </summary>
public bool IsMarkedRecycle { get; set; } = false;
/// The object name which stored remotely,
/// multiple cloud file may have same storage id to indicate they are the same file
///
/// If the storage id was null and the uploaded at is not null, means it is an embedding file,
/// The embedding file means the file is store on another site,
/// or it is a webpage (based on mimetype)
[MaxLength(32)]
public string? StorageId { get; set; }
/// This field should be null when the storage id is filled
/// Indicates the off-site accessible url of the file
[MaxLength(4096)]
public string? StorageUrl { get; set; }
[NotMapped]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FastUploadLink { get; set; }
public ICollection<CloudFileReference> References { get; set; } = new List<CloudFileReference>();
public Guid AccountId { get; set; }
public SnCloudFileReferenceObject ToReferenceObject()
{
return new SnCloudFileReferenceObject
{
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
DeletedAt = DeletedAt,
Id = Id,
Name = Name,
FileMeta = FileMeta ?? [],
UserMeta = UserMeta ?? [],
SensitiveMarks = SensitiveMarks,
MimeType = MimeType,
Hash = Hash,
Size = Size,
HasCompression = HasCompression
};
}
public string ResourceIdentifier => $"file:{Id}";
/// <summary>
/// Converts the CloudFile to a protobuf message
/// </summary>
/// <returns>The protobuf message representation of this object</returns>
public Proto.CloudFile ToProtoValue()
{
var proto = new Proto.CloudFile
{
Id = Id,
Name = Name,
MimeType = MimeType ?? string.Empty,
Hash = Hash ?? string.Empty,
Size = Size,
HasCompression = HasCompression,
Url = StorageUrl ?? string.Empty,
ContentType = MimeType ?? string.Empty,
UploadedAt = UploadedAt?.ToTimestamp(),
// Convert file metadata
FileMeta = GrpcTypeHelper.ConvertObjectToByteString(FileMeta),
// Convert user metadata
UserMeta = GrpcTypeHelper.ConvertObjectToByteString(UserMeta),
SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks)
};
return proto;
}
}
public class CloudFileReference : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(32)] public string FileId { get; set; } = null!;
public SnCloudFile File { get; set; } = null!;
[MaxLength(1024)] public string Usage { get; set; } = null!;
[MaxLength(1024)] public string ResourceId { get; set; } = null!;
/// <summary>
/// Optional expiration date for the file reference
/// </summary>
public Instant? ExpiredAt { get; set; }
/// <summary>
/// Converts the CloudFileReference to a protobuf message
/// </summary>
/// <returns>The protobuf message representation of this object</returns>
public Proto.CloudFileReference ToProtoValue()
{
return new Proto.CloudFileReference
{
Id = Id.ToString(),
FileId = FileId,
File = File?.ToProtoValue(),
Usage = Usage,
ResourceId = ResourceId,
ExpiredAt = ExpiredAt?.ToTimestamp()
};
}
}

View File

@@ -0,0 +1,80 @@
using DysonNetwork.Shared.Proto;
namespace DysonNetwork.Shared.Models;
public enum ContentSensitiveMark
{
Language,
SexualContent,
Violence,
Profanity,
HateSpeech,
Racism,
AdultContent,
DrugAbuse,
AlcoholAbuse,
Gambling,
SelfHarm,
ChildAbuse,
Other
}
/// <summary>
/// The class that used in jsonb columns which referenced the cloud file.
/// The aim of this class is to store some properties that won't change to a file to reduce the database load.
/// </summary>
public class SnCloudFileReferenceObject : ModelBase, ICloudFile
{
public string Id { get; set; } = null!;
public string Name { get; set; } = string.Empty;
public Dictionary<string, object?> FileMeta { get; set; } = null!;
public Dictionary<string, object?> UserMeta { get; set; } = null!;
public List<ContentSensitiveMark>? SensitiveMarks { get; set; }
public string? MimeType { get; set; }
public string? Hash { get; set; }
public long Size { get; set; }
public bool HasCompression { get; set; } = false;
public static SnCloudFileReferenceObject FromProtoValue(Proto.CloudFile proto)
{
return new SnCloudFileReferenceObject
{
Id = proto.Id,
Name = proto.Name,
FileMeta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object?>>(proto.FileMeta) ?? [],
UserMeta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object?>>(proto.UserMeta) ?? [],
SensitiveMarks = proto.HasSensitiveMarks
? GrpcTypeHelper.ConvertByteStringToObject<List<ContentSensitiveMark>>(proto.SensitiveMarks)
: [],
MimeType = proto.MimeType,
Hash = proto.Hash,
Size = proto.Size,
HasCompression = proto.HasCompression
};
}
/// <summary>
/// Converts the current object to its protobuf representation
/// </summary>
public CloudFile ToProtoValue()
{
var proto = new CloudFile
{
Id = Id,
Name = Name,
MimeType = MimeType ?? string.Empty,
Hash = Hash ?? string.Empty,
Size = Size,
HasCompression = HasCompression,
ContentType = MimeType ?? string.Empty,
Url = string.Empty,
// Convert file metadata
FileMeta = GrpcTypeHelper.ConvertObjectToByteString(FileMeta),
// Convert user metadata
UserMeta = GrpcTypeHelper.ConvertObjectToByteString(UserMeta),
SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks)
};
return proto;
}
}

View File

@@ -0,0 +1,174 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Google.Protobuf.WellKnownTypes;
using NodaTime.Serialization.Protobuf;
using NodaTime;
using SnVerificationMark = DysonNetwork.Shared.Models.SnVerificationMark;
namespace DysonNetwork.Shared.Models;
public enum CustomAppStatus
{
Developing,
Staging,
Production,
Suspended
}
public class SnCustomApp : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Slug { get; set; } = null!;
[MaxLength(1024)] public string Name { get; set; } = null!;
[MaxLength(4096)] public string? Description { get; set; }
public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
[JsonIgnore] public ICollection<SnCustomAppSecret> Secrets { get; set; } = new List<SnCustomAppSecret>();
public Guid ProjectId { get; set; }
public SnDevProject Project { get; set; } = null!;
[NotMapped]
public SnDeveloper Developer => Project.Developer;
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
public Proto.CustomApp ToProto()
{
return new Proto.CustomApp
{
Id = Id.ToString(),
Slug = Slug,
Name = Name,
Description = Description ?? string.Empty,
Status = Status switch
{
CustomAppStatus.Developing => Shared.Proto.CustomAppStatus.Developing,
CustomAppStatus.Staging => Shared.Proto.CustomAppStatus.Staging,
CustomAppStatus.Production => Shared.Proto.CustomAppStatus.Production,
CustomAppStatus.Suspended => Shared.Proto.CustomAppStatus.Suspended,
_ => Shared.Proto.CustomAppStatus.Unspecified
},
Picture = Picture?.ToProtoValue(),
Background = Background?.ToProtoValue(),
Verification = Verification?.ToProtoValue(),
Links = Links is null ? null : new Proto.CustomAppLinks
{
HomePage = Links.HomePage ?? string.Empty,
PrivacyPolicy = Links.PrivacyPolicy ?? string.Empty,
TermsOfService = Links.TermsOfService ?? string.Empty
},
OauthConfig = OauthConfig is null ? null : new Proto.CustomAppOauthConfig
{
ClientUri = OauthConfig.ClientUri ?? string.Empty,
RedirectUris = { OauthConfig.RedirectUris ?? [] },
PostLogoutRedirectUris = { OauthConfig.PostLogoutRedirectUris ?? [] },
AllowedScopes = { OauthConfig.AllowedScopes ?? [] },
AllowedGrantTypes = { OauthConfig.AllowedGrantTypes ?? [] },
RequirePkce = OauthConfig.RequirePkce,
AllowOfflineAccess = OauthConfig.AllowOfflineAccess
},
ProjectId = ProjectId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
}
public SnCustomApp FromProtoValue(Proto.CustomApp p)
{
Id = Guid.Parse(p.Id);
Slug = p.Slug;
Name = p.Name;
Description = string.IsNullOrEmpty(p.Description) ? null : p.Description;
Status = p.Status switch
{
Shared.Proto.CustomAppStatus.Developing => CustomAppStatus.Developing,
Shared.Proto.CustomAppStatus.Staging => CustomAppStatus.Staging,
Shared.Proto.CustomAppStatus.Production => CustomAppStatus.Production,
Shared.Proto.CustomAppStatus.Suspended => CustomAppStatus.Suspended,
_ => CustomAppStatus.Developing
};
ProjectId = string.IsNullOrEmpty(p.ProjectId) ? Guid.Empty : Guid.Parse(p.ProjectId);
CreatedAt = p.CreatedAt.ToInstant();
UpdatedAt = p.UpdatedAt.ToInstant();
if (p.Picture is not null) Picture = SnCloudFileReferenceObject.FromProtoValue(p.Picture);
if (p.Background is not null) Background = SnCloudFileReferenceObject.FromProtoValue(p.Background);
if (p.Verification is not null) Verification = SnVerificationMark.FromProtoValue(p.Verification);
if (p.Links is not null)
{
Links = new CustomAppLinks
{
HomePage = string.IsNullOrEmpty(p.Links.HomePage) ? null : p.Links.HomePage,
PrivacyPolicy = string.IsNullOrEmpty(p.Links.PrivacyPolicy) ? null : p.Links.PrivacyPolicy,
TermsOfService = string.IsNullOrEmpty(p.Links.TermsOfService) ? null : p.Links.TermsOfService
};
}
return this;
}
}
public class CustomAppLinks
{
[MaxLength(8192)] public string? HomePage { get; set; }
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
[MaxLength(8192)] public string? TermsOfService { get; set; }
}
public class CustomAppOauthConfig
{
[MaxLength(1024)] public string? ClientUri { get; set; }
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
[MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
[MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"];
[MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
public bool RequirePkce { get; set; } = true;
public bool AllowOfflineAccess { get; set; } = false;
}
public class SnCustomAppSecret : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Secret { get; set; } = null!;
[MaxLength(4096)] public string? Description { get; set; } = null!;
public Instant? ExpiredAt { get; set; }
public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth
public Guid AppId { get; set; }
public SnCustomApp App { get; set; } = null!;
public static SnCustomAppSecret FromProtoValue(Proto.CustomAppSecret p)
{
return new SnCustomAppSecret
{
Id = Guid.Parse(p.Id),
Secret = p.Secret,
Description = p.Description,
ExpiredAt = p.ExpiredAt?.ToInstant(),
IsOidc = p.IsOidc,
AppId = Guid.Parse(p.AppId),
};
}
public Proto.CustomAppSecret ToProto()
{
return new Proto.CustomAppSecret
{
Id = Id.ToString(),
Secret = Secret,
Description = Description,
ExpiredAt = ExpiredAt?.ToTimestamp(),
IsOidc = IsOidc,
AppId = Id.ToString(),
};
}
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace DysonNetwork.Shared.Models;
public class SnDevProject : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Slug { get; set; } = string.Empty;
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
[MaxLength(4096)] public string Description { get; set; } = string.Empty;
public SnDeveloper Developer { get; set; } = null!;
public Guid DeveloperId { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace DysonNetwork.Shared.Models;
public class SnDeveloper
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PublisherId { get; set; }
[JsonIgnore] public List<SnDevProject> Projects { get; set; } = [];
[NotMapped] public SnPublisher? Publisher { get; set; }
}

View File

@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class ExperienceRecord : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string ReasonType { get; set; } = string.Empty;
[MaxLength(1024)] public string Reason { get; set; } = string.Empty;
public long Delta { get; set; }
public double BonusMultiplier { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Proto.ExperienceRecord ToProto()
{
var proto = new Proto.ExperienceRecord
{
Id = Id.ToString(),
ReasonType = ReasonType,
Reason = Reason,
Delta = Delta,
BonusMultiplier = BonusMultiplier,
AccountId = AccountId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
return proto;
}
}

View File

@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Shared.Models;
[Index(nameof(Slug), IsUnique = true)]
public class SnFileBundle : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Slug { get; set; } = null!;
[MaxLength(1024)] public string Name { get; set; } = null!;
[MaxLength(8192)] public string? Description { get; set; }
[MaxLength(256)] public string? Passcode { get; set; }
public List<SnCloudFile> Files { get; set; } = new();
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
public SnFileBundle HashPasscode()
{
if (string.IsNullOrEmpty(Passcode)) return this;
Passcode = BCrypt.Net.BCrypt.HashPassword(Passcode);
return this;
}
public bool VerifyPasscode(string? passcode)
{
if (string.IsNullOrEmpty(Passcode)) return true;
if (string.IsNullOrEmpty(passcode)) return false;
return BCrypt.Net.BCrypt.Verify(passcode, Passcode);
}
}

View File

@@ -0,0 +1,56 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class RemoteStorageConfig
{
public string Region { get; set; } = string.Empty;
public string Bucket { get; set; } = string.Empty;
public string Endpoint { get; set; } = string.Empty;
public string SecretId { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public bool EnableSigned { get; set; }
public bool EnableSsl { get; set; }
public string? ImageProxy { get; set; }
public string? AccessProxy { get; set; }
public Duration? Expiration { get; set; }
}
public class BillingConfig
{
public double? CostMultiplier { get; set; } = 1.0;
}
public class PolicyConfig
{
public bool EnableFastUpload { get; set; } = false;
public bool EnableRecycle { get; set; } = false;
public bool PublicIndexable { get; set; } = false;
public bool PublicUsable { get; set; } = false;
public bool NoOptimization { get; set; } = false;
public bool NoMetadata { get; set; } = false;
public bool AllowEncryption { get; set; } = true;
public bool AllowAnonymous { get; set; } = true;
public List<string>? AcceptTypes { get; set; }
public long? MaxFileSize { get; set; }
public int RequirePrivilege { get; set; } = 0;
}
public class FilePool : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
[MaxLength(8192)] public string Description { get; set; } = string.Empty;
[Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new();
[Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new();
[Column(TypeName = "jsonb")] public PolicyConfig PolicyConfig { get; set; } = new();
public bool IsHidden { get; set; } = false;
public Guid? AccountId { get; set; }
public string ResourceIdentifier => $"file-pool/{Id}";
}

View File

@@ -0,0 +1,55 @@
using NodaTime;
namespace DysonNetwork.Shared.Models;
/// <summary>
/// Common interface for cloud file entities that can be used in file operations.
/// This interface exposes the essential properties needed for file operations
/// and is implemented by both CloudFile and CloudFileReferenceObject.
/// </summary>
public interface ICloudFile
{
public Instant CreatedAt { get; }
public Instant UpdatedAt { get; }
public Instant? DeletedAt { get; }
/// <summary>
/// Gets the unique identifier of the cloud file.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the name of the cloud file.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the file metadata dictionary.
/// </summary>
Dictionary<string, object?> FileMeta { get; }
/// <summary>
/// Gets the user metadata dictionary.
/// </summary>
Dictionary<string, object>? UserMeta { get; }
/// <summary>
/// Gets the MIME type of the file.
/// </summary>
string? MimeType { get; }
/// <summary>
/// Gets the hash of the file content.
/// </summary>
string? Hash { get; }
/// <summary>
/// Gets the size of the file in bytes.
/// </summary>
long Size { get; }
/// <summary>
/// Gets whether the file has a compressed version available.
/// </summary>
bool HasCompression { get; }
}

View File

@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum MagicSpellType
{
AccountActivation,
AccountDeactivation,
AccountRemoval,
AuthPasswordReset,
ContactVerification,
}
[Index(nameof(Spell), IsUnique = true)]
public class SnMagicSpell : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[JsonIgnore] [MaxLength(1024)] public string Spell { get; set; } = null!;
public MagicSpellType Type { get; set; }
public Instant? ExpiresAt { get; set; }
public Instant? AffectedAt { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
public Guid? AccountId { get; set; }
public SnAccount? Account { get; set; }
}

View File

@@ -0,0 +1,15 @@
using NodaTime;
namespace DysonNetwork.Shared.Models;
public interface IIdentifiedResource
{
public string ResourceIdentifier { get; }
}
public abstract class ModelBase
{
public Instant CreatedAt { get; set; }
public Instant UpdatedAt { get; set; }
public Instant? DeletedAt { get; set; }
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class SnNotification : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Topic { get; set; } = null!;
[MaxLength(1024)] public string? Title { get; set; }
[MaxLength(2048)] public string? Subtitle { get; set; }
[MaxLength(4096)] public string? Content { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object?> Meta { get; set; } = new();
public int Priority { get; set; } = 10;
public Instant? ViewedAt { get; set; }
public Guid AccountId { get; set; }
}
public enum PushProvider
{
Apple,
Google
}
[Index(nameof(AccountId), nameof(DeviceId), nameof(DeletedAt), IsUnique = true)]
public class SnNotificationPushSubscription : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid AccountId { get; set; }
[MaxLength(8192)] public string DeviceId { get; set; } = null!;
[MaxLength(8192)] public string DeviceToken { get; set; } = null!;
public PushProvider Provider { get; set; }
public int CountDelivered { get; set; }
public Instant? LastUsedAt { get; set; }
}

View File

@@ -0,0 +1,125 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class WalletCurrency
{
public const string SourcePoint = "points";
public const string GoldenPoint = "golds";
}
public enum OrderStatus
{
Unpaid,
Paid,
Cancelled,
Finished,
Expired
}
public class SnWalletOrder : ModelBase
{
public const string InternalAppIdentifier = "internal";
public Guid Id { get; set; } = Guid.NewGuid();
public OrderStatus Status { get; set; } = OrderStatus.Unpaid;
[MaxLength(128)] public string Currency { get; set; } = null!;
[MaxLength(4096)] public string? Remarks { get; set; }
[MaxLength(4096)] public string? AppIdentifier { get; set; }
[MaxLength(4096)] public string? ProductIdentifier { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
public decimal Amount { get; set; }
public Instant ExpiredAt { get; set; }
public Guid? PayeeWalletId { get; set; }
public SnWallet? PayeeWallet { get; set; } = null!;
public Guid? TransactionId { get; set; }
public SnWalletTransaction? Transaction { get; set; }
public Proto.Order ToProtoValue() => new()
{
Id = Id.ToString(),
Status = (Proto.OrderStatus)Status,
Currency = Currency,
Remarks = Remarks,
AppIdentifier = AppIdentifier,
ProductIdentifier = ProductIdentifier,
Meta = Meta == null
? null
: Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)),
Amount = Amount.ToString(CultureInfo.InvariantCulture),
ExpiredAt = ExpiredAt.ToTimestamp(),
PayeeWalletId = PayeeWalletId?.ToString(),
TransactionId = TransactionId?.ToString(),
Transaction = Transaction?.ToProtoValue(),
};
public static SnWalletOrder FromProtoValue(Proto.Order proto) => new()
{
Id = Guid.Parse(proto.Id),
Status = (OrderStatus)proto.Status,
Currency = proto.Currency,
Remarks = proto.Remarks,
AppIdentifier = proto.AppIdentifier,
ProductIdentifier = proto.ProductIdentifier,
Meta = proto.HasMeta
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray())
: null,
Amount = decimal.Parse(proto.Amount),
ExpiredAt = proto.ExpiredAt.ToInstant(),
PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null,
TransactionId = proto.TransactionId is not null ? Guid.Parse(proto.TransactionId) : null,
Transaction = proto.Transaction is not null ? SnWalletTransaction.FromProtoValue(proto.Transaction) : null,
};
}
public enum TransactionType
{
System,
Transfer,
Order
}
public class SnWalletTransaction : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(128)] public string Currency { get; set; } = null!;
public decimal Amount { get; set; }
[MaxLength(4096)] public string? Remarks { get; set; }
public TransactionType Type { get; set; }
// When the payer is null, it's pay from the system
public Guid? PayerWalletId { get; set; }
public SnWallet? PayerWallet { get; set; }
// When the payee is null, it's pay for the system
public Guid? PayeeWalletId { get; set; }
public SnWallet? PayeeWallet { get; set; }
public Proto.Transaction ToProtoValue() => new()
{
Id = Id.ToString(),
Currency = Currency,
Amount = Amount.ToString(CultureInfo.InvariantCulture),
Remarks = Remarks,
Type = (Proto.TransactionType)Type,
PayerWalletId = PayerWalletId?.ToString(),
PayeeWalletId = PayeeWalletId?.ToString(),
};
public static SnWalletTransaction FromProtoValue(Proto.Transaction proto) => new()
{
Id = Guid.Parse(proto.Id),
Currency = proto.Currency,
Amount = decimal.Parse(proto.Amount),
Remarks = proto.Remarks,
Type = (TransactionType)proto.Type,
PayerWalletId = proto.PayerWalletId is not null ? Guid.Parse(proto.PayerWalletId) : null,
PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null,
};
}

View File

@@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Shared.Models;
/// The permission node model provides the infrastructure of permission control in Dyson Network.
/// It based on the ABAC permission model.
///
/// The value can be any type, boolean and number for most cases and stored in jsonb.
///
/// The area represents the region this permission affects. For example, the pub:&lt;publisherId&gt;
/// indicates it's a permission node for the publishers managing.
///
/// And the actor shows who owns the permission, in most cases, the user:&lt;userId&gt;
/// and when the permission node has a GroupId, the actor will be set to the group, but it won't work on checking
/// expect the member of that permission group inherent the permission from the group.
[Index(nameof(Key), nameof(Area), nameof(Actor))]
public class SnPermissionNode : ModelBase, IDisposable
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Actor { get; set; } = null!;
[MaxLength(1024)] public string Area { get; set; } = null!;
[MaxLength(1024)] public string Key { get; set; } = null!;
[Column(TypeName = "jsonb")] public JsonDocument Value { get; set; } = null!;
public Instant? ExpiredAt { get; set; } = null;
public Instant? AffectedAt { get; set; } = null;
public Guid? GroupId { get; set; } = null;
[JsonIgnore] public SnPermissionGroup? Group { get; set; } = null;
public void Dispose()
{
Value.Dispose();
GC.SuppressFinalize(this);
}
}
public class SnPermissionGroup : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Key { get; set; } = null!;
public ICollection<SnPermissionNode> Nodes { get; set; } = [];
[JsonIgnore] public ICollection<SnPermissionGroupMember> Members { get; set; } = [];
}
public class SnPermissionGroupMember : ModelBase
{
public Guid GroupId { get; set; }
public SnPermissionGroup Group { get; set; } = null!;
[MaxLength(1024)] public string Actor { get; set; } = null!;
public Instant? ExpiredAt { get; set; }
public Instant? AffectedAt { get; set; }
}

View File

@@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class SnPoll : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public List<SnPollQuestion> Questions { get; set; } = new();
[MaxLength(1024)] public string? Title { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
public Instant? EndedAt { get; set; }
public bool IsAnonymous { get; set; }
public Guid PublisherId { get; set; }
[JsonIgnore] public SnPublisher? Publisher { get; set; }
}
public enum PollQuestionType
{
SingleChoice,
MultipleChoice,
YesNo,
Rating,
FreeText
}
public class SnPollQuestion : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public PollQuestionType Type { get; set; }
[Column(TypeName = "jsonb")] public List<SnPollOption>? Options { get; set; }
[MaxLength(1024)] public string Title { get; set; } = null!;
[MaxLength(4096)] public string? Description { get; set; }
public int Order { get; set; } = 0;
public bool IsRequired { get; set; }
public Guid PollId { get; set; }
[JsonIgnore] public SnPoll Poll { get; set; } = null!;
}
public class SnPollOption
{
public Guid Id { get; set; } = Guid.NewGuid();
[Required][MaxLength(1024)] public string Label { get; set; } = null!;
[MaxLength(4096)] public string? Description { get; set; }
public int Order { get; set; } = 0;
}
public class SnPollAnswer : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[Column(TypeName = "jsonb")] public Dictionary<string, JsonElement> Answer { get; set; } = null!;
public Guid AccountId { get; set; }
public Guid PollId { get; set; }
[JsonIgnore] public SnPoll? Poll { get; set; }
[NotMapped] public SnAccount? Account { get; set; }
}

View File

@@ -0,0 +1,200 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
using NpgsqlTypes;
namespace DysonNetwork.Shared.Models;
public enum PostType
{
Moment,
Article
}
public enum PostVisibility
{
Public,
Friends,
Unlisted,
Private
}
public enum PostPinMode
{
PublisherPage,
RealmPage,
ReplyPage,
}
public class SnPost : ModelBase, IIdentifiedResource, IActivity
{
public Guid Id { get; set; }
[MaxLength(1024)] public string? Title { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
[MaxLength(1024)] public string? Slug { get; set; }
public Instant? EditedAt { get; set; }
public Instant? PublishedAt { get; set; }
public PostVisibility Visibility { get; set; } = PostVisibility.Public;
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string? Content { get; set; }
public PostType Type { get; set; }
public PostPinMode? PinMode { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
[Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = [];
[Column(TypeName = "jsonb")] public PostEmbedView? EmbedView { get; set; }
public int ViewsUnique { get; set; }
public int ViewsTotal { get; set; }
public int Upvotes { get; set; }
public int Downvotes { get; set; }
public decimal AwardedScore { get; set; }
[NotMapped] public Dictionary<string, int> ReactionsCount { get; set; } = new();
[NotMapped] public int RepliesCount { get; set; }
[NotMapped] public Dictionary<string, bool>? ReactionsMade { get; set; }
public bool RepliedGone { get; set; }
public bool ForwardedGone { get; set; }
public Guid? RepliedPostId { get; set; }
public SnPost? RepliedPost { get; set; }
public Guid? ForwardedPostId { get; set; }
public SnPost? ForwardedPost { get; set; }
public Guid? RealmId { get; set; }
public SnRealm? Realm { get; set; }
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
[JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!;
public Guid PublisherId { get; set; }
public SnPublisher Publisher { get; set; } = null!;
public ICollection<SnPostAward> Awards { get; set; } = null!;
[JsonIgnore] public ICollection<SnPostReaction> Reactions { get; set; } = new List<SnPostReaction>();
public ICollection<SnPostTag> Tags { get; set; } = new List<SnPostTag>();
public ICollection<SnPostCategory> Categories { get; set; } = new List<SnPostCategory>();
[JsonIgnore] public ICollection<SnPostCollection> Collections { get; set; } = new List<SnPostCollection>();
[JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null;
[NotMapped] public bool IsTruncated { get; set; } = false;
public string ResourceIdentifier => $"post:{Id}";
public SnActivity ToActivity()
{
return new SnActivity()
{
CreatedAt = PublishedAt ?? CreatedAt,
UpdatedAt = UpdatedAt,
DeletedAt = DeletedAt,
Id = Id,
Type = RepliedPostId is null ? "posts.new" : "posts.new.replies",
ResourceIdentifier = ResourceIdentifier,
Data = this
};
}
}
public class SnPostTag : ModelBase
{
public Guid Id { get; set; }
[MaxLength(128)] public string Slug { get; set; } = null!;
[MaxLength(256)] public string? Name { get; set; }
[JsonIgnore] public ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped] public int? Usage { get; set; }
}
public class SnPostCategory : ModelBase
{
public Guid Id { get; set; }
[MaxLength(128)] public string Slug { get; set; } = null!;
[MaxLength(256)] public string? Name { get; set; }
[JsonIgnore] public ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped] public int? Usage { get; set; }
}
public class SnPostCategorySubscription : ModelBase
{
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public Guid? CategoryId { get; set; }
public SnPostCategory? Category { get; set; }
public Guid? TagId { get; set; }
public SnPostTag? Tag { get; set; }
}
public class SnPostCollection : ModelBase
{
public Guid Id { get; set; }
[MaxLength(128)] public string Slug { get; set; } = null!;
[MaxLength(256)] public string? Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
public SnPublisher Publisher { get; set; } = null!;
public ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
}
public class SnPostFeaturedRecord : ModelBase
{
public Guid Id { get; set; }
public Guid PostId { get; set; }
public SnPost Post { get; set; } = null!;
public Instant? FeaturedAt { get; set; }
public int SocialCredits { get; set; }
}
public enum PostReactionAttitude
{
Positive,
Neutral,
Negative,
}
public class SnPostReaction : ModelBase
{
public Guid Id { get; set; }
[MaxLength(256)] public string Symbol { get; set; } = null!;
public PostReactionAttitude Attitude { get; set; }
public Guid PostId { get; set; }
[JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; }
}
public class SnPostAward : ModelBase
{
public Guid Id { get; set; }
public decimal Amount { get; set; }
public PostReactionAttitude Attitude { get; set; }
[MaxLength(4096)] public string? Message { get; set; }
public Guid PostId { get; set; }
[JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; }
}
/// <summary>
/// This model is used to tell the client to render a WebView / iframe
/// Usually external website and web pages
/// Used as a JSON column
/// </summary>
public class PostEmbedView
{
public string Uri { get; set; } = null!;
public double? AspectRatio { get; set; }
public PostEmbedViewRenderer Renderer { get; set; } = PostEmbedViewRenderer.WebView;
}
public enum PostEmbedViewRenderer
{
WebView
}

View File

@@ -0,0 +1,166 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public enum PublisherType
{
Individual,
Organizational
}
[Index(nameof(Name), IsUnique = true)]
public class SnPublisher : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
public PublisherType Type { get; set; }
[MaxLength(256)] public string Name { get; set; } = string.Empty;
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
[MaxLength(4096)] public string? Bio { get; set; }
// Outdated fields, for backward compability
[MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<SnPost> Posts { get; set; } = [];
[JsonIgnore] public ICollection<SnPoll> Polls { get; set; } = [];
[JsonIgnore] public ICollection<SnPostCollection> Collections { get; set; } = [];
[JsonIgnore] public ICollection<SnPublisherMember> Members { get; set; } = [];
[JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = [];
[JsonIgnore]
public ICollection<PublisherSubscription> Subscriptions { get; set; } = [];
public Guid? AccountId { get; set; }
public Guid? RealmId { get; set; }
[JsonIgnore] public SnRealm? Realm { get; set; }
[NotMapped] public Account? Account { get; set; }
public string ResourceIdentifier => $"publisher:{Id}";
public Proto.Publisher ToProto()
{
var p = new Proto.Publisher()
{
Id = Id.ToString(),
Type = Type == PublisherType.Individual
? Shared.Proto.PublisherType.PubIndividual
: Shared.Proto.PublisherType.PubOrganizational,
Name = Name,
Nick = Nick,
Bio = Bio,
AccountId = AccountId?.ToString() ?? string.Empty,
RealmId = RealmId?.ToString() ?? string.Empty,
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
if (Picture is not null)
{
p.Picture = new Proto.CloudFile
{
Id = Picture.Id,
Name = Picture.Name,
MimeType = Picture.MimeType,
Hash = Picture.Hash,
Size = Picture.Size,
};
}
if (Background is not null)
{
p.Background = new Proto.CloudFile
{
Id = Background.Id,
Name = Background.Name,
MimeType = Background.MimeType,
Hash = Background.Hash,
Size = Background.Size,
};
}
return p;
}
}
public enum PublisherMemberRole
{
Owner = 100,
Manager = 75,
Editor = 50,
Viewer = 25
}
public class SnPublisherMember : ModelBase
{
public Guid PublisherId { get; set; }
[JsonIgnore] public SnPublisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public Account? Account { get; set; }
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
public Instant? JoinedAt { get; set; }
public Proto.PublisherMember ToProto()
{
return new Proto.PublisherMember()
{
PublisherId = PublisherId.ToString(),
AccountId = AccountId.ToString(),
Role = Role switch
{
PublisherMemberRole.Owner => Shared.Proto.PublisherMemberRole.Owner,
PublisherMemberRole.Manager => Shared.Proto.PublisherMemberRole.Manager,
PublisherMemberRole.Editor => Shared.Proto.PublisherMemberRole.Editor,
PublisherMemberRole.Viewer => Shared.Proto.PublisherMemberRole.Viewer,
_ => throw new ArgumentOutOfRangeException(nameof(Role), Role, null)
},
JoinedAt = JoinedAt?.ToTimestamp()
};
}
}
public enum PublisherSubscriptionStatus
{
Active,
Expired,
Cancelled
}
public class PublisherSubscription : ModelBase
{
public Guid Id { get; set; }
public Guid PublisherId { get; set; }
[JsonIgnore] public SnPublisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; }
public PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active;
public int Tier { get; set; } = 0;
}
public class PublisherFeature : ModelBase
{
public Guid Id { get; set; }
[MaxLength(1024)] public string Flag { get; set; } = null!;
public Instant? ExpiredAt { get; set; }
public Guid PublisherId { get; set; }
public SnPublisher Publisher { get; set; } = null!;
}
public abstract class PublisherFeatureFlag
{
public static List<string> AllFlags => [Develop];
public static string Develop = "develop";
}

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum PunishmentType
{
// TODO: impl the permission modification
PermissionModification,
BlockLogin,
DisableAccount,
Strike
}
public class SnAccountPunishment : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(8192)] public string Reason { get; set; } = string.Empty;
public Instant? ExpiredAt { get; set; }
public PunishmentType Type { get; set; }
[Column(TypeName = "jsonb")] public List<string>? BlockedPermissions { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
}

View File

@@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Shared.Models;
[Index(nameof(Slug), IsUnique = true)]
public class SnRealm : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; }
[MaxLength(1024)] public string Slug { get; set; } = string.Empty;
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
[MaxLength(4096)] public string Description { get; set; } = string.Empty;
public bool IsCommunity { get; set; }
public bool IsPublic { get; set; }
// Outdated fields, for backward compability
[MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<SnRealmMember> Members { get; set; } = new List<SnRealmMember>();
[JsonIgnore] public ICollection<SnChatRoom> ChatRooms { get; set; } = new List<SnChatRoom>();
public Guid AccountId { get; set; }
public string ResourceIdentifier => $"realm:{Id}";
}
public abstract class RealmMemberRole
{
public const int Owner = 100;
public const int Moderator = 50;
public const int Normal = 0;
}
public class SnRealmMember : ModelBase
{
public Guid RealmId { get; set; }
public SnRealm Realm { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public SnAccount? Account { get; set; }
[NotMapped] public SnAccountStatus? Status { get; set; }
public int Role { get; set; } = RealmMemberRole.Normal;
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
}

View File

@@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class SnRealtimeCall : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Instant? EndedAt { get; set; }
public Guid SenderId { get; set; }
public SnChatMember Sender { get; set; } = null!;
public Guid RoomId { get; set; }
public SnChatRoom Room { get; set; } = null!;
/// <summary>
/// Provider name (e.g., "cloudflare", "agora", "twilio")
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// Service provider's session identifier
/// </summary>
public string? SessionId { get; set; }
/// <summary>
/// JSONB column containing provider-specific configuration
/// </summary>
[Column(name: "upstream", TypeName = "jsonb")]
public string? UpstreamConfigJson { get; set; }
/// <summary>
/// Deserialized upstream configuration
/// </summary>
[NotMapped]
public Dictionary<string, object> UpstreamConfig
{
get => string.IsNullOrEmpty(UpstreamConfigJson)
? new Dictionary<string, object>()
: JsonSerializer.Deserialize<Dictionary<string, object>>(UpstreamConfigJson) ?? new Dictionary<string, object>();
set => UpstreamConfigJson = value.Count > 0
? JsonSerializer.Serialize(value)
: null;
}
}

View File

@@ -0,0 +1,34 @@
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public enum RelationshipStatus : short
{
Friends = 100,
Pending = 0,
Blocked = -100
}
public class SnAccountRelationship : ModelBase
{
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Guid RelatedId { get; set; }
public SnAccount Related { get; set; } = null!;
public Instant? ExpiredAt { get; set; }
public RelationshipStatus Status { get; set; } = RelationshipStatus.Pending;
public Proto.Relationship ToProtoValue() => new()
{
AccountId = AccountId.ToString(),
RelatedId = RelatedId.ToString(),
Account = Account.ToProtoValue(),
Related = Related.ToProtoValue(),
Status = (int)Status,
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
}

View File

@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public class SocialCreditRecord : ModelBase
{
public Guid Id { get; set; }
[MaxLength(1024)] public string ReasonType { get; set; } = string.Empty;
[MaxLength(1024)] public string Reason { get; set; } = string.Empty;
public double Delta { get; set; }
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Proto.SocialCreditRecord ToProto()
{
var proto = new Proto.SocialCreditRecord
{
Id = Id.ToString(),
ReasonType = ReasonType,
Reason = Reason,
Delta = Delta,
AccountId = AccountId.ToString(),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
return proto;
}
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Shared.Models;
[Index(nameof(Slug))] // The slug index shouldn't be unique, the sticker slug can be repeated across packs.
public class SnSticker : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(128)] public string Slug { get; set; } = null!;
// Outdated fields, for backward compability
[MaxLength(32)] public string? ImageId { get; set; }
[Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Image { get; set; } = null!;
public Guid PackId { get; set; }
[JsonIgnore] public StickerPack Pack { get; set; } = null!;
public string ResourceIdentifier => $"sticker/{Id}";
}
[Index(nameof(Prefix), IsUnique = true)]
public class StickerPack : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Name { get; set; } = null!;
[MaxLength(4096)] public string Description { get; set; } = string.Empty;
[MaxLength(128)] public string Prefix { get; set; } = null!;
public List<SnSticker> Stickers { get; set; } = [];
[JsonIgnore] public List<StickerPackOwnership> Ownerships { get; set; } = [];
public Guid PublisherId { get; set; }
public SnPublisher Publisher { get; set; } = null!;
}
public class StickerPackOwnership : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PackId { get; set; }
public StickerPack Pack { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public SnAccount Account { get; set; } = null!;
}

View File

@@ -0,0 +1,404 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
public record class SubscriptionTypeData(
string Identifier,
string? GroupIdentifier,
string Currency,
decimal BasePrice,
int? RequiredLevel = null
)
{
public static readonly Dictionary<string, SubscriptionTypeData> SubscriptionDict =
new()
{
[SubscriptionType.Twinkle] = new SubscriptionTypeData(
SubscriptionType.Twinkle,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
0,
1
),
[SubscriptionType.Stellar] = new SubscriptionTypeData(
SubscriptionType.Stellar,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
1200,
3
),
[SubscriptionType.Nova] = new SubscriptionTypeData(
SubscriptionType.Nova,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
2400,
6
),
[SubscriptionType.Supernova] = new SubscriptionTypeData(
SubscriptionType.Supernova,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
3600,
9
)
};
public static readonly Dictionary<string, string> SubscriptionHumanReadable =
new()
{
[SubscriptionType.Twinkle] = "Stellar Program Twinkle",
[SubscriptionType.Stellar] = "Stellar Program",
[SubscriptionType.Nova] = "Stellar Program Nova",
[SubscriptionType.Supernova] = "Stellar Program Supernova"
};
}
public abstract class SubscriptionType
{
/// <summary>
/// DO NOT USE THIS TYPE DIRECTLY,
/// this is the prefix of all the stellar program subscriptions.
/// </summary>
public const string StellarProgram = "solian.stellar";
/// <summary>
/// No actual usage, just tells there is a free level named twinkle.
/// Applies to every registered user by default, so there is no need to create a record in db for that.
/// </summary>
public const string Twinkle = "solian.stellar.twinkle";
public const string Stellar = "solian.stellar.primary";
public const string Nova = "solian.stellar.nova";
public const string Supernova = "solian.stellar.supernova";
}
public abstract class SubscriptionPaymentMethod
{
/// <summary>
/// The solar points / solar dollars.
/// </summary>
public const string InAppWallet = "solian.wallet";
/// <summary>
/// afdian.com
/// aka. China patreon
/// </summary>
public const string Afdian = "afdian";
}
public enum SubscriptionStatus
{
Unpaid,
Active,
Expired,
Cancelled
}
/// <summary>
/// The subscription is for the Stellar Program in most cases.
/// The paid subscription in another word.
/// </summary>
[Index(nameof(Identifier))]
public class SnSubscription : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Instant BegunAt { get; set; }
public Instant? EndedAt { get; set; }
/// <summary>
/// The type of the subscriptions
/// </summary>
[MaxLength(4096)]
public string Identifier { get; set; } = null!;
/// <summary>
/// The field is used to override the activation status of the membership.
/// Might be used for refund handling and other special cases.
///
/// Go see the IsAvailable field if you want to get real the status of the membership.
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Indicates is the current user got the membership for free,
/// to prevent giving the same discount for the same user again.
/// </summary>
public bool IsFreeTrial { get; set; }
public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Unpaid;
[MaxLength(4096)] public string PaymentMethod { get; set; } = null!;
[Column(TypeName = "jsonb")] public PaymentDetails PaymentDetails { get; set; } = null!;
public decimal BasePrice { get; set; }
public Guid? CouponId { get; set; }
public Coupon? Coupon { get; set; }
public Instant? RenewalAt { get; set; }
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
[NotMapped]
public bool IsAvailable
{
get
{
if (!IsActive) return false;
var now = SystemClock.Instance.GetCurrentInstant();
if (BegunAt > now) return false;
if (EndedAt.HasValue && now > EndedAt.Value) return false;
if (RenewalAt.HasValue && now > RenewalAt.Value) return false;
if (Status != SubscriptionStatus.Active) return false;
return true;
}
}
[NotMapped]
public decimal FinalPrice
{
get
{
if (IsFreeTrial) return 0;
if (Coupon == null) return BasePrice;
var now = SystemClock.Instance.GetCurrentInstant();
if (Coupon.AffectedAt.HasValue && now < Coupon.AffectedAt.Value ||
Coupon.ExpiredAt.HasValue && now > Coupon.ExpiredAt.Value) return BasePrice;
if (Coupon.DiscountAmount.HasValue) return BasePrice - Coupon.DiscountAmount.Value;
if (Coupon.DiscountRate.HasValue) return BasePrice * (decimal)(1 - Coupon.DiscountRate.Value);
return BasePrice;
}
}
/// <summary>
/// Returns a reference object that contains a subset of subscription data
/// suitable for client-side use, with sensitive information removed.
/// </summary>
public SubscriptionReferenceObject ToReference()
{
return new SubscriptionReferenceObject
{
Id = Id,
Identifier = Identifier,
BegunAt = BegunAt,
EndedAt = EndedAt,
IsActive = IsActive,
IsAvailable = IsAvailable,
IsFreeTrial = IsFreeTrial,
Status = Status,
BasePrice = BasePrice,
FinalPrice = FinalPrice,
RenewalAt = RenewalAt,
AccountId = AccountId
};
}
public Proto.Subscription ToProtoValue() => new()
{
Id = Id.ToString(),
BegunAt = BegunAt.ToTimestamp(),
EndedAt = EndedAt?.ToTimestamp(),
Identifier = Identifier,
IsActive = IsActive,
IsFreeTrial = IsFreeTrial,
Status = (Proto.SubscriptionStatus)Status,
PaymentMethod = PaymentMethod,
PaymentDetails = PaymentDetails.ToProtoValue(),
BasePrice = BasePrice.ToString(CultureInfo.InvariantCulture),
CouponId = CouponId?.ToString(),
Coupon = Coupon?.ToProtoValue(),
RenewalAt = RenewalAt?.ToTimestamp(),
AccountId = AccountId.ToString(),
IsAvailable = IsAvailable,
FinalPrice = FinalPrice.ToString(CultureInfo.InvariantCulture),
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
public static SnSubscription FromProtoValue(Proto.Subscription proto) => new()
{
Id = Guid.Parse(proto.Id),
BegunAt = proto.BegunAt.ToInstant(),
EndedAt = proto.EndedAt?.ToInstant(),
Identifier = proto.Identifier,
IsActive = proto.IsActive,
IsFreeTrial = proto.IsFreeTrial,
Status = (SubscriptionStatus)proto.Status,
PaymentMethod = proto.PaymentMethod,
PaymentDetails = PaymentDetails.FromProtoValue(proto.PaymentDetails),
BasePrice = decimal.Parse(proto.BasePrice),
CouponId = proto.HasCouponId ? Guid.Parse(proto.CouponId) : null,
Coupon = proto.Coupon is not null ? Coupon.FromProtoValue(proto.Coupon) : null,
RenewalAt = proto.RenewalAt?.ToInstant(),
AccountId = Guid.Parse(proto.AccountId),
CreatedAt = proto.CreatedAt.ToInstant(),
UpdatedAt = proto.UpdatedAt.ToInstant()
};
}
/// <summary>
/// A reference object for Subscription that contains only non-sensitive information
/// suitable for client-side use.
/// </summary>
public class SubscriptionReferenceObject : ModelBase
{
public Guid Id { get; set; }
public string Identifier { get; set; } = null!;
public Instant BegunAt { get; set; }
public Instant? EndedAt { get; set; }
public bool IsActive { get; set; }
public bool IsAvailable { get; set; }
public bool IsFreeTrial { get; set; }
public SubscriptionStatus Status { get; set; }
public decimal BasePrice { get; set; }
public decimal FinalPrice { get; set; }
public Instant? RenewalAt { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// Gets the human-readable name of the subscription type if available.
/// </summary>
[NotMapped]
public string? DisplayName => SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name)
? name
: null;
public Proto.SubscriptionReferenceObject ToProtoValue() => new()
{
Id = Id.ToString(),
Identifier = Identifier,
BegunAt = BegunAt.ToTimestamp(),
EndedAt = EndedAt?.ToTimestamp(),
IsActive = IsActive,
IsAvailable = IsAvailable,
IsFreeTrial = IsFreeTrial,
Status = (Proto.SubscriptionStatus)Status,
BasePrice = BasePrice.ToString(CultureInfo.CurrentCulture),
FinalPrice = FinalPrice.ToString(CultureInfo.CurrentCulture),
RenewalAt = RenewalAt?.ToTimestamp(),
AccountId = AccountId.ToString(),
DisplayName = DisplayName,
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
public static SubscriptionReferenceObject FromProtoValue(Proto.SubscriptionReferenceObject proto) => new()
{
Id = Guid.Parse(proto.Id),
Identifier = proto.Identifier,
BegunAt = proto.BegunAt.ToInstant(),
EndedAt = proto.EndedAt?.ToInstant(),
IsActive = proto.IsActive,
IsAvailable = proto.IsAvailable,
IsFreeTrial = proto.IsFreeTrial,
Status = (SubscriptionStatus)proto.Status,
BasePrice = decimal.Parse(proto.BasePrice),
FinalPrice = decimal.Parse(proto.FinalPrice),
RenewalAt = proto.RenewalAt?.ToInstant(),
AccountId = Guid.Parse(proto.AccountId),
CreatedAt = proto.CreatedAt.ToInstant(),
UpdatedAt = proto.UpdatedAt.ToInstant()
};
}
public class PaymentDetails
{
public string Currency { get; set; } = null!;
public string? OrderId { get; set; }
public Proto.PaymentDetails ToProtoValue() => new()
{
Currency = Currency,
OrderId = OrderId,
};
public static PaymentDetails FromProtoValue(Proto.PaymentDetails proto) => new()
{
Currency = proto.Currency,
OrderId = proto.OrderId,
};
}
/// <summary>
/// A discount that can applies in purchases among the Solar Network.
/// For now, it can be used in the subscription purchase.
/// </summary>
public class Coupon : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// The items that can apply this coupon.
/// Leave it to null to apply to all items.
/// </summary>
[MaxLength(4096)]
public string? Identifier { get; set; }
/// <summary>
/// The code that human-readable and memorizable.
/// Leave it blank to use it only with the ID.
/// </summary>
[MaxLength(1024)]
public string? Code { get; set; }
public Instant? AffectedAt { get; set; }
public Instant? ExpiredAt { get; set; }
/// <summary>
/// The amount of the discount.
/// If this field and the rate field are both not null,
/// the amount discount will be applied and the discount rate will be ignored.
/// Formula: <code>final price = base price - discount amount</code>
/// </summary>
public decimal? DiscountAmount { get; set; }
/// <summary>
/// The percentage of the discount.
/// If this field and the amount field are both not null,
/// this field will be ignored.
/// Formula: <code>final price = base price * (1 - discount rate)</code>
/// </summary>
public double? DiscountRate { get; set; }
/// <summary>
/// The max usage of the current coupon.
/// Leave it to null to use it unlimited.
/// </summary>
public int? MaxUsage { get; set; }
public Proto.Coupon ToProtoValue() => new()
{
Id = Id.ToString(),
Identifier = Identifier,
Code = Code,
AffectedAt = AffectedAt?.ToTimestamp(),
ExpiredAt = ExpiredAt?.ToTimestamp(),
DiscountAmount = DiscountAmount?.ToString(),
DiscountRate = DiscountRate,
MaxUsage = MaxUsage,
CreatedAt = CreatedAt.ToTimestamp(),
UpdatedAt = UpdatedAt.ToTimestamp()
};
public static Coupon FromProtoValue(Proto.Coupon proto) => new()
{
Id = Guid.Parse(proto.Id),
Identifier = proto.Identifier,
Code = proto.Code,
AffectedAt = proto.AffectedAt?.ToInstant(),
ExpiredAt = proto.ExpiredAt?.ToInstant(),
DiscountAmount = proto.HasDiscountAmount ? decimal.Parse(proto.DiscountAmount) : null,
DiscountRate = proto.DiscountRate,
MaxUsage = proto.MaxUsage,
CreatedAt = proto.CreatedAt.ToInstant(),
UpdatedAt = proto.UpdatedAt.ToInstant()
};
}

View File

@@ -0,0 +1,72 @@
using System.ComponentModel.DataAnnotations;
namespace DysonNetwork.Shared.Models;
/// <summary>
/// The verification info of a resource
/// stands, for it is really an individual or organization or a company in the real world.
/// Besides, it can also be use for mark parody or fake.
/// </summary>
public class SnVerificationMark
{
public VerificationMarkType Type { get; set; }
[MaxLength(1024)] public string? Title { get; set; }
[MaxLength(8192)] public string? Description { get; set; }
[MaxLength(1024)] public string? VerifiedBy { get; set; }
public Proto.VerificationMark ToProtoValue()
{
var proto = new Proto.VerificationMark
{
Type = Type switch
{
VerificationMarkType.Official => Proto.VerificationMarkType.Official,
VerificationMarkType.Individual => Proto.VerificationMarkType.Individual,
VerificationMarkType.Organization => Proto.VerificationMarkType.Organization,
VerificationMarkType.Government => Proto.VerificationMarkType.Government,
VerificationMarkType.Creator => Proto.VerificationMarkType.Creator,
VerificationMarkType.Developer => Proto.VerificationMarkType.Developer,
VerificationMarkType.Parody => Proto.VerificationMarkType.Parody,
_ => Proto.VerificationMarkType.Unspecified
},
Title = Title ?? string.Empty,
Description = Description ?? string.Empty,
VerifiedBy = VerifiedBy ?? string.Empty
};
return proto;
}
public static SnVerificationMark FromProtoValue(Proto.VerificationMark proto)
{
return new SnVerificationMark
{
Type = proto.Type switch
{
Proto.VerificationMarkType.Official => VerificationMarkType.Official,
Proto.VerificationMarkType.Individual => VerificationMarkType.Individual,
Proto.VerificationMarkType.Organization => VerificationMarkType.Organization,
Proto.VerificationMarkType.Government => VerificationMarkType.Government,
Proto.VerificationMarkType.Creator => VerificationMarkType.Creator,
Proto.VerificationMarkType.Developer => VerificationMarkType.Developer,
Proto.VerificationMarkType.Parody => VerificationMarkType.Parody,
_ => VerificationMarkType.Individual
},
Title = proto.Title,
Description = proto.Description,
VerifiedBy = proto.VerifiedBy
};
}
}
public enum VerificationMarkType
{
Official,
Individual,
Organization,
Government,
Creator,
Developer,
Parody
}

View File

@@ -0,0 +1,64 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.Json.Serialization;
namespace DysonNetwork.Shared.Models;
public class SnWallet : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public ICollection<SnWalletPocket> Pockets { get; set; } = new List<SnWalletPocket>();
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
public Proto.Wallet ToProtoValue()
{
var proto = new Proto.Wallet
{
Id = Id.ToString(),
AccountId = AccountId.ToString(),
};
foreach (var pocket in Pockets)
{
proto.Pockets.Add(pocket.ToProtoValue());
}
return proto;
}
public static SnWallet FromProtoValue(Proto.Wallet proto) => new()
{
Id = Guid.Parse(proto.Id),
AccountId = Guid.Parse(proto.AccountId),
Pockets = [.. proto.Pockets.Select(SnWalletPocket.FromProtoValue)],
};
}
public class SnWalletPocket : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(128)] public string Currency { get; set; } = null!;
public decimal Amount { get; set; }
public Guid WalletId { get; set; }
[JsonIgnore] public SnWallet Wallet { get; set; } = null!;
public Proto.WalletPocket ToProtoValue() => new()
{
Id = Id.ToString(),
Currency = Currency,
Amount = Amount.ToString(CultureInfo.CurrentCulture),
WalletId = WalletId.ToString(),
};
public static SnWalletPocket FromProtoValue(Proto.WalletPocket proto) => new()
{
Id = Guid.Parse(proto.Id),
Currency = proto.Currency,
Amount = decimal.Parse(proto.Amount),
WalletId = Guid.Parse(proto.WalletId),
};
}

View File

@@ -0,0 +1,13 @@
namespace DysonNetwork.Shared.Models;
public abstract class WebSocketPacketType
{
public const string Ping = "ping";
public const string Pong = "pong";
public const string Error = "error";
public const string MessageNew = "messages.new";
public const string MessageUpdate = "messages.update";
public const string MessageDelete = "messages.delete";
public const string CallParticipantsUpdate = "call.participants.update";
}

View File

@@ -0,0 +1,77 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
namespace DysonNetwork.Shared.Models;
public class WebSocketPacket
{
public string Type { get; set; } = null!;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Data { get; set; } = null!;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Endpoint { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ErrorMessage { get; set; }
/// <summary>
/// Creates a WebSocketPacket from raw WebSocket message bytes
/// </summary>
/// <param name="bytes">Raw WebSocket message bytes</param>
/// <returns>Deserialized WebSocketPacket</returns>
public static WebSocketPacket FromBytes(byte[] bytes)
{
var json = System.Text.Encoding.UTF8.GetString(bytes);
return JsonSerializer.Deserialize<WebSocketPacket>(json, GrpcTypeHelper.SerializerOptions) ??
throw new JsonException("Failed to deserialize WebSocketPacket");
}
/// <summary>
/// Deserializes the Data property to the specified type T
/// </summary>
/// <typeparam name="T">Target type to deserialize to</typeparam>
/// <returns>Deserialized data of type T</returns>
public T? GetData<T>()
{
if (Data is T typedData)
return typedData;
return JsonSerializer.Deserialize<T>(
JsonSerializer.Serialize(Data, GrpcTypeHelper.SerializerOptions),
GrpcTypeHelper.SerializerOptions
);
}
/// <summary>
/// Serializes this WebSocketPacket to a byte array for sending over WebSocket
/// </summary>
/// <returns>Byte array representation of the packet</returns>
public byte[] ToBytes()
{
var json = JsonSerializer.Serialize(this, GrpcTypeHelper.SerializerOptions);
return System.Text.Encoding.UTF8.GetBytes(json);
}
public Proto.WebSocketPacket ToProtoValue()
{
return new Proto.WebSocketPacket
{
Type = Type,
Data = GrpcTypeHelper.ConvertObjectToByteString(Data),
ErrorMessage = ErrorMessage
};
}
public static WebSocketPacket FromProtoValue(Proto.WebSocketPacket packet)
{
return new WebSocketPacket
{
Type = packet.Type,
Data = GrpcTypeHelper.ConvertByteStringToObject<object?>(packet.Data),
ErrorMessage = packet.ErrorMessage
};
}
}