♻️ Finish centerlizing the data models
This commit is contained in:
@@ -23,18 +23,18 @@ public class SnAccount : ModelBase
|
||||
public Guid? AutomatedId { get; set; }
|
||||
|
||||
public SnAccountProfile Profile { get; set; } = null!;
|
||||
public ICollection<AccountContact> Contacts { get; set; } = [];
|
||||
public ICollection<SnAccountContact> 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<SnAccountAuthFactor> AuthFactors { get; set; } = [];
|
||||
[JsonIgnore] public ICollection<SnAccountConnection> Connections { get; set; } = [];
|
||||
[JsonIgnore] public ICollection<SnAuthSession> Sessions { get; set; } = [];
|
||||
[JsonIgnore] public ICollection<SnAuthChallenge> Challenges { get; set; } = [];
|
||||
|
||||
[JsonIgnore] public ICollection<SnAccountRelationship> OutgoingRelationships { get; set; } = [];
|
||||
[JsonIgnore] public ICollection<SnAccountRelationship> IncomingRelationships { get; set; } = [];
|
||||
|
||||
[NotMapped] public SubscriptionReferenceObject? PerkSubscription { get; set; }
|
||||
[NotMapped] public SnSubscriptionReferenceObject? PerkSubscription { get; set; }
|
||||
|
||||
public Proto.Account ToProtoValue()
|
||||
{
|
||||
@@ -78,7 +78,7 @@ public class SnAccount : ModelBase
|
||||
ActivatedAt = proto.ActivatedAt?.ToInstant(),
|
||||
IsSuperuser = proto.IsSuperuser,
|
||||
PerkSubscription = proto.PerkSubscription is not null
|
||||
? SubscriptionReferenceObject.FromProtoValue(proto.PerkSubscription)
|
||||
? SnSubscriptionReferenceObject.FromProtoValue(proto.PerkSubscription)
|
||||
: null,
|
||||
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||
UpdatedAt = proto.UpdatedAt.ToInstant(),
|
||||
@@ -87,7 +87,7 @@ public class SnAccount : ModelBase
|
||||
};
|
||||
|
||||
foreach (var contactProto in proto.Contacts)
|
||||
account.Contacts.Add(AccountContact.FromProtoValue(contactProto));
|
||||
account.Contacts.Add(SnAccountContact.FromProtoValue(contactProto));
|
||||
|
||||
foreach (var badgeProto in proto.Badges)
|
||||
account.Badges.Add(SnAccountBadge.FromProtoValue(badgeProto));
|
||||
@@ -254,7 +254,7 @@ public class ProfileLink
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AccountContact : ModelBase
|
||||
public class SnAccountContact : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public AccountContactType Type { get; set; }
|
||||
@@ -289,9 +289,9 @@ public class AccountContact : ModelBase
|
||||
return proto;
|
||||
}
|
||||
|
||||
public static AccountContact FromProtoValue(Proto.AccountContact proto)
|
||||
public static SnAccountContact FromProtoValue(Proto.AccountContact proto)
|
||||
{
|
||||
var contact = new AccountContact
|
||||
var contact = new SnAccountContact
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
AccountId = Guid.Parse(proto.AccountId),
|
||||
@@ -320,7 +320,7 @@ public enum AccountContactType
|
||||
Address
|
||||
}
|
||||
|
||||
public class AccountAuthFactor : ModelBase
|
||||
public class SnAccountAuthFactor : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public AccountAuthFactorType Type { get; set; }
|
||||
@@ -343,7 +343,7 @@ public class AccountAuthFactor : ModelBase
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public SnAccount Account { get; set; } = null!;
|
||||
|
||||
public AccountAuthFactor HashSecret(int cost = 12)
|
||||
public SnAccountAuthFactor HashSecret(int cost = 12)
|
||||
{
|
||||
if (Secret == null) return this;
|
||||
Secret = BCrypt.Net.BCrypt.HashPassword(Secret, workFactor: cost);
|
||||
@@ -386,7 +386,7 @@ public enum AccountAuthFactorType
|
||||
PinCode,
|
||||
}
|
||||
|
||||
public class AccountConnection : ModelBase
|
||||
public class SnAccountConnection : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(4096)] public string Provider { get; set; } = null!;
|
||||
|
@@ -40,4 +40,44 @@ public class SnActionLog : ModelBase
|
||||
|
||||
return protoLog;
|
||||
}
|
||||
}
|
||||
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 PostPin = "posts.pin";
|
||||
public const string PostUnpin = "posts.unpin";
|
||||
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";
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
@@ -5,8 +5,6 @@ using Google.Protobuf.WellKnownTypes;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
using NodaTime;
|
||||
|
||||
using SnVerificationMark = DysonNetwork.Shared.Models.SnVerificationMark;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public enum CustomAppStatus
|
||||
@@ -29,8 +27,8 @@ public class SnCustomApp : ModelBase, IIdentifiedResource
|
||||
[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; }
|
||||
[Column(TypeName = "jsonb")] public SnCustomAppOauthConfig? OauthConfig { get; set; }
|
||||
[Column(TypeName = "jsonb")] public SnCustomAppLinks? Links { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<SnCustomAppSecret> Secrets { get; set; } = new List<SnCustomAppSecret>();
|
||||
|
||||
@@ -105,7 +103,7 @@ public class SnCustomApp : ModelBase, IIdentifiedResource
|
||||
if (p.Verification is not null) Verification = SnVerificationMark.FromProtoValue(p.Verification);
|
||||
if (p.Links is not null)
|
||||
{
|
||||
Links = new CustomAppLinks
|
||||
Links = new SnCustomAppLinks
|
||||
{
|
||||
HomePage = string.IsNullOrEmpty(p.Links.HomePage) ? null : p.Links.HomePage,
|
||||
PrivacyPolicy = string.IsNullOrEmpty(p.Links.PrivacyPolicy) ? null : p.Links.PrivacyPolicy,
|
||||
@@ -116,14 +114,14 @@ public class SnCustomApp : ModelBase, IIdentifiedResource
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomAppLinks
|
||||
public class SnCustomAppLinks
|
||||
{
|
||||
[MaxLength(8192)] public string? HomePage { get; set; }
|
||||
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
|
||||
[MaxLength(8192)] public string? TermsOfService { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAppOauthConfig
|
||||
public class SnCustomAppOauthConfig
|
||||
{
|
||||
[MaxLength(1024)] public string? ClientUri { get; set; }
|
||||
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
||||
|
@@ -3,7 +3,7 @@ using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public class ExperienceRecord : ModelBase
|
||||
public class SnExperienceRecord : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string ReasonType { get; set; } = string.Empty;
|
||||
|
@@ -1,7 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
@@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
@@ -32,6 +33,21 @@ public class SnPermissionNode : ModelBase, IDisposable
|
||||
public Guid? GroupId { get; set; } = null;
|
||||
[JsonIgnore] public SnPermissionGroup? Group { get; set; } = null;
|
||||
|
||||
public Proto.PermissionNode ToProtoValue()
|
||||
{
|
||||
return new Proto.PermissionNode
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Actor = Actor,
|
||||
Area = Area,
|
||||
Key = Key,
|
||||
Value = Google.Protobuf.WellKnownTypes.Value.Parser.ParseJson(Value.RootElement.GetRawText()),
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||
AffectedAt = AffectedAt?.ToTimestamp(),
|
||||
GroupId = GroupId?.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Value.Dispose();
|
||||
|
@@ -2,7 +2,6 @@ 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;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -36,18 +35,60 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
||||
[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<SnPublisherFeature> Features { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public ICollection<PublisherSubscription> Subscriptions { get; set; } = [];
|
||||
public ICollection<SnPublisherSubscription> 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; }
|
||||
[NotMapped] public SnAccount? Account { get; set; }
|
||||
|
||||
public string ResourceIdentifier => $"publisher:{Id}";
|
||||
|
||||
public static SnPublisher FromProto(Proto.Publisher proto)
|
||||
{
|
||||
var publisher = new SnPublisher
|
||||
{
|
||||
Id = Guid.TryParse(proto.Id, out var id) ? id : Guid.NewGuid(),
|
||||
Type = proto.Type == Shared.Proto.PublisherType.PubIndividual
|
||||
? PublisherType.Individual
|
||||
: PublisherType.Organizational,
|
||||
Name = proto.Name,
|
||||
Nick = proto.Nick,
|
||||
Bio = proto.Bio,
|
||||
AccountId = Guid.TryParse(proto.AccountId, out var accountId) ? accountId : null,
|
||||
RealmId = Guid.TryParse(proto.RealmId, out var realmId) ? realmId : null,
|
||||
};
|
||||
|
||||
if (proto.Picture != null)
|
||||
{
|
||||
publisher.Picture = new SnCloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Picture.Id,
|
||||
Name = proto.Picture.Name,
|
||||
MimeType = proto.Picture.MimeType,
|
||||
Hash = proto.Picture.Hash,
|
||||
Size = proto.Picture.Size,
|
||||
};
|
||||
}
|
||||
|
||||
if (proto.Background != null)
|
||||
{
|
||||
publisher.Background = new SnCloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Background.Id,
|
||||
Name = proto.Background.Name,
|
||||
MimeType = proto.Background.MimeType,
|
||||
Hash = proto.Background.Hash,
|
||||
Size = proto.Background.Size,
|
||||
};
|
||||
}
|
||||
|
||||
return publisher;
|
||||
}
|
||||
|
||||
public Proto.Publisher ToProto()
|
||||
{
|
||||
var p = new Proto.Publisher()
|
||||
@@ -105,7 +146,7 @@ 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; }
|
||||
[NotMapped] public SnAccount? Account { get; set; }
|
||||
|
||||
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
|
||||
public Instant? JoinedAt { get; set; }
|
||||
@@ -137,7 +178,7 @@ public enum PublisherSubscriptionStatus
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public class PublisherSubscription : ModelBase
|
||||
public class SnPublisherSubscription : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
@@ -149,7 +190,7 @@ public class PublisherSubscription : ModelBase
|
||||
public int Tier { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class PublisherFeature : ModelBase
|
||||
public class SnPublisherFeature : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
[MaxLength(1024)] public string Flag { get; set; } = null!;
|
||||
|
@@ -4,7 +4,7 @@ using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public class SocialCreditRecord : ModelBase
|
||||
public class SnSocialCreditRecord : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
[MaxLength(1024)] public string ReasonType { get; set; } = string.Empty;
|
||||
|
@@ -104,7 +104,7 @@ public enum SubscriptionStatus
|
||||
/// The paid subscription in another word.
|
||||
/// </summary>
|
||||
[Index(nameof(Identifier))]
|
||||
public class SnSubscription : ModelBase
|
||||
public class SnWalletSubscription : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Instant BegunAt { get; set; }
|
||||
@@ -133,10 +133,10 @@ public class SnSubscription : ModelBase
|
||||
public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Unpaid;
|
||||
|
||||
[MaxLength(4096)] public string PaymentMethod { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public PaymentDetails PaymentDetails { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public SnPaymentDetails PaymentDetails { get; set; } = null!;
|
||||
public decimal BasePrice { get; set; }
|
||||
public Guid? CouponId { get; set; }
|
||||
public Coupon? Coupon { get; set; }
|
||||
public SnWalletCoupon? Coupon { get; set; }
|
||||
public Instant? RenewalAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
@@ -182,9 +182,9 @@ public class SnSubscription : ModelBase
|
||||
/// Returns a reference object that contains a subset of subscription data
|
||||
/// suitable for client-side use, with sensitive information removed.
|
||||
/// </summary>
|
||||
public SubscriptionReferenceObject ToReference()
|
||||
public SnSubscriptionReferenceObject ToReference()
|
||||
{
|
||||
return new SubscriptionReferenceObject
|
||||
return new SnSubscriptionReferenceObject
|
||||
{
|
||||
Id = Id,
|
||||
Identifier = Identifier,
|
||||
@@ -223,7 +223,7 @@ public class SnSubscription : ModelBase
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
|
||||
public static SnSubscription FromProtoValue(Proto.Subscription proto) => new()
|
||||
public static SnWalletSubscription FromProtoValue(Proto.Subscription proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
BegunAt = proto.BegunAt.ToInstant(),
|
||||
@@ -233,10 +233,10 @@ public class SnSubscription : ModelBase
|
||||
IsFreeTrial = proto.IsFreeTrial,
|
||||
Status = (SubscriptionStatus)proto.Status,
|
||||
PaymentMethod = proto.PaymentMethod,
|
||||
PaymentDetails = PaymentDetails.FromProtoValue(proto.PaymentDetails),
|
||||
PaymentDetails = SnPaymentDetails.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,
|
||||
Coupon = proto.Coupon is not null ? SnWalletCoupon.FromProtoValue(proto.Coupon) : null,
|
||||
RenewalAt = proto.RenewalAt?.ToInstant(),
|
||||
AccountId = Guid.Parse(proto.AccountId),
|
||||
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||
@@ -248,7 +248,7 @@ public class SnSubscription : ModelBase
|
||||
/// A reference object for Subscription that contains only non-sensitive information
|
||||
/// suitable for client-side use.
|
||||
/// </summary>
|
||||
public class SubscriptionReferenceObject : ModelBase
|
||||
public class SnSubscriptionReferenceObject : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Identifier { get; set; } = null!;
|
||||
@@ -290,7 +290,7 @@ public class SubscriptionReferenceObject : ModelBase
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
|
||||
public static SubscriptionReferenceObject FromProtoValue(Proto.SubscriptionReferenceObject proto) => new()
|
||||
public static SnSubscriptionReferenceObject FromProtoValue(Proto.SubscriptionReferenceObject proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Identifier = proto.Identifier,
|
||||
@@ -309,7 +309,7 @@ public class SubscriptionReferenceObject : ModelBase
|
||||
};
|
||||
}
|
||||
|
||||
public class PaymentDetails
|
||||
public class SnPaymentDetails
|
||||
{
|
||||
public string Currency { get; set; } = null!;
|
||||
public string? OrderId { get; set; }
|
||||
@@ -320,7 +320,7 @@ public class PaymentDetails
|
||||
OrderId = OrderId,
|
||||
};
|
||||
|
||||
public static PaymentDetails FromProtoValue(Proto.PaymentDetails proto) => new()
|
||||
public static SnPaymentDetails FromProtoValue(Proto.PaymentDetails proto) => new()
|
||||
{
|
||||
Currency = proto.Currency,
|
||||
OrderId = proto.OrderId,
|
||||
@@ -331,7 +331,7 @@ public class PaymentDetails
|
||||
/// 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 class SnWalletCoupon : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
@@ -388,7 +388,7 @@ public class Coupon : ModelBase
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
|
||||
public static Coupon FromProtoValue(Proto.Coupon proto) => new()
|
||||
public static SnWalletCoupon FromProtoValue(Proto.Coupon proto) => new()
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Identifier = proto.Identifier,
|
||||
|
Reference in New Issue
Block a user