diff --git a/DysonNetwork.Shared/Models/Activity.cs b/DysonNetwork.Shared/Models/Activity.cs deleted file mode 100644 index 51019d5..0000000 --- a/DysonNetwork.Shared/Models/Activity.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 Meta { get; set; } = new(); - - public object? Data { get; set; } - - public static SnActivity Empty() - { - var now = SystemClock.Instance.GetCurrentInstant(); - return new SnActivity - { - CreatedAt = now, - UpdatedAt = now, - Id = Guid.NewGuid(), - Type = "empty", - ResourceIdentifier = "none" - }; - } -} \ No newline at end of file diff --git a/DysonNetwork.Shared/Models/Post.cs b/DysonNetwork.Shared/Models/Post.cs index 6f7bb30..f0bae12 100644 --- a/DysonNetwork.Shared/Models/Post.cs +++ b/DysonNetwork.Shared/Models/Post.cs @@ -2,16 +2,15 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using DysonNetwork.Shared.Proto; -using NodaTime; -using NpgsqlTypes; using Google.Protobuf.WellKnownTypes; +using NodaTime; namespace DysonNetwork.Shared.Models; public enum PostType { Moment, - Article + Article, } public enum PostVisibility @@ -19,7 +18,7 @@ public enum PostVisibility Public, Friends, Unlisted, - Private + Private, } public enum PostPinMode @@ -29,12 +28,18 @@ public enum PostPinMode ReplyPage, } -public class SnPost : ModelBase, IIdentifiedResource, IActivity +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; } + + [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; @@ -44,18 +49,30 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity public PostType Type { get; set; } public PostPinMode? PinMode { get; set; } - [Column(TypeName = "jsonb")] public Dictionary? Meta { get; set; } - [Column(TypeName = "jsonb")] public List? SensitiveMarks { get; set; } = []; - [Column(TypeName = "jsonb")] public PostEmbedView? EmbedView { get; set; } + + [Column(TypeName = "jsonb")] + public Dictionary? Meta { get; set; } + + [Column(TypeName = "jsonb")] + public List? 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 ReactionsCount { get; set; } = new(); - [NotMapped] public int RepliesCount { get; set; } - [NotMapped] public Dictionary? ReactionsMade { get; set; } + + [NotMapped] + public Dictionary ReactionsCount { get; set; } = new(); + + [NotMapped] + public int RepliesCount { get; set; } + + [NotMapped] + public Dictionary? ReactionsMade { get; set; } public bool RepliedGone { get; set; } public bool ForwardedGone { get; set; } @@ -66,22 +83,32 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity public SnPost? ForwardedPost { get; set; } public Guid? RealmId { get; set; } - [NotMapped] public SnRealm? Realm { get; set; } - [Column(TypeName = "jsonb")] public List Attachments { get; set; } = []; + [NotMapped] + public SnRealm? Realm { get; set; } + + [Column(TypeName = "jsonb")] + public List Attachments { get; set; } = []; public Guid PublisherId { get; set; } public SnPublisher Publisher { get; set; } = null!; public List Awards { get; set; } = []; - [JsonIgnore] public List Reactions { get; set; } = []; + + [JsonIgnore] + public List Reactions { get; set; } = []; public List Tags { get; set; } = []; public List Categories { get; set; } = []; - [JsonIgnore] public List Collections { get; set; } = []; + + [JsonIgnore] + public List Collections { get; set; } = []; public List FeaturedRecords { get; set; } = []; - [JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; - [NotMapped] public bool IsTruncated { get; set; } = false; + [JsonIgnore] + public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; + + [NotMapped] + public bool IsTruncated { get; set; } = false; public string ResourceIdentifier => $"post:{Id}"; @@ -108,7 +135,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity PublisherId = PublisherId.ToString(), Publisher = Publisher.ToProtoValue(), CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), - UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), }; if (EditedAt.HasValue) @@ -195,7 +222,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity PublisherId = Guid.Parse(proto.PublisherId), Publisher = SnPublisher.FromProtoValue(proto.Publisher), CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), - UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), }; if (proto.EditedAt is not null) @@ -211,11 +238,14 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity post.PinMode = (PostPinMode)(proto.PinMode - 1); if (proto.Meta != null) - post.Meta = GrpcTypeHelper.ConvertByteStringToObject>(proto.Meta); + post.Meta = GrpcTypeHelper.ConvertByteStringToObject>( + proto.Meta + ); if (proto.SensitiveMarks != null) - post.SensitiveMarks = - GrpcTypeHelper.ConvertByteStringToObject>(proto.SensitiveMarks); + post.SensitiveMarks = GrpcTypeHelper.ConvertByteStringToObject< + List + >(proto.SensitiveMarks); if (proto.EmbedView is not null) post.EmbedView = PostEmbedView.FromProtoValue(proto.EmbedView); @@ -241,19 +271,28 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity 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.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)); + post.FeaturedRecords.AddRange( + proto.FeaturedRecords.Select(SnPostFeaturedRecord.FromProtoValue) + ); if (proto.DeletedAt is not null) post.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset()); @@ -261,9 +300,9 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity return post; } - public SnActivity ToActivity() + public SnTimelineEvent ToActivity() { - return new SnActivity() + return new SnTimelineEvent() { CreatedAt = PublishedAt ?? CreatedAt, UpdatedAt = UpdatedAt, @@ -271,7 +310,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity Id = Id, Type = RepliedPostId is null ? "posts.new" : "posts.new.replies", ResourceIdentifier = ResourceIdentifier, - Data = this + Data = this, }; } } @@ -279,11 +318,18 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity 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 Posts { get; set; } = new List(); - [NotMapped] public int? Usage { get; set; } + [MaxLength(128)] + public string Slug { get; set; } = null!; + + [MaxLength(256)] + public string? Name { get; set; } + + [JsonIgnore] + public List Posts { get; set; } = new List(); + + [NotMapped] + public int? Usage { get; set; } public PostTag ToProtoValue() { @@ -293,7 +339,7 @@ public class SnPostTag : ModelBase Slug = Slug, Name = Name ?? string.Empty, CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), - UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), }; } @@ -305,7 +351,7 @@ public class SnPostTag : ModelBase Slug = proto.Slug, Name = proto.Name != string.Empty ? proto.Name : null, CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), - UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), }; } } @@ -313,11 +359,18 @@ public class SnPostTag : ModelBase 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 Posts { get; set; } = new List(); - [NotMapped] public int? Usage { get; set; } + [MaxLength(128)] + public string Slug { get; set; } = null!; + + [MaxLength(256)] + public string? Name { get; set; } + + [JsonIgnore] + public List Posts { get; set; } = new List(); + + [NotMapped] + public int? Usage { get; set; } public PostCategory ToProtoValue() { @@ -327,7 +380,7 @@ public class SnPostCategory : ModelBase Slug = Slug, Name = Name ?? string.Empty, CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), - UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), }; } @@ -339,7 +392,7 @@ public class SnPostCategory : ModelBase Slug = proto.Slug, Name = proto.Name != string.Empty ? proto.Name : null, CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), - UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), }; } } @@ -358,9 +411,15 @@ public class SnPostCategorySubscription : ModelBase 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; } + + [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!; @@ -371,7 +430,9 @@ public class SnPostFeaturedRecord : ModelBase { public Guid Id { get; set; } public Guid PostId { get; set; } - [JsonIgnore] public SnPost Post { get; set; } = null!; + + [JsonIgnore] + public SnPost Post { get; set; } = null!; public Instant? FeaturedAt { get; set; } public int SocialCredits { get; set; } @@ -383,7 +444,7 @@ public class SnPostFeaturedRecord : ModelBase PostId = PostId.ToString(), SocialCredits = SocialCredits, CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), - UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), }; if (FeaturedAt.HasValue) { @@ -402,7 +463,10 @@ public class SnPostFeaturedRecord : ModelBase 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 + FeaturedAt = + proto.FeaturedAt != null + ? Instant.FromDateTimeOffset(proto.FeaturedAt.ToDateTimeOffset()) + : null, }; } } @@ -417,13 +481,19 @@ public enum PostReactionAttitude public class SnPostReaction : ModelBase { public Guid Id { get; set; } - [MaxLength(256)] public string Symbol { get; set; } = null!; + + [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!; + + [JsonIgnore] + public SnPost Post { get; set; } = null!; public Guid AccountId { get; set; } - [NotMapped] public SnAccount? Account { get; set; } + + [NotMapped] + public SnAccount? Account { get; set; } public PostReaction ToProtoValue() { @@ -435,7 +505,7 @@ public class SnPostReaction : ModelBase PostId = PostId.ToString(), AccountId = AccountId.ToString(), CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), - UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), }; if (Account != null) { @@ -456,7 +526,7 @@ public class SnPostReaction : ModelBase AccountId = Guid.Parse(proto.AccountId), Account = proto.Account != null ? SnAccount.FromProtoValue(proto.Account) : null, CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), - UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), }; } } @@ -466,10 +536,14 @@ 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; } + + [MaxLength(4096)] + public string? Message { get; set; } public Guid PostId { get; set; } - [JsonIgnore] public SnPost Post { get; set; } = null!; + + [JsonIgnore] + public SnPost Post { get; set; } = null!; public Guid AccountId { get; set; } public PostAward ToProtoValue() @@ -482,7 +556,7 @@ public class SnPostAward : ModelBase PostId = PostId.ToString(), AccountId = AccountId.ToString(), CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), - UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()) + UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), }; if (Message != null) proto.Message = Message; @@ -506,7 +580,7 @@ public class PostEmbedView var proto = new Proto.PostEmbedView { Uri = Uri, - Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer + Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer, }; if (AspectRatio.HasValue) { @@ -522,12 +596,12 @@ public class PostEmbedView { Uri = proto.Uri, AspectRatio = proto.HasAspectRatio ? proto.AspectRatio : null, - Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1) + Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1), }; } } public enum PostEmbedViewRenderer { - WebView + WebView, } diff --git a/DysonNetwork.Shared/Models/Timeline.cs b/DysonNetwork.Shared/Models/Timeline.cs new file mode 100644 index 0000000..38ea3be --- /dev/null +++ b/DysonNetwork.Shared/Models/Timeline.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using NodaTime; + +namespace DysonNetwork.Shared.Models; + +public interface ITimelineEvent +{ + public SnTimelineEvent ToActivity(); +} + +[NotMapped] +public class SnTimelineEvent : 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 Meta { get; set; } = new(); + + public object? Data { get; set; } + + public static SnTimelineEvent Empty() + { + var now = SystemClock.Instance.GetCurrentInstant(); + return new SnTimelineEvent + { + CreatedAt = now, + UpdatedAt = now, + Id = Guid.NewGuid(), + Type = "empty", + ResourceIdentifier = "none", + }; + } +} + diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index 20a4a25..daa1b73 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -1,23 +1,23 @@ using System.Globalization; -using DysonNetwork.Sphere.Activity; -using DysonNetwork.Sphere.Chat; -using DysonNetwork.Sphere.Chat.Realtime; -using DysonNetwork.Sphere.Localization; -using DysonNetwork.Sphere.Post; -using DysonNetwork.Sphere.Publisher; -using DysonNetwork.Sphere.Sticker; -using NodaTime; -using NodaTime.Serialization.SystemTextJson; using System.Text.Json; using System.Text.Json.Serialization; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Registry; using DysonNetwork.Sphere.Autocompletion; -using DysonNetwork.Sphere.WebReader; +using DysonNetwork.Sphere.Chat; +using DysonNetwork.Sphere.Chat.Realtime; using DysonNetwork.Sphere.Discovery; +using DysonNetwork.Sphere.Localization; using DysonNetwork.Sphere.Poll; +using DysonNetwork.Sphere.Post; +using DysonNetwork.Sphere.Publisher; +using DysonNetwork.Sphere.Sticker; +using DysonNetwork.Sphere.Timeline; using DysonNetwork.Sphere.Translation; +using DysonNetwork.Sphere.WebReader; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; namespace DysonNetwork.Sphere.Startup; @@ -34,34 +34,41 @@ public static class ServiceCollectionExtensions services.AddHttpClient(); - services.AddControllers().AddJsonOptions(options => - { - options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + services + .AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.NumberHandling = + JsonNumberHandling.AllowNamedFloatingPointLiterals; + options.JsonSerializerOptions.PropertyNamingPolicy = + JsonNamingPolicy.SnakeCaseLower; - options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - }).AddDataAnnotationsLocalization(options => - { - options.DataAnnotationLocalizerProvider = (type, factory) => - factory.Create(typeof(SharedResource)); - }).ConfigureApplicationPartManager(opts => - { - var mockingPart = opts.ApplicationParts.FirstOrDefault(a => a.Name == "DysonNetwork.Pass"); - if (mockingPart != null) - opts.ApplicationParts.Remove(mockingPart); - }); + options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + }) + .AddDataAnnotationsLocalization(options => + { + options.DataAnnotationLocalizerProvider = (type, factory) => + factory.Create(typeof(SharedResource)); + }) + .ConfigureApplicationPartManager(opts => + { + var mockingPart = opts.ApplicationParts.FirstOrDefault(a => + a.Name == "DysonNetwork.Pass" + ); + if (mockingPart != null) + opts.ApplicationParts.Remove(mockingPart); + }); services.AddRazorPages(); - services.AddGrpc(options => { options.EnableDetailedErrors = true; }); + services.AddGrpc(options => + { + options.EnableDetailedErrors = true; + }); services.AddGrpcReflection(); services.Configure(options => { - var supportedCultures = new[] - { - new CultureInfo("en-US"), - new CultureInfo("zh-Hans"), - }; + var supportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("zh-Hans") }; options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; @@ -71,7 +78,7 @@ public static class ServiceCollectionExtensions return services; } - + public static IServiceCollection AddAppAuthentication(this IServiceCollection services) { services.AddAuthorization(); @@ -86,14 +93,16 @@ public static class ServiceCollectionExtensions return services; } - public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, - IConfiguration configuration) + public static IServiceCollection AddAppBusinessServices( + this IServiceCollection services, + IConfiguration configuration + ) { services.Configure(configuration.GetSection("GeoIP")); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.Sphere/Activity/ActivityController.cs b/DysonNetwork.Sphere/Timeline/TimelineController.cs similarity index 78% rename from DysonNetwork.Sphere/Activity/ActivityController.cs rename to DysonNetwork.Sphere/Timeline/TimelineController.cs index 549c352..6d2508d 100644 --- a/DysonNetwork.Sphere/Activity/ActivityController.cs +++ b/DysonNetwork.Sphere/Timeline/TimelineController.cs @@ -4,16 +4,14 @@ using Microsoft.AspNetCore.Mvc; using NodaTime; using NodaTime.Text; -namespace DysonNetwork.Sphere.Activity; +namespace DysonNetwork.Sphere.Timeline; /// /// Activity is a universal feed that contains multiple kinds of data. Personalized and generated dynamically. /// [ApiController] -[Route("/api/activities")] -public class ActivityController( - ActivityService acts -) : ControllerBase +[Route("/api/timeline")] +public class ActivityController(TimelineService acts) : ControllerBase { /// /// Listing the activities for the user, users may be logged in or not to use this API. @@ -24,7 +22,7 @@ public class ActivityController( /// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them. /// [HttpGet] - public async Task>> ListActivities( + public async Task>> ListEvents( [FromQuery] string? cursor, [FromQuery] string? filter, [FromQuery] int take = 20, @@ -48,7 +46,9 @@ public class ActivityController( HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); return currentUserValue is not Account currentUser - ? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet)) - : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet)); + ? Ok(await acts.ListEventsForAnyone(take, cursorTimestamp, debugIncludeSet)) + : Ok( + await acts.ListEvents(take, cursorTimestamp, currentUser, filter, debugIncludeSet) + ); } -} \ No newline at end of file +} diff --git a/DysonNetwork.Sphere/Activity/DiscoveryActivity.cs b/DysonNetwork.Sphere/Timeline/TimelineDiscovery.cs similarity index 61% rename from DysonNetwork.Sphere/Activity/DiscoveryActivity.cs rename to DysonNetwork.Sphere/Timeline/TimelineDiscovery.cs index af04729..426e86e 100644 --- a/DysonNetwork.Sphere/Activity/DiscoveryActivity.cs +++ b/DysonNetwork.Sphere/Timeline/TimelineDiscovery.cs @@ -1,16 +1,16 @@ using DysonNetwork.Shared.Models; using NodaTime; -namespace DysonNetwork.Sphere.Activity; +namespace DysonNetwork.Sphere.Timeline; -public class DiscoveryActivity(List items) : IActivity +public class TimelineDiscoveryEvent(List items) : ITimelineEvent { public List Items { get; set; } = items; - public SnActivity ToActivity() + public SnTimelineEvent ToActivity() { var now = SystemClock.Instance.GetCurrentInstant(); - return new SnActivity + return new SnTimelineEvent { Id = Guid.NewGuid(), Type = "discovery", @@ -22,4 +22,5 @@ public class DiscoveryActivity(List items) : IActivity } } -public record DiscoveryItem(string Type, object Data); \ No newline at end of file +public record DiscoveryItem(string Type, object Data); + diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Timeline/TimelineService.cs similarity index 75% rename from DysonNetwork.Sphere/Activity/ActivityService.cs rename to DysonNetwork.Sphere/Timeline/TimelineService.cs index 9104a3d..878c8fc 100644 --- a/DysonNetwork.Sphere/Activity/ActivityService.cs +++ b/DysonNetwork.Sphere/Timeline/TimelineService.cs @@ -7,9 +7,9 @@ using DysonNetwork.Sphere.WebReader; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace DysonNetwork.Sphere.Activity; +namespace DysonNetwork.Sphere.Timeline; -public class ActivityService( +public class TimelineService( AppDatabase db, Publisher.PublisherService pub, Post.PostService ps, @@ -20,23 +20,25 @@ public class ActivityService( { private static double CalculateHotRank(SnPost post, Instant now) { - var performanceScore = post.Upvotes - post.Downvotes + post.RepliesCount + (int)post.AwardedScore / 10; + var performanceScore = + post.Upvotes - post.Downvotes + post.RepliesCount + (int)post.AwardedScore / 10; var postTime = post.PublishedAt ?? post.CreatedAt; var timeScore = (now - postTime).TotalMinutes; // Add 1 to score to prevent negative results for posts with more downvotes than upvotes // Time dominates ranking, performance adjusts within similar timeframes. var performanceWeight = performanceScore + 5; // Normalize time influence since average post interval ~60 minutes - var normalizedTime = timeScore / 60.0; + var normalizedTime = timeScore / 60.0; return performanceWeight / Math.Pow(normalizedTime + 1.0, 1.2); } - public async Task> GetActivitiesForAnyone( + public async Task> ListEventsForAnyone( int take, Instant? cursor, - HashSet? debugInclude = null) + HashSet? debugInclude = null + ) { - var activities = new List(); + var activities = new List(); debugInclude ??= new HashSet(); // Get and process posts @@ -51,7 +53,7 @@ public class ActivityService( await LoadPostsRealmsAsync(posts, rs); posts = RankPosts(posts, take); - var interleaved = new List(); + var interleaved = new List(); var random = new Random(); foreach (var post in posts) { @@ -69,26 +71,26 @@ public class ActivityService( activities.AddRange(interleaved); if (activities.Count == 0) - activities.Add(SnActivity.Empty()); + activities.Add(SnTimelineEvent.Empty()); return activities; } - public async Task> GetActivities( + public async Task> ListEvents( int take, Instant? cursor, Account currentUser, string? filter = null, - HashSet? debugInclude = null) + HashSet? debugInclude = null + ) { - var activities = new List(); + var activities = new List(); debugInclude ??= new HashSet(); // Get user's friends and publishers - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { - AccountId = currentUser.Id - }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); @@ -107,20 +109,18 @@ public class ActivityService( currentUser, userFriends, filter is null ? userPublishers : [], - isListing: true) + isListing: true + ) .Take(take * 5); // Get, process and rank posts - var posts = await GetAndProcessPosts( - postsQuery, - currentUser, - trackViews: true); + var posts = await GetAndProcessPosts(postsQuery, currentUser, trackViews: true); await LoadPostsRealmsAsync(posts, rs); posts = RankPosts(posts, take); - var interleaved = new List(); + var interleaved = new List(); var random = new Random(); foreach (var post in posts) { @@ -137,15 +137,19 @@ public class ActivityService( activities.AddRange(interleaved); if (activities.Count == 0) - activities.Add(SnActivity.Empty()); + activities.Add(SnTimelineEvent.Empty()); return activities; } - private async Task MaybeGetDiscoveryActivity(HashSet debugInclude, Instant? cursor) + private async Task MaybeGetDiscoveryActivity( + HashSet debugInclude, + Instant? cursor + ) { - if (cursor != null) return null; - var options = new List>>(); + if (cursor != null) + return null; + var options = new List>>(); if (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2) options.Add(() => GetRealmDiscoveryActivity()); if (debugInclude.Contains("publishers") || Random.Shared.NextDouble() < 0.2) @@ -154,7 +158,8 @@ public class ActivityService( options.Add(() => GetArticleDiscoveryActivity()); if (debugInclude.Contains("shuffledPosts") || Random.Shared.NextDouble() < 0.2) options.Add(() => GetShuffledPostsActivity()); - if (options.Count == 0) return null; + if (options.Count == 0) + return null; var random = new Random(); var pick = options[random.Next(options.Count)]; return await pick(); @@ -177,9 +182,7 @@ public class ActivityService( var now = SystemClock.Instance.GetCurrentInstant(); var recent = now.Minus(Duration.FromDays(7)); - var posts = await db.Posts - .Where(p => p.PublishedAt > recent) - .ToListAsync(); + var posts = await db.Posts.Where(p => p.PublishedAt > recent).ToListAsync(); var publisherIds = posts.Select(p => p.PublisherId).Distinct().ToList(); var publishers = await db.Publishers.Where(p => publisherIds.Contains(p.Id)).ToListAsync(); @@ -188,7 +191,7 @@ public class ActivityService( .Select(p => new { Publisher = p, - Rank = CalculatePopularity(posts.Where(post => post.PublisherId == p.Id).ToList()) + Rank = CalculatePopularity(posts.Where(post => post.PublisherId == p.Id).ToList()), }) .OrderByDescending(x => x.Rank) .Select(x => x.Publisher) @@ -196,30 +199,33 @@ public class ActivityService( .ToList(); } - private async Task GetRealmDiscoveryActivity(int count = 5) + private async Task GetRealmDiscoveryActivity(int count = 5) { var realms = await ds.GetCommunityRealmAsync(null, count, 0, true); return realms.Count > 0 - ? new DiscoveryActivity(realms.Select(x => new DiscoveryItem("realm", x)).ToList()).ToActivity() + ? new TimelineDiscoveryEvent( + realms.Select(x => new DiscoveryItem("realm", x)).ToList() + ).ToActivity() : null; } - private async Task GetPublisherDiscoveryActivity(int count = 5) + private async Task GetPublisherDiscoveryActivity(int count = 5) { var popularPublishers = await GetPopularPublishers(count); return popularPublishers.Count > 0 - ? new DiscoveryActivity(popularPublishers.Select(x => new DiscoveryItem("publisher", x)).ToList()) - .ToActivity() + ? new TimelineDiscoveryEvent( + popularPublishers.Select(x => new DiscoveryItem("publisher", x)).ToList() + ).ToActivity() : null; } - private async Task GetShuffledPostsActivity(int count = 5) + private async Task GetShuffledPostsActivity(int count = 5) { var publicRealms = await rs.GetPublicRealms(); var publicRealmIds = publicRealms.Select(r => r.Id).ToList(); - var postsQuery = db.Posts - .Include(p => p.Categories) + var postsQuery = db + .Posts.Include(p => p.Categories) .Include(p => p.Tags) .Where(p => p.RepliedPostId == null) .Where(p => p.RealmId == null || publicRealmIds.Contains(p.RealmId.Value)) @@ -231,17 +237,22 @@ public class ActivityService( return posts.Count == 0 ? null - : new DiscoveryActivity(posts.Select(x => new DiscoveryItem("post", x)).ToList()).ToActivity(); + : new TimelineDiscoveryEvent( + posts.Select(x => new DiscoveryItem("post", x)).ToList() + ).ToActivity(); } - private async Task GetArticleDiscoveryActivity(int count = 5, int feedSampleSize = 10) + private async Task GetArticleDiscoveryActivity( + int count = 5, + int feedSampleSize = 10 + ) { var now = SystemClock.Instance.GetCurrentInstant(); var today = now.InZone(DateTimeZone.Utc).Date; var todayBegin = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); var todayEnd = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); - var recentFeedIds = await db.WebArticles - .Where(a => a.CreatedAt >= todayBegin && a.CreatedAt < todayEnd) + var recentFeedIds = await db + .WebArticles.Where(a => a.CreatedAt >= todayBegin && a.CreatedAt < todayEnd) .GroupBy(a => a.FeedId) .OrderByDescending(g => g.Max(a => a.PublishedAt)) .Take(feedSampleSize) @@ -253,26 +264,31 @@ public class ActivityService( foreach (var feedId in recentFeedIds.OrderBy(_ => random.Next())) { - var article = await db.WebArticles - .Include(a => a.Feed) + var article = await db + .WebArticles.Include(a => a.Feed) .Where(a => a.FeedId == feedId) .OrderBy(_ => EF.Functions.Random()) .FirstOrDefaultAsync(); - if (article == null) continue; + if (article == null) + continue; recentArticles.Add(article); - if (recentArticles.Count >= count) break; + if (recentArticles.Count >= count) + break; } return recentArticles.Count > 0 - ? new DiscoveryActivity(recentArticles.Select(x => new DiscoveryItem("article", x)).ToList()).ToActivity() + ? new TimelineDiscoveryEvent( + recentArticles.Select(x => new DiscoveryItem("article", x)).ToList() + ).ToActivity() : null; } private async Task> GetAndProcessPosts( IQueryable baseQuery, Account? currentUser = null, - bool trackViews = true) + bool trackViews = true + ) { var posts = await baseQuery.ToListAsync(); posts = await ps.LoadPostInfo(posts, currentUser, true); @@ -282,7 +298,10 @@ public class ActivityService( foreach (var post in posts) { - post.ReactionsCount = reactionMaps.GetValueOrDefault(post.Id, new Dictionary()); + post.ReactionsCount = reactionMaps.GetValueOrDefault( + post.Id, + new Dictionary() + ); if (trackViews && currentUser != null) { @@ -299,8 +318,8 @@ public class ActivityService( List? userRealms = null ) { - var query = db.Posts - .Include(e => e.RepliedPost) + var query = db + .Posts.Include(e => e.RepliedPost) .Include(e => e.ForwardedPost) .Include(e => e.Categories) .Include(e => e.Tags) @@ -319,8 +338,7 @@ public class ActivityService( query = query.Where(p => p.RealmId == null); // Modify in caller } else - query = query.Where(p => - p.RealmId == null || userRealms.Contains(p.RealmId.Value)); + query = query.Where(p => p.RealmId == null || userRealms.Contains(p.RealmId.Value)); return query; } @@ -328,7 +346,8 @@ public class ActivityService( private async Task?> GetFilteredPublishers( string? filter, Account currentUser, - List userFriends) + List userFriends + ) { return filter?.ToLower() switch { @@ -337,14 +356,19 @@ public class ActivityService( .SelectMany(x => x.Value) .DistinctBy(x => x.Id) .ToList(), - _ => null + _ => null, }; } private static async Task LoadPostsRealmsAsync(List posts, RemoteRealmService rs) { - var postRealmIds = posts.Where(p => p.RealmId != null).Select(p => p.RealmId!.Value).Distinct().ToList(); - if (!postRealmIds.Any()) return; + var postRealmIds = posts + .Where(p => p.RealmId != null) + .Select(p => p.RealmId!.Value) + .Distinct() + .ToList(); + if (!postRealmIds.Any()) + return; var realms = await rs.GetRealmBatch(postRealmIds.Select(id => id.ToString()).ToList()); var realmDict = realms.ToDictionary(r => r.Id, r => r);