Files
Swarm/DysonNetwork.Shared/Models/Post.cs

722 lines
22 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using Google.Protobuf.WellKnownTypes;
using NodaTime;
namespace DysonNetwork.Shared.Models;
public enum PostType
{
Moment,
Article,
}
public enum PostVisibility
{
Public,
Friends,
Unlisted,
Private,
}
public enum PostContentType
{
Markdown,
Html,
}
public enum PostPinMode
{
PublisherPage,
RealmPage,
ReplyPage,
}
public class ContentMention
{
[MaxLength(256)]
public string? Username { get; set; }
[MaxLength(2048)]
public string? Url { get; set; }
[MaxLength(2048)]
public string? ActorUri { get; set; }
}
public class ContentTag
{
[MaxLength(256)]
public string? Name { get; set; }
[MaxLength(2048)]
public string? Url { get; set; }
}
public class ContentEmoji
{
[MaxLength(64)]
public string? Shortcode { get; set; }
[MaxLength(2048)]
public string? StaticUrl { get; set; }
[MaxLength(2048)]
public string? Url { get; set; }
}
public class SnPost : ModelBase, IIdentifiedResource, ITimelineEvent
{
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 PostContentType ContentType { get; set; } = PostContentType.Markdown;
public PostType Type { get; set; }
public PostPinMode? PinMode { get; set; }
[Column(TypeName = "jsonb")]
public Dictionary<string, object>? Metadata { get; set; }
[Column(TypeName = "jsonb")]
public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = [];
[Column(TypeName = "jsonb")]
public PostEmbedView? EmbedView { get; set; }
[MaxLength(8192)] public string? FediverseUri { get; set; }
public FediverseContentType? FediverseType { get; set; }
[MaxLength(2048)] public string? Language { get; set; }
[Column(TypeName = "jsonb")]
public List<ContentMention>? Mentions { get; set; }
public int BoostCount { get; set; }
public int LikeCount { get; set; }
public Guid? ActorId { get; set; }
public SnFediverseActor? Actor { 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; }
public int RepliesCount { get; set; }
[NotMapped]
public Dictionary<string, int> ReactionsCount { get; set; } = new();
[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; }
[NotMapped]
public SnRealm? Realm { get; set; }
[Column(TypeName = "jsonb")]
public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
public Guid? PublisherId { get; set; }
public SnPublisher? Publisher { get; set; }
public List<SnPostAward> Awards { get; set; } = [];
[JsonIgnore]
public List<SnPostReaction> Reactions { get; set; } = [];
public List<SnPostTag> Tags { get; set; } = [];
public List<SnPostCategory> Categories { get; set; } = [];
[JsonIgnore]
public List<SnPostCollection> Collections { get; set; } = [];
public List<SnPostFeaturedRecord> FeaturedRecords { get; set; } = [];
[JsonIgnore]
public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null;
[NotMapped]
public bool IsTruncated { get; set; } = false;
public string ResourceIdentifier => $"post:{Id}";
public Post ToProtoValue()
{
var proto = new Post
{
Id = Id.ToString(),
Title = Title ?? string.Empty,
Description = Description ?? string.Empty,
Slug = Slug ?? string.Empty,
Visibility = (Proto.PostVisibility)((int)Visibility + 1),
Type = (Proto.PostType)((int)Type + 1),
ViewsUnique = ViewsUnique,
ViewsTotal = ViewsTotal,
Upvotes = Upvotes,
Downvotes = Downvotes,
AwardedScore = (double)AwardedScore,
ReactionsCount = { ReactionsCount },
RepliesCount = RepliesCount,
ReactionsMade = { ReactionsMade ?? new Dictionary<string, bool>() },
RepliedGone = RepliedGone,
ForwardedGone = ForwardedGone,
PublisherId = PublisherId.ToString(),
Publisher = Publisher?.ToProtoValue(),
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
};
if (EditedAt.HasValue)
proto.EditedAt = Timestamp.FromDateTimeOffset(EditedAt.Value.ToDateTimeOffset());
if (PublishedAt.HasValue)
proto.PublishedAt = Timestamp.FromDateTimeOffset(PublishedAt.Value.ToDateTimeOffset());
if (Content != null)
proto.Content = Content;
proto.ContentType = (Proto.PostContentType)((int)ContentType + 1);
if (PinMode.HasValue)
proto.PinMode = (Proto.PostPinMode)((int)PinMode.Value + 1);
if (Metadata != null)
proto.Meta = GrpcTypeHelper.ConvertObjectToByteString(Metadata);
if (SensitiveMarks != null)
proto.SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks);
if (EmbedView != null)
proto.EmbedView = EmbedView.ToProtoValue();
if (!string.IsNullOrEmpty(FediverseUri))
proto.FediverseUri = FediverseUri;
if (FediverseType.HasValue)
proto.FediverseType = (Proto.FediverseContentType)((int)FediverseType.Value + 1);
if (!string.IsNullOrEmpty(Language))
proto.Language = Language;
if (Mentions != null)
proto.Mentions.AddRange(Mentions.Select(m => new Proto.ContentMention
{
Username = m.Username,
Url = m.Url,
ActorUri = m.ActorUri
}));
proto.RepliesCount = RepliesCount;
proto.BoostCount = BoostCount;
proto.LikeCount = LikeCount;
if (ActorId.HasValue)
proto.ActorId = ActorId.Value.ToString();
if (RepliedPostId.HasValue)
{
proto.RepliedPostId = RepliedPostId.Value.ToString();
if (RepliedPost != null)
{
proto.RepliedPost = RepliedPost.ToProtoValue();
}
}
if (ForwardedPostId.HasValue)
{
proto.ForwardedPostId = ForwardedPostId.Value.ToString();
if (ForwardedPost != null)
{
proto.ForwardedPost = ForwardedPost.ToProtoValue();
}
}
if (RealmId.HasValue)
{
proto.RealmId = RealmId.Value.ToString();
if (Realm != null)
{
proto.Realm = Realm.ToProtoValue();
}
}
proto.Attachments.AddRange(Attachments.Select(a => a.ToProtoValue()));
proto.Awards.AddRange(Awards.Select(a => a.ToProtoValue()));
proto.Reactions.AddRange(Reactions.Select(r => r.ToProtoValue()));
proto.Tags.AddRange(Tags.Select(t => t.ToProtoValue()));
proto.Categories.AddRange(Categories.Select(c => c.ToProtoValue()));
proto.FeaturedRecords.AddRange(FeaturedRecords.Select(f => f.ToProtoValue()));
if (DeletedAt.HasValue)
proto.DeletedAt = Timestamp.FromDateTimeOffset(DeletedAt.Value.ToDateTimeOffset());
return proto;
}
public static SnPost FromProtoValue(Post proto)
{
var post = new SnPost
{
Id = Guid.Parse(proto.Id),
Title = string.IsNullOrEmpty(proto.Title) ? null : proto.Title,
Description = string.IsNullOrEmpty(proto.Description) ? null : proto.Description,
Slug = string.IsNullOrEmpty(proto.Slug) ? null : proto.Slug,
Visibility = (PostVisibility)((int)proto.Visibility - 1),
Type = (PostType)((int)proto.Type - 1),
ViewsUnique = proto.ViewsUnique,
ViewsTotal = proto.ViewsTotal,
Upvotes = proto.Upvotes,
Downvotes = proto.Downvotes,
AwardedScore = (decimal)proto.AwardedScore,
ReactionsCount = proto.ReactionsCount.ToDictionary(kv => kv.Key, kv => kv.Value),
RepliesCount = proto.RepliesCount,
ReactionsMade = proto.ReactionsMade.ToDictionary(kv => kv.Key, kv => kv.Value),
RepliedGone = proto.RepliedGone,
ForwardedGone = proto.ForwardedGone,
PublisherId = Guid.Parse(proto.PublisherId),
Publisher = proto.Publisher != null ? SnPublisher.FromProtoValue(proto.Publisher) : null,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
};
if (proto.EditedAt is not null)
post.EditedAt = Instant.FromDateTimeOffset(proto.EditedAt.ToDateTimeOffset());
if (proto.PublishedAt is not null)
post.PublishedAt = Instant.FromDateTimeOffset(proto.PublishedAt.ToDateTimeOffset());
if (!string.IsNullOrEmpty(proto.Content))
post.Content = proto.Content;
post.ContentType = (PostContentType)((int)proto.ContentType - 1);
if (proto is { HasPinMode: true, PinMode: > 0 })
post.PinMode = (PostPinMode)(proto.PinMode - 1);
if (proto.Meta != null)
post.Metadata = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(
proto.Meta
);
if (proto.SensitiveMarks != null)
post.SensitiveMarks = GrpcTypeHelper.ConvertByteStringToObject<
List<ContentSensitiveMark>
>(proto.SensitiveMarks);
if (proto.EmbedView is not null)
post.EmbedView = PostEmbedView.FromProtoValue(proto.EmbedView);
if (!string.IsNullOrEmpty(proto.FediverseUri))
post.FediverseUri = proto.FediverseUri;
if (proto.HasFediverseType && proto.FediverseType > 0)
post.FediverseType = (FediverseContentType)((int)proto.FediverseType - 1);
if (!string.IsNullOrEmpty(proto.Language))
post.Language = proto.Language;
if (proto.Mentions != null && proto.Mentions.Count > 0)
post.Mentions = proto.Mentions.Select(m => new ContentMention
{
Username = m.Username,
Url = m.Url,
ActorUri = m.ActorUri
}).ToList();
post.RepliesCount = proto.RepliesCount;
post.BoostCount = proto.BoostCount;
post.LikeCount = proto.LikeCount;
if (!string.IsNullOrEmpty(proto.ActorId))
post.ActorId = Guid.Parse(proto.ActorId);
if (!string.IsNullOrEmpty(proto.RepliedPostId))
{
post.RepliedPostId = Guid.Parse(proto.RepliedPostId);
if (proto.RepliedPost is not null)
post.RepliedPost = FromProtoValue(proto.RepliedPost);
}
if (!string.IsNullOrEmpty(proto.ForwardedPostId))
{
post.ForwardedPostId = Guid.Parse(proto.ForwardedPostId);
if (proto.ForwardedPost is not null)
post.ForwardedPost = FromProtoValue(proto.ForwardedPost);
}
if (!string.IsNullOrEmpty(proto.RealmId))
{
post.RealmId = Guid.Parse(proto.RealmId);
if (proto.Realm is not null)
post.Realm = SnRealm.FromProtoValue(proto.Realm);
}
post.Attachments.AddRange(
proto.Attachments.Select(SnCloudFileReferenceObject.FromProtoValue)
);
post.Awards.AddRange(
proto.Awards.Select(a => new SnPostAward
{
Id = Guid.Parse(a.Id),
PostId = Guid.Parse(a.PostId),
AccountId = Guid.Parse(a.AccountId),
Amount = (decimal)a.Amount,
Attitude = (PostReactionAttitude)((int)a.Attitude - 1),
Message = string.IsNullOrEmpty(a.Message) ? null : a.Message,
CreatedAt = Instant.FromDateTimeOffset(a.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(a.UpdatedAt.ToDateTimeOffset()),
})
);
post.Reactions.AddRange(proto.Reactions.Select(SnPostReaction.FromProtoValue));
post.Tags.AddRange(proto.Tags.Select(SnPostTag.FromProtoValue));
post.Categories.AddRange(proto.Categories.Select(SnPostCategory.FromProtoValue));
post.FeaturedRecords.AddRange(
proto.FeaturedRecords.Select(SnPostFeaturedRecord.FromProtoValue)
);
if (proto.DeletedAt is not null)
post.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset());
return post;
}
public SnTimelineEvent ToActivity()
{
return new SnTimelineEvent()
{
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 List<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped]
public int? Usage { get; set; }
public PostTag ToProtoValue()
{
return new PostTag
{
Id = Id.ToString(),
Slug = Slug,
Name = Name ?? string.Empty,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
};
}
public static SnPostTag FromProtoValue(PostTag proto)
{
return new SnPostTag
{
Id = Guid.Parse(proto.Id),
Slug = proto.Slug,
Name = proto.Name != string.Empty ? proto.Name : null,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
};
}
}
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 List<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped]
public int? Usage { get; set; }
public PostCategory ToProtoValue()
{
return new PostCategory
{
Id = Id.ToString(),
Slug = Slug,
Name = Name ?? string.Empty,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
};
}
public static SnPostCategory FromProtoValue(PostCategory proto)
{
return new SnPostCategory
{
Id = Guid.Parse(proto.Id),
Slug = proto.Slug,
Name = proto.Name != string.Empty ? proto.Name : null,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
};
}
}
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 List<SnPost> Posts { get; set; } = new List<SnPost>();
}
public class SnPostFeaturedRecord : ModelBase
{
public Guid Id { get; set; }
public Guid PostId { get; set; }
[JsonIgnore]
public SnPost Post { get; set; } = null!;
public Instant? FeaturedAt { get; set; }
public int SocialCredits { get; set; }
public PostFeaturedRecord ToProtoValue()
{
var proto = new PostFeaturedRecord
{
Id = Id.ToString(),
PostId = PostId.ToString(),
SocialCredits = SocialCredits,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
};
if (FeaturedAt.HasValue)
{
proto.FeaturedAt = Timestamp.FromDateTimeOffset(FeaturedAt.Value.ToDateTimeOffset());
}
return proto;
}
public static SnPostFeaturedRecord FromProtoValue(PostFeaturedRecord proto)
{
return new SnPostFeaturedRecord
{
Id = Guid.Parse(proto.Id),
PostId = Guid.Parse(proto.PostId),
SocialCredits = proto.SocialCredits,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
FeaturedAt =
proto.FeaturedAt != null
? Instant.FromDateTimeOffset(proto.FeaturedAt.ToDateTimeOffset())
: null,
};
}
}
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; }
[NotMapped] public SnAccount? Account { get; set; }
[MaxLength(2048)] public string? FediverseUri { get; set; }
public Guid? ActorId { get; set; }
public SnFediverseActor? Actor { get; set; }
public bool IsLocal { get; set; } = true;
public PostReaction ToProtoValue()
{
var proto = new PostReaction
{
Id = Id.ToString(),
Symbol = Symbol,
Attitude = (Proto.PostReactionAttitude)((int)Attitude + 1),
PostId = PostId.ToString(),
AccountId = AccountId?.ToString() ?? string.Empty,
FediverseUri = FediverseUri ?? string.Empty,
IsLocal = IsLocal,
ActorId = ActorId?.ToString() ?? string.Empty,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
};
if (Account != null)
{
proto.Account = Account.ToProtoValue();
}
return proto;
}
public static SnPostReaction FromProtoValue(PostReaction proto)
{
return new SnPostReaction
{
Id = Guid.Parse(proto.Id),
Symbol = proto.Symbol,
Attitude = (PostReactionAttitude)((int)proto.Attitude - 1),
PostId = Guid.Parse(proto.PostId),
AccountId = !string.IsNullOrEmpty(proto.AccountId) ? Guid.Parse(proto.AccountId) : null,
Account = proto.Account != null ? SnAccount.FromProtoValue(proto.Account) : null,
FediverseUri = !string.IsNullOrEmpty(proto.FediverseUri) ? proto.FediverseUri : null,
ActorId = !string.IsNullOrEmpty(proto.ActorId) ? Guid.Parse(proto.ActorId) : null,
IsLocal = proto.IsLocal,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
};
}
}
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; }
public PostAward ToProtoValue()
{
var proto = new PostAward
{
Id = Id.ToString(),
Amount = (double)Amount,
Attitude = (Proto.PostReactionAttitude)((int)Attitude + 1),
PostId = PostId.ToString(),
AccountId = AccountId.ToString(),
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
};
if (Message != null)
proto.Message = Message;
return proto;
}
}
/// <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 Proto.PostEmbedView ToProtoValue()
{
var proto = new Proto.PostEmbedView
{
Uri = Uri,
Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer,
};
if (AspectRatio.HasValue)
{
proto.AspectRatio = AspectRatio.Value;
}
return proto;
}
public static PostEmbedView FromProtoValue(Proto.PostEmbedView proto)
{
return new PostEmbedView
{
Uri = proto.Uri,
AspectRatio = proto.HasAspectRatio ? proto.AspectRatio : null,
Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1),
};
}
}
public enum PostEmbedViewRenderer
{
WebView,
}