♻️ Move most of models to the Shared package

This commit is contained in:
2025-07-06 22:34:52 +08:00
parent cb4acbb3fc
commit 65450e8511
170 changed files with 679 additions and 101121 deletions

View File

@ -0,0 +1,193 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using OtpNet;
namespace DysonNetwork.Shared.Models;
[Index(nameof(Name), IsUnique = true)]
public class Account : ModelBase
{
public Guid Id { get; set; }
[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;
public Instant? ActivatedAt { get; set; }
public bool IsSuperuser { get; set; } = false;
public Profile Profile { get; set; } = null!;
public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>();
public ICollection<Badge> Badges { get; set; } = new List<Badge>();
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>();
[JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>();
[JsonIgnore] public ICollection<Session> Sessions { get; set; } = new List<Session>();
[JsonIgnore] public ICollection<Challenge> Challenges { get; set; } = new List<Challenge>();
[JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
[JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>();
[JsonIgnore] public ICollection<Subscription> Subscriptions { get; set; } = new List<Subscription>();
}
public abstract class Leveling
{
public static readonly List<int> ExperiencePerLevel =
[
0, // Level 0
100, // Level 1
250, // Level 2
500, // Level 3
1000, // Level 4
2000, // Level 5
4000, // Level 6
8000, // Level 7
16000, // Level 8
32000, // Level 9
64000, // Level 10
128000, // Level 11
256000, // Level 12
512000, // Level 13
1024000 // Level 14
];
}
public class Profile : ModelBase
{
public Guid Id { get; set; }
[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; }
public Instant? Birthday { get; set; }
public Instant? LastSeenAt { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public BadgeReferenceObject? ActiveBadge { get; set; }
[Column(TypeName = "jsonb")] public SubscriptionReferenceObject? StellarMembership { get; set; }
public int Experience { get; set; } = 0;
[NotMapped] public int Level => Leveling.ExperiencePerLevel.Count(xp => Experience >= xp) - 1;
[NotMapped]
public double LevelingProgress => Level >= Leveling.ExperiencePerLevel.Count - 1
? 100
: (Experience - Leveling.ExperiencePerLevel[Level]) * 100.0 /
(Leveling.ExperiencePerLevel[Level + 1] - Leveling.ExperiencePerLevel[Level]);
// Outdated fields, for backward compability
[MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
}
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;
[MaxLength(1024)] public string Content { get; set; } = string.Empty;
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
}
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; } = new();
/// <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 Account 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; } = new();
[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 Account Account { get; set; } = null!;
}

View File

@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum StatusAttitude
{
Positive,
Negative,
Neutral
}
public class Status : 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; }
public Instant? ClearedAt { get; set; }
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
}
public enum CheckInResultLevel
{
Worst,
Worse,
Normal,
Better,
Best
}
public class CheckInResult : 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<FortuneTip> Tips { get; set; } = new List<FortuneTip>();
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
}
public class FortuneTip
{
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 CheckInResult? CheckInResult { get; set; }
public ICollection<Status> Statuses { get; set; } = new List<Status>();
}

View File

@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Point = NetTopologySuite.Geometries.Point;
namespace DysonNetwork.Shared.Models;
public abstract class ActionLogType
{
public const string NewLogin = "login";
public const string ChallengeAttempt = "challenges.attempt";
public const string ChallengeSuccess = "challenges.success";
public const string ChallengeFailure = "challenges.failure";
public const string PostCreate = "posts.create";
public const string PostUpdate = "posts.update";
public const string PostDelete = "posts.delete";
public const string PostReact = "posts.react";
public const string MessageCreate = "messages.create";
public const string MessageUpdate = "messages.update";
public const string MessageDelete = "messages.delete";
public const string MessageReact = "messages.react";
public const string PublisherCreate = "publishers.create";
public const string PublisherUpdate = "publishers.update";
public const string PublisherDelete = "publishers.delete";
public const string PublisherMemberInvite = "publishers.members.invite";
public const string PublisherMemberJoin = "publishers.members.join";
public const string PublisherMemberLeave = "publishers.members.leave";
public const string PublisherMemberKick = "publishers.members.kick";
public const string RealmCreate = "realms.create";
public const string RealmUpdate = "realms.update";
public const string RealmDelete = "realms.delete";
public const string RealmInvite = "realms.invite";
public const string RealmJoin = "realms.join";
public const string RealmLeave = "realms.leave";
public const string RealmKick = "realms.kick";
public const string RealmAdjustRole = "realms.role.edit";
public const string ChatroomCreate = "chatrooms.create";
public const string ChatroomUpdate = "chatrooms.update";
public const string ChatroomDelete = "chatrooms.delete";
public const string ChatroomInvite = "chatrooms.invite";
public const string ChatroomJoin = "chatrooms.join";
public const string ChatroomLeave = "chatrooms.leave";
public const string ChatroomKick = "chatrooms.kick";
public const string ChatroomAdjustRole = "chatrooms.role.edit";
}
public class ActionLog : 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; }
public Point? Location { get; set; }
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
public Guid? SessionId { get; set; }
}

View File

@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class Badge : 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 Shared.Models.Account Account { get; set; } = null!;
public BadgeReferenceObject ToReference()
{
return new BadgeReferenceObject
{
Id = Id,
Type = Type,
Label = Label,
Caption = Caption,
Meta = Meta,
ActivatedAt = ActivatedAt,
ExpiredAt = ExpiredAt,
AccountId = AccountId
};
}
}
public class BadgeReferenceObject : 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; }
public Instant? ActivatedAt { get; set; }
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
}

View File

@ -0,0 +1,143 @@
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 ChatRoom : 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 CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>();
public Guid? RealmId { get; set; }
public Shared.Models.Realm? 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 ChatMember : ModelBase
{
public Guid Id { get; set; }
public Guid ChatRoomId { get; set; }
public ChatRoom ChatRoom { get; set; } = null!;
public Guid AccountId { get; set; }
public Shared.Models.Account 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? 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; }
public Shared.Models.Account 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(ChatMember 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,183 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class RemoteStorageConfig
{
public string Id { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
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; }
}
/// <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; }
}
/// <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 CloudFileReferenceObject : 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 string? MimeType { get; set; }
public string? Hash { get; set; }
public long Size { get; set; }
public bool HasCompression { get; set; } = false;
}
public class CloudFile : 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();
[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; } = null!;
[Column(TypeName = "jsonb")] public Dictionary<string, object>? UserMeta { get; set; } = null!;
[Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = [];
[MaxLength(256)] public string? MimeType { get; set; }
[MaxLength(256)] public string? Hash { get; set; }
public long Size { get; set; }
public Instant? UploadedAt { get; set; }
[MaxLength(128)] public string? UploadedTo { get; set; }
public bool HasCompression { get; set; } = false;
/// <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; }
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
public Guid AccountId { get; set; }
public CloudFileReferenceObject ToReferenceObject()
{
return new CloudFileReferenceObject
{
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
DeletedAt = DeletedAt,
Id = Id,
Name = Name,
FileMeta = FileMeta,
UserMeta = UserMeta,
MimeType = MimeType,
Hash = Hash,
Size = Size,
HasCompression = HasCompression
};
}
public string ResourceIdentifier => $"file/{Id}";
}
public enum ContentSensitiveMark
{
Language,
SexualContent,
Violence,
Profanity,
HateSpeech,
Racism,
AdultContent,
DrugAbuse,
AlcoholAbuse,
Gambling,
SelfHarm,
ChildAbuse,
Other
}
public class CloudFileReference : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(32)] public string FileId { get; set; } = null!;
public CloudFile 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; }
}

View File

@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum CustomAppStatus
{
Developing,
Staging,
Production,
Suspended
}
public class CustomApp : 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 CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
public Guid PublisherId { get; set; }
public Publisher Developer { get; set; } = null!;
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
}
public class CustomAppLinks : ModelBase
{
[MaxLength(8192)] public string? HomePage { get; set; }
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
[MaxLength(8192)] public string? TermsOfService { get; set; }
}
public class CustomAppOauthConfig : ModelBase
{
[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 CustomAppSecret : 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 CustomApp App { get; set; } = null!;
}

View File

@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class Message : 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<CloudFileReferenceObject> Attachments { get; set; } = [];
// Outdated fields, keep for backward compability
public ICollection<CloudFile> OutdatedAttachments { get; set; } = new List<CloudFile>();
public ICollection<MessageReaction> Reactions { get; set; } = new List<MessageReaction>();
public Guid? RepliedMessageId { get; set; }
public Message? RepliedMessage { get; set; }
public Guid? ForwardedMessageId { get; set; }
public Message? ForwardedMessage { get; set; }
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
public Guid ChatRoomId { get; set; }
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
public string ResourceIdentifier => $"message/{Id}";
}
public enum MessageReactionAttitude
{
Positive,
Neutral,
Negative,
}
public class MessageReaction : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid MessageId { get; set; }
[JsonIgnore] public Message Message { get; set; } = null!;
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
[MaxLength(256)] public string Symbol { get; set; } = null!;
public MessageReactionAttitude Attitude { get; set; }
}
/// <summary>
/// The data model for updating the last read at field for chat member,
/// after the refactor of the unread system, this no longer stored in the database.
/// Not only used for the data transmission object
/// </summary>
[NotMapped]
public class MessageReadReceipt
{
public Guid SenderId { 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,41 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class Notification : 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; }
public int Priority { get; set; } = 10;
public Instant? ViewedAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
}
public enum NotificationPushProvider
{
Apple,
Google
}
[Index(nameof(DeviceToken), nameof(DeviceId), nameof(AccountId), IsUnique = true)]
public class NotificationPushSubscription : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(4096)] public string DeviceId { get; set; } = null!;
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
public NotificationPushProvider Provider { get; set; }
public Instant? LastUsedAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
}

View File

@ -0,0 +1,62 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NodaTime;
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 Order : ModelBase
{
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; }
[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 Shared.Models.Wallet? PayeeWallet { get; set; } = null!;
public Guid? TransactionId { get; set; }
public Transaction? Transaction { get; set; }
public Guid? IssuerAppId { get; set; }
public CustomApp? IssuerApp { get; set; }
}
public enum TransactionType
{
System,
Transfer,
Order
}
public class Transaction : 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 Shared.Models.Wallet? PayerWallet { get; set; }
// When the payee is null, it's pay for the system
public Guid? PayeeWalletId { get; set; }
public Shared.Models.Wallet? PayeeWallet { get; set; }
}

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 PermissionNode : 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 PermissionGroup? Group { get; set; } = null;
public void Dispose()
{
Value.Dispose();
GC.SuppressFinalize(this);
}
}
public class PermissionGroup : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string Key { get; set; } = null!;
public ICollection<PermissionNode> Nodes { get; set; } = new List<PermissionNode>();
[JsonIgnore] public ICollection<PermissionGroupMember> Members { get; set; } = new List<PermissionGroupMember>();
}
public class PermissionGroupMember : ModelBase
{
public Guid GroupId { get; set; }
public PermissionGroup 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,100 @@
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 PublisherType
{
Individual,
Organizational
}
[Index(nameof(Name), IsUnique = true)]
public class Publisher : ModelBase, IIdentifiedResource
{
public Guid Id { get; set; }
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 CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
[JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = new List<PublisherFeature>();
[JsonIgnore]
public ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>();
public Guid? AccountId { get; set; }
public Shared.Models.Account? Account { get; set; }
public Guid? RealmId { get; set; }
[JsonIgnore] public Realm? Realm { get; set; }
public string ResourceIdentifier => $"publisher/{Id}";
}
public enum PublisherMemberRole
{
Owner = 100,
Manager = 75,
Editor = 50,
Viewer = 25
}
public class PublisherMember : ModelBase
{
public Guid PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
public Instant? JoinedAt { get; set; }
}
public enum PublisherSubscriptionStatus
{
Active,
Expired,
Cancelled
}
public class PublisherSubscription : ModelBase
{
public Guid Id { get; set; }
public Guid PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; }
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
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 Publisher Publisher { get; set; } = null!;
}
public abstract class PublisherFeatureFlag
{
public static List<string> AllFlags => [Develop];
public static string Develop = "develop";
}

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 Realm : 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 CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
public Guid AccountId { get; set; }
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
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 RealmMember : ModelBase
{
public Guid RealmId { get; set; }
public Realm Realm { get; set; } = null!;
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
public int Role { get; set; } = RealmMemberRole.Normal;
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
}

View File

@ -0,0 +1,46 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public class RealtimeCall : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Instant? EndedAt { get; set; }
public Guid SenderId { get; set; }
public ChatMember Sender { get; set; } = null!;
public Guid RoomId { get; set; }
public ChatRoom 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,22 @@
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum RelationshipStatus : short
{
Friends = 100,
Pending = 0,
Blocked = -100
}
public class Relationship : ModelBase
{
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
public Guid RelatedId { get; set; }
public Shared.Models.Account Related { get; set; } = null!;
public Instant? ExpiredAt { get; set; }
public RelationshipStatus Status { get; set; } = RelationshipStatus.Pending;
}

View File

@ -0,0 +1,68 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
using Point = NetTopologySuite.Geometries.Point;
namespace DysonNetwork.Shared.Models;
public class Session : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(1024)] public string? Label { get; set; }
public Instant? LastGrantedAt { get; set; }
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
public Guid ChallengeId { get; set; }
public Challenge Challenge { get; set; } = null!;
public Guid? AppId { get; set; }
public CustomApp? App { get; set; }
}
public enum ChallengeType
{
Login,
OAuth, // Trying to authorize other platforms
Oidc // Trying to connect other platforms
}
public enum ChallengePlatform
{
Unidentified,
Web,
Ios,
Android,
MacOs,
Windows,
Linux
}
public class Challenge : 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 ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
public ChallengeType Type { get; set; } = ChallengeType.Login;
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = [];
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
[MaxLength(128)] public string? IpAddress { get; set; }
[MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(256)] public string? DeviceId { get; set; }
[MaxLength(1024)] public string? Nonce { get; set; }
public Point? Location { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
public Challenge Normalize()
{
if (StepRemain == 0 && BlacklistFactors.Count == 0) StepRemain = StepTotal;
return this;
}
}

View File

@ -0,0 +1,256 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using NodaTime;
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 Subscription : 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 Shared.Models.Account 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;
}
}
public SubscriptionReferenceObject ToReference()
{
return new SubscriptionReferenceObject
{
Id = Id,
Status = Status,
Identifier = Identifier,
IsActive = IsActive,
AccountId = AccountId,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
DeletedAt = DeletedAt,
};
}
}
public class PaymentDetails
{
public string Currency { get; set; } = null!;
public string? OrderId { get; set; }
}
public class SubscriptionReferenceObject : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public SubscriptionStatus Status { get; set; }
public string Identifier { get; set; } = null!;
public bool IsActive { get; set; } = true;
public Guid AccountId { get; set; }
}
/// <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; }
}

View File

@ -0,0 +1,25 @@
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 VerificationMark
{
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 enum VerificationMarkType
{
Official,
Individual,
Organization,
Government,
Creator
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace DysonNetwork.Shared.Models;
public class Wallet : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>();
public Guid AccountId { get; set; }
public Shared.Models.Account Account { get; set; } = null!;
}
public class WalletPocket : 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 Wallet Wallet { get; set; } = null!;
}