Publisher API

This commit is contained in:
LittleSheep 2025-04-19 14:41:34 +08:00
parent f9701764f3
commit 0e3b88c51c
15 changed files with 2105 additions and 26 deletions

View File

@ -3,12 +3,13 @@ using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace DysonNetwork.Sphere.Account;
[ApiController]
[Route("/accounts")]
public class AccountController(AppDatabase db, FileService fs) : ControllerBase
public class AccountController(AppDatabase db, FileService fs, IMemoryCache memCache) : ControllerBase
{
[HttpGet("{name}")]
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
@ -77,9 +78,8 @@ public class AccountController(AppDatabase db, FileService fs) : ControllerBase
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
public async Task<ActionResult<Account>> GetMe()
{
var userIdClaim = User.FindFirst("user_id")?.Value;
long? userId = long.TryParse(userIdClaim, out var id) ? id : null;
if (userId is null) return BadRequest("Invalid or missing user_id claim.");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var account = await db.Accounts
.Include(e => e.Profile)
@ -101,15 +101,12 @@ public class AccountController(AppDatabase db, FileService fs) : ControllerBase
[HttpPatch("me")]
public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
{
var userIdClaim = User.FindFirst("user_id")?.Value;
long? userId = long.TryParse(userIdClaim, out var id) ? id : null;
if (userId is null) return BadRequest("Invalid or missing user_id claim.");
var account = await db.Accounts.FindAsync(userId);
if (account is null) return BadRequest("Unable to get your account.");
if (HttpContext.Items["CurrentUser"] is not Account account) return Unauthorized();
if (request.Nick is not null) account.Nick = request.Nick;
if (request.Language is not null) account.Language = request.Language;
memCache.Remove($"user_${account.Id}");
await db.SaveChangesAsync();
return account;
@ -130,9 +127,8 @@ public class AccountController(AppDatabase db, FileService fs) : ControllerBase
[HttpPatch("me/profile")]
public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request)
{
var userIdClaim = User.FindFirst("user_id")?.Value;
long? userId = long.TryParse(userIdClaim, out var id) ? id : null;
if (userId is null) return BadRequest("Invalid or missing user_id claim.");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var profile = await db.AccountProfiles
.Where(p => p.Account.Id == userId)
@ -170,6 +166,9 @@ public class AccountController(AppDatabase db, FileService fs) : ControllerBase
db.Update(profile);
await db.SaveChangesAsync();
memCache.Remove($"user_${userId}");
return profile;
}
}

View File

@ -14,9 +14,8 @@ public class RelationshipController(AppDatabase db, AccountService accounts) : C
public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0,
[FromQuery] int take = 20)
{
var userIdClaim = User.FindFirst("user_id")?.Value;
long? userId = long.TryParse(userIdClaim, out var id) ? id : null;
if (userId is null) return BadRequest("Invalid or missing user_id claim.");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var totalCount = await db.AccountRelationships
.CountAsync(r => r.Account.Id == userId);
@ -42,12 +41,8 @@ public class RelationshipController(AppDatabase db, AccountService accounts) : C
[Authorize]
public async Task<ActionResult<Relationship>> CreateRelationship([FromBody] RelationshipCreateRequest request)
{
var userIdClaim = User.FindFirst("user_id")?.Value;
long? userId = long.TryParse(userIdClaim, out var id) ? id : null;
if (userId is null) return BadRequest("Invalid or missing user_id claim.");
var currentUser = await db.Accounts.FindAsync(userId.Value);
if (currentUser is null) return BadRequest("Failed to get your current user");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(request.UserId);
if (relatedUser is null) return BadRequest("Invalid related user");

View File

@ -27,6 +27,10 @@ public class AppDatabase(
public DbSet<Auth.Session> AuthSessions { get; set; }
public DbSet<Auth.Challenge> AuthChallenges { get; set; }
public DbSet<Storage.CloudFile> Files { get; set; }
public DbSet<Post.Publisher> Publishers { get; set; }
public DbSet<Post.PublisherMember> PublisherMembers { get; set; }
public DbSet<Post.Post> Posts { get; set; }
public DbSet<Post.PostReaction> PostReactions { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@ -63,6 +67,30 @@ public class AppDatabase(
.WithMany(a => a.IncomingRelationships)
.HasForeignKey(r => r.RelatedId);
modelBuilder.Entity<Post.PublisherMember>()
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
modelBuilder.Entity<Post.PublisherMember>()
.HasOne(pm => pm.Publisher)
.WithMany(p => p.Members)
.HasForeignKey(pm => pm.PublisherId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.PublisherMember>()
.HasOne(pm => pm.Account)
.WithMany()
.HasForeignKey(pm => pm.AccountId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.Post>()
.HasOne(p => p.RepliedPost)
.WithMany()
.HasForeignKey("RepliedPostId")
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Post.Post>()
.HasOne(p => p.ForwardedPost)
.WithMany()
.HasForeignKey("ForwardedPostId")
.OnDelete(DeleteBehavior.Restrict);
// Automatically apply soft-delete filter to all entities inheriting BaseModel
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
@ -119,7 +147,7 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin
public async Task Execute(IJobExecutionContext context)
{
logger.LogInformation("Deleting soft-deleted records...");
var now = SystemClock.Instance.GetCurrentInstant();
var threshold = now - Duration.FromDays(7);

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace DysonNetwork.Sphere.Auth;
public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache)
{
public async Task InvokeAsync(HttpContext context, AppDatabase db)
{
var userIdClaim = context.User.FindFirst("user_id")?.Value;
if (userIdClaim is not null && long.TryParse(userIdClaim, out var userId))
{
if (!cache.TryGetValue($"user_{userId}", out Account.Account? user))
{
user = await db.Accounts
.Include(e => e.Profile)
.Where(e => e.Id == userId)
.FirstOrDefaultAsync();
if (user is not null)
{
cache.Set($"user_{userId}", user, TimeSpan.FromMinutes(10));
}
}
if (user is not null)
{
context.Items["CurrentUser"] = user;
}
}
await next(context);
}
}

View File

@ -0,0 +1,966 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using DysonNetwork.Sphere;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
[DbContext(typeof(AppDatabase))]
[Migration("20250419062728_AddPost")]
partial class AddPost
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<bool>("IsSuperuser")
.HasColumnType("boolean")
.HasColumnName("is_superuser");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("language");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<string>("Nick")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("nick");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_accounts");
b.ToTable("accounts", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Secret")
.HasColumnType("text")
.HasColumnName("secret");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_account_auth_factors");
b.HasIndex("AccountId")
.HasDatabaseName("ix_account_auth_factors_account_id");
b.ToTable("account_auth_factors", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("content");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("VerifiedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("verified_at");
b.HasKey("Id")
.HasName("pk_account_contacts");
b.HasIndex("AccountId")
.HasDatabaseName("ix_account_contacts_account_id");
b.ToTable("account_contacts", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<string>("BackgroundId")
.HasColumnType("text")
.HasColumnName("background_id");
b.Property<string>("Bio")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("bio");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("FirstName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("first_name");
b.Property<string>("LastName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("last_name");
b.Property<string>("MiddleName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("middle_name");
b.Property<string>("PictureId")
.HasColumnType("text")
.HasColumnName("picture_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_account_profiles");
b.HasIndex("BackgroundId")
.HasDatabaseName("ix_account_profiles_background_id");
b.HasIndex("PictureId")
.HasDatabaseName("ix_account_profiles_picture_id");
b.ToTable("account_profiles", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b =>
{
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<long>("RelatedId")
.HasColumnType("bigint")
.HasColumnName("related_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("AccountId", "RelatedId")
.HasName("pk_account_relationships");
b.HasIndex("RelatedId")
.HasDatabaseName("ix_account_relationships_related_id");
b.ToTable("account_relationships", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<List<string>>("Audiences")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("audiences");
b.Property<List<long>>("BlacklistFactors")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("blacklist_factors");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("device_id");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<string>("IpAddress")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("ip_address");
b.Property<string>("Nonce")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("nonce");
b.Property<List<string>>("Scopes")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("scopes");
b.Property<int>("StepRemain")
.HasColumnType("integer")
.HasColumnName("step_remain");
b.Property<int>("StepTotal")
.HasColumnType("integer")
.HasColumnName("step_total");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("UserAgent")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("user_agent");
b.HasKey("Id")
.HasName("pk_auth_challenges");
b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_challenges_account_id");
b.ToTable("auth_challenges", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<Guid>("ChallengeId")
.HasColumnType("uuid")
.HasColumnName("challenge_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<Instant?>("LastGrantedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_granted_at");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_auth_sessions");
b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_sessions_account_id");
b.HasIndex("ChallengeId")
.HasDatabaseName("ix_auth_sessions_challenge_id");
b.ToTable("auth_sessions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Content")
.HasColumnType("text")
.HasColumnName("content");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<int>("Downvotes")
.HasColumnType("integer")
.HasColumnName("downvotes");
b.Property<long?>("ForwardedPostId")
.HasColumnType("bigint")
.HasColumnName("forwarded_post_id");
b.Property<long>("PublisherId")
.HasColumnType("bigint")
.HasColumnName("publisher_id");
b.Property<long?>("RepliedPostId")
.HasColumnType("bigint")
.HasColumnName("replied_post_id");
b.Property<string>("Title")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("title");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Upvotes")
.HasColumnType("integer")
.HasColumnName("upvotes");
b.Property<int>("ViewsTotal")
.HasColumnType("integer")
.HasColumnName("views_total");
b.Property<int>("ViewsUnique")
.HasColumnType("integer")
.HasColumnName("views_unique");
b.HasKey("Id")
.HasName("pk_posts");
b.HasIndex("ForwardedPostId")
.HasDatabaseName("ix_posts_forwarded_post_id");
b.HasIndex("PublisherId")
.HasDatabaseName("ix_posts_publisher_id");
b.HasIndex("RepliedPostId")
.HasDatabaseName("ix_posts_replied_post_id");
b.ToTable("posts", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<int>("Attitude")
.HasColumnType("integer")
.HasColumnName("attitude");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<long>("PostId")
.HasColumnType("bigint")
.HasColumnName("post_id");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("symbol");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_post_reactions");
b.HasIndex("AccountId")
.HasDatabaseName("ix_post_reactions_account_id");
b.HasIndex("PostId")
.HasDatabaseName("ix_post_reactions_post_id");
b.ToTable("post_reactions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Publisher", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long?>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<string>("BackgroundId")
.HasColumnType("text")
.HasColumnName("background_id");
b.Property<string>("Bio")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("bio");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<string>("Nick")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("nick");
b.Property<string>("PictureId")
.HasColumnType("text")
.HasColumnName("picture_id");
b.Property<int>("PublisherType")
.HasColumnType("integer")
.HasColumnName("publisher_type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_publishers");
b.HasIndex("AccountId")
.HasDatabaseName("ix_publishers_account_id");
b.HasIndex("BackgroundId")
.HasDatabaseName("ix_publishers_background_id");
b.HasIndex("PictureId")
.HasDatabaseName("ix_publishers_picture_id");
b.ToTable("publishers", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
{
b.Property<long>("PublisherId")
.HasColumnType("bigint")
.HasColumnName("publisher_id");
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("JoinedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("joined_at");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("PublisherId", "AccountId")
.HasName("pk_publisher_members");
b.HasIndex("AccountId")
.HasDatabaseName("ix_publisher_members_account_id");
b.ToTable("publisher_members", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasColumnName("id");
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<Dictionary<string, object>>("FileMeta")
.HasColumnType("jsonb")
.HasColumnName("file_meta");
b.Property<string>("Hash")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("hash");
b.Property<string>("MimeType")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("mime_type");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<long?>("PostId")
.HasColumnType("bigint")
.HasColumnName("post_id");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("UploadedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("uploaded_at");
b.Property<string>("UploadedTo")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("uploaded_to");
b.Property<int>("UsedCount")
.HasColumnType("integer")
.HasColumnName("used_count");
b.Property<Dictionary<string, object>>("UserMeta")
.HasColumnType("jsonb")
.HasColumnName("user_meta");
b.HasKey("Id")
.HasName("pk_files");
b.HasIndex("AccountId")
.HasDatabaseName("ix_files_account_id");
b.HasIndex("PostId")
.HasDatabaseName("ix_files_post_id");
b.ToTable("files", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany("AuthFactors")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_account_auth_factors_accounts_account_id");
b.Navigation("Account");
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany("Contacts")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_account_contacts_accounts_account_id");
b.Navigation("Account");
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b =>
{
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background")
.WithMany()
.HasForeignKey("BackgroundId")
.HasConstraintName("fk_account_profiles_files_background_id");
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithOne("Profile")
.HasForeignKey("DysonNetwork.Sphere.Account.Profile", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_account_profiles_accounts_id");
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture")
.WithMany()
.HasForeignKey("PictureId")
.HasConstraintName("fk_account_profiles_files_picture_id");
b.Navigation("Account");
b.Navigation("Background");
b.Navigation("Picture");
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany("OutgoingRelationships")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_account_relationships_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Account.Account", "Related")
.WithMany("IncomingRelationships")
.HasForeignKey("RelatedId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_account_relationships_accounts_related_id");
b.Navigation("Account");
b.Navigation("Related");
});
modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany("Challenges")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_auth_challenges_accounts_account_id");
b.Navigation("Account");
});
modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany("Sessions")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_auth_sessions_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Auth.Challenge", "Challenge")
.WithMany()
.HasForeignKey("ChallengeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id");
b.Navigation("Account");
b.Navigation("Challenge");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b =>
{
b.HasOne("DysonNetwork.Sphere.Post.Post", "ForwardedPost")
.WithMany()
.HasForeignKey("ForwardedPostId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_posts_posts_forwarded_post_id");
b.HasOne("DysonNetwork.Sphere.Post.Publisher", "Publisher")
.WithMany("Posts")
.HasForeignKey("PublisherId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_posts_publishers_publisher_id");
b.HasOne("DysonNetwork.Sphere.Post.Post", "RepliedPost")
.WithMany()
.HasForeignKey("RepliedPostId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_posts_posts_replied_post_id");
b.Navigation("ForwardedPost");
b.Navigation("Publisher");
b.Navigation("RepliedPost");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_post_reactions_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Post", "Post")
.WithMany("Reactions")
.HasForeignKey("PostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_post_reactions_posts_post_id");
b.Navigation("Account");
b.Navigation("Post");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Publisher", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.HasConstraintName("fk_publishers_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background")
.WithMany()
.HasForeignKey("BackgroundId")
.HasConstraintName("fk_publishers_files_background_id");
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture")
.WithMany()
.HasForeignKey("PictureId")
.HasConstraintName("fk_publishers_files_picture_id");
b.Navigation("Account");
b.Navigation("Background");
b.Navigation("Picture");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_publisher_members_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Publisher", "Publisher")
.WithMany("Members")
.HasForeignKey("PublisherId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_publisher_members_publishers_publisher_id");
b.Navigation("Account");
b.Navigation("Publisher");
});
modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_files_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Post", null)
.WithMany("Attachments")
.HasForeignKey("PostId")
.HasConstraintName("fk_files_posts_post_id");
b.Navigation("Account");
});
modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b =>
{
b.Navigation("AuthFactors");
b.Navigation("Challenges");
b.Navigation("Contacts");
b.Navigation("IncomingRelationships");
b.Navigation("OutgoingRelationships");
b.Navigation("Profile")
.IsRequired();
b.Navigation("Sessions");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b =>
{
b.Navigation("Attachments");
b.Navigation("Reactions");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Publisher", b =>
{
b.Navigation("Members");
b.Navigation("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,248 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class AddPost : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "post_id",
table: "files",
type: "bigint",
nullable: true);
migrationBuilder.CreateTable(
name: "publishers",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
publisher_type = table.Column<int>(type: "integer", nullable: false),
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
nick = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
bio = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
picture_id = table.Column<string>(type: "text", nullable: true),
background_id = table.Column<string>(type: "text", nullable: true),
account_id = table.Column<long>(type: "bigint", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_publishers", x => x.id);
table.ForeignKey(
name: "fk_publishers_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id");
table.ForeignKey(
name: "fk_publishers_files_background_id",
column: x => x.background_id,
principalTable: "files",
principalColumn: "id");
table.ForeignKey(
name: "fk_publishers_files_picture_id",
column: x => x.picture_id,
principalTable: "files",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "posts",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
content = table.Column<string>(type: "text", nullable: true),
type = table.Column<int>(type: "integer", nullable: false),
views_unique = table.Column<int>(type: "integer", nullable: false),
views_total = table.Column<int>(type: "integer", nullable: false),
upvotes = table.Column<int>(type: "integer", nullable: false),
downvotes = table.Column<int>(type: "integer", nullable: false),
replied_post_id = table.Column<long>(type: "bigint", nullable: true),
forwarded_post_id = table.Column<long>(type: "bigint", nullable: true),
publisher_id = table.Column<long>(type: "bigint", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_posts", x => x.id);
table.ForeignKey(
name: "fk_posts_posts_forwarded_post_id",
column: x => x.forwarded_post_id,
principalTable: "posts",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "fk_posts_posts_replied_post_id",
column: x => x.replied_post_id,
principalTable: "posts",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "fk_posts_publishers_publisher_id",
column: x => x.publisher_id,
principalTable: "publishers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "publisher_members",
columns: table => new
{
publisher_id = table.Column<long>(type: "bigint", nullable: false),
account_id = table.Column<long>(type: "bigint", nullable: false),
role = table.Column<int>(type: "integer", nullable: false),
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_publisher_members", x => new { x.publisher_id, x.account_id });
table.ForeignKey(
name: "fk_publisher_members_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_publisher_members_publishers_publisher_id",
column: x => x.publisher_id,
principalTable: "publishers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "post_reactions",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
symbol = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
attitude = table.Column<int>(type: "integer", nullable: false),
post_id = table.Column<long>(type: "bigint", nullable: false),
account_id = table.Column<long>(type: "bigint", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_post_reactions", x => x.id);
table.ForeignKey(
name: "fk_post_reactions_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_post_reactions_posts_post_id",
column: x => x.post_id,
principalTable: "posts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_files_post_id",
table: "files",
column: "post_id");
migrationBuilder.CreateIndex(
name: "ix_post_reactions_account_id",
table: "post_reactions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_post_reactions_post_id",
table: "post_reactions",
column: "post_id");
migrationBuilder.CreateIndex(
name: "ix_posts_forwarded_post_id",
table: "posts",
column: "forwarded_post_id");
migrationBuilder.CreateIndex(
name: "ix_posts_publisher_id",
table: "posts",
column: "publisher_id");
migrationBuilder.CreateIndex(
name: "ix_posts_replied_post_id",
table: "posts",
column: "replied_post_id");
migrationBuilder.CreateIndex(
name: "ix_publisher_members_account_id",
table: "publisher_members",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_publishers_account_id",
table: "publishers",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_publishers_background_id",
table: "publishers",
column: "background_id");
migrationBuilder.CreateIndex(
name: "ix_publishers_picture_id",
table: "publishers",
column: "picture_id");
migrationBuilder.AddForeignKey(
name: "fk_files_posts_post_id",
table: "files",
column: "post_id",
principalTable: "posts",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_files_posts_post_id",
table: "files");
migrationBuilder.DropTable(
name: "post_reactions");
migrationBuilder.DropTable(
name: "publisher_members");
migrationBuilder.DropTable(
name: "posts");
migrationBuilder.DropTable(
name: "publishers");
migrationBuilder.DropIndex(
name: "ix_files_post_id",
table: "files");
migrationBuilder.DropColumn(
name: "post_id",
table: "files");
}
}
}

View File

@ -386,6 +386,247 @@ namespace DysonNetwork.Sphere.Migrations
b.ToTable("auth_sessions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Content")
.HasColumnType("text")
.HasColumnName("content");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<int>("Downvotes")
.HasColumnType("integer")
.HasColumnName("downvotes");
b.Property<long?>("ForwardedPostId")
.HasColumnType("bigint")
.HasColumnName("forwarded_post_id");
b.Property<long>("PublisherId")
.HasColumnType("bigint")
.HasColumnName("publisher_id");
b.Property<long?>("RepliedPostId")
.HasColumnType("bigint")
.HasColumnName("replied_post_id");
b.Property<string>("Title")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("title");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Upvotes")
.HasColumnType("integer")
.HasColumnName("upvotes");
b.Property<int>("ViewsTotal")
.HasColumnType("integer")
.HasColumnName("views_total");
b.Property<int>("ViewsUnique")
.HasColumnType("integer")
.HasColumnName("views_unique");
b.HasKey("Id")
.HasName("pk_posts");
b.HasIndex("ForwardedPostId")
.HasDatabaseName("ix_posts_forwarded_post_id");
b.HasIndex("PublisherId")
.HasDatabaseName("ix_posts_publisher_id");
b.HasIndex("RepliedPostId")
.HasDatabaseName("ix_posts_replied_post_id");
b.ToTable("posts", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<int>("Attitude")
.HasColumnType("integer")
.HasColumnName("attitude");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<long>("PostId")
.HasColumnType("bigint")
.HasColumnName("post_id");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("symbol");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_post_reactions");
b.HasIndex("AccountId")
.HasDatabaseName("ix_post_reactions_account_id");
b.HasIndex("PostId")
.HasDatabaseName("ix_post_reactions_post_id");
b.ToTable("post_reactions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Publisher", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long?>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<string>("BackgroundId")
.HasColumnType("text")
.HasColumnName("background_id");
b.Property<string>("Bio")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("bio");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<string>("Nick")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("nick");
b.Property<string>("PictureId")
.HasColumnType("text")
.HasColumnName("picture_id");
b.Property<int>("PublisherType")
.HasColumnType("integer")
.HasColumnName("publisher_type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_publishers");
b.HasIndex("AccountId")
.HasDatabaseName("ix_publishers_account_id");
b.HasIndex("BackgroundId")
.HasDatabaseName("ix_publishers_background_id");
b.HasIndex("PictureId")
.HasDatabaseName("ix_publishers_picture_id");
b.ToTable("publishers", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
{
b.Property<long>("PublisherId")
.HasColumnType("bigint")
.HasColumnName("publisher_id");
b.Property<long>("AccountId")
.HasColumnType("bigint")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("JoinedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("joined_at");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("PublisherId", "AccountId")
.HasName("pk_publisher_members");
b.HasIndex("AccountId")
.HasDatabaseName("ix_publisher_members_account_id");
b.ToTable("publisher_members", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b =>
{
b.Property<string>("Id")
@ -429,6 +670,10 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<long?>("PostId")
.HasColumnType("bigint")
.HasColumnName("post_id");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
@ -460,6 +705,9 @@ namespace DysonNetwork.Sphere.Migrations
b.HasIndex("AccountId")
.HasDatabaseName("ix_files_account_id");
b.HasIndex("PostId")
.HasDatabaseName("ix_files_post_id");
b.ToTable("files", (string)null);
});
@ -567,6 +815,100 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Challenge");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b =>
{
b.HasOne("DysonNetwork.Sphere.Post.Post", "ForwardedPost")
.WithMany()
.HasForeignKey("ForwardedPostId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_posts_posts_forwarded_post_id");
b.HasOne("DysonNetwork.Sphere.Post.Publisher", "Publisher")
.WithMany("Posts")
.HasForeignKey("PublisherId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_posts_publishers_publisher_id");
b.HasOne("DysonNetwork.Sphere.Post.Post", "RepliedPost")
.WithMany()
.HasForeignKey("RepliedPostId")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_posts_posts_replied_post_id");
b.Navigation("ForwardedPost");
b.Navigation("Publisher");
b.Navigation("RepliedPost");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_post_reactions_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Post", "Post")
.WithMany("Reactions")
.HasForeignKey("PostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_post_reactions_posts_post_id");
b.Navigation("Account");
b.Navigation("Post");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Publisher", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.HasConstraintName("fk_publishers_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Background")
.WithMany()
.HasForeignKey("BackgroundId")
.HasConstraintName("fk_publishers_files_background_id");
b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "Picture")
.WithMany()
.HasForeignKey("PictureId")
.HasConstraintName("fk_publishers_files_picture_id");
b.Navigation("Account");
b.Navigation("Background");
b.Navigation("Picture");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_publisher_members_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Publisher", "Publisher")
.WithMany("Members")
.HasForeignKey("PublisherId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_publisher_members_publishers_publisher_id");
b.Navigation("Account");
b.Navigation("Publisher");
});
modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
@ -576,6 +918,11 @@ namespace DysonNetwork.Sphere.Migrations
.IsRequired()
.HasConstraintName("fk_files_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Post", null)
.WithMany("Attachments")
.HasForeignKey("PostId")
.HasConstraintName("fk_files_posts_post_id");
b.Navigation("Account");
});
@ -596,6 +943,20 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Sessions");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b =>
{
b.Navigation("Attachments");
b.Navigation("Reactions");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.Publisher", b =>
{
b.Navigation("Members");
b.Navigation("Posts");
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,55 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage;
namespace DysonNetwork.Sphere.Post;
public enum PostType
{
Moment,
Article,
Video
}
public class Post : ModelBase
{
public long Id { get; set; }
[MaxLength(1024)] public string? Title { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string? Content { get; set; }
public PostType Type { get; set; }
[Column(TypeName = "jsonb")] Dictionary<string, object>? Meta { get; set; }
public int ViewsUnique { get; set; }
public int ViewsTotal { get; set; }
public int Upvotes { get; set; }
public int Downvotes { get; set; }
public Post? RepliedPost { get; set; }
public Post? ForwardedPost { get; set; }
public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>();
public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>();
public Publisher Publisher { get; set; } = null!;
}
public enum PostReactionAttitude
{
Positive,
Neutral,
Negative,
}
public class PostReaction : ModelBase
{
public long Id { get; set; }
[MaxLength(256)] public string Symbol { get; set; } = null!;
public PostReactionAttitude Attitude { get; set; }
public long PostId { get; set; }
[JsonIgnore] public Post Post { get; set; } = null!;
public Account.Account Account { get; set; } = null!;
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Sphere.Post;
public class PostService(AppDatabase db)
{
}

View File

@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage;
using NodaTime;
namespace DysonNetwork.Sphere.Post;
public enum PublisherType
{
Individual,
Organizational
}
public class Publisher : ModelBase
{
public long Id { get; set; }
public PublisherType PublisherType { 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; }
public CloudFile? Picture { get; set; }
public CloudFile? Background { get; set; }
[JsonIgnore] public ICollection<Post> Posts { get; set; } = new List<Post>();
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
[JsonIgnore] public Account.Account? Account { get; set; }
}
public enum PublisherMemberRole
{
Owner = 100,
Manager = 75,
Editor = 50,
Viewer = 25
}
public class PublisherMember : ModelBase
{
public long PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
public long AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
public PublisherMemberRole Role { get; set; }
public Instant? JoinedAt { get; set; }
}

View File

@ -0,0 +1,285 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Post;
[ApiController]
[Route("/publishers")]
public class PublisherController(AppDatabase db, PublisherService ps, FileService fs) : ControllerBase
{
[HttpGet("{name}")]
public async Task<ActionResult<Publisher>> GetPublisher(string name)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var publisher = await db.Publishers
.Where(e => e.Name == name)
.FirstOrDefaultAsync();
if (publisher is null) return NotFound();
return Ok(publisher);
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<Publisher>>> ListManagedPublishers()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var members = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.JoinedAt != null)
.Include(e => e.Publisher)
.ToListAsync();
return members.Select(m => m.Publisher).ToList();
}
[HttpGet("invites")]
[Authorize]
public async Task<ActionResult<List<PublisherMember>>> ListInvites()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var members = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.JoinedAt == null)
.Include(e => e.Publisher)
.ToListAsync();
return members.ToList();
}
public class PublisherMemberRequest
{
[Required] public long RelatedUserId { get; set; }
[Required] public PublisherMemberRole Role { get; set; }
}
[HttpPost("invites/{name}")]
[Authorize]
public async Task<ActionResult<PublisherMember>> InviteMember(string name, [FromBody] PublisherMemberRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
if (relatedUser is null) return BadRequest("Related user was not found");
var publisher = await db.Publishers
.Where(p => p.Name == name)
.Include(publisher => publisher.Picture)
.Include(publisher => publisher.Background)
.FirstOrDefaultAsync();
if (publisher is null) return NotFound();
var member = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.PublisherId == publisher.Id)
.FirstOrDefaultAsync();
if (member is null) return StatusCode(403, "You are not even a member of the targeted publisher.");
if (member.Role < PublisherMemberRole.Manager)
return StatusCode(403, "You need at least be a manager to invite other members to collaborate this publisher.");
if (member.Role < request.Role)
return StatusCode(403, "You cannot invite member has higher permission than yours.");
var newMember = new PublisherMember
{
Account = relatedUser,
AccountId = relatedUser.Id,
Publisher = publisher,
PublisherId = publisher.Id,
Role = request.Role,
};
db.PublisherMembers.Add(newMember);
await db.SaveChangesAsync();
return Ok(newMember);
}
[HttpPost("invites/{name}/accept")]
[Authorize]
public async Task<ActionResult<Publisher>> AcceptMemberInvite(string name)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var member = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.Publisher.Name == name)
.Where(m => m.JoinedAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
member.JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
db.Update(member);
await db.SaveChangesAsync();
return Ok(member);
}
[HttpPost("invites/{name}/decline")]
[Authorize]
public async Task<ActionResult> DeclineMemberInvite(string name)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var member = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.Publisher.Name == name)
.Where(m => m.JoinedAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
db.PublisherMembers.Remove(member);
await db.SaveChangesAsync();
return NoContent();
}
public class PublisherRequest
{
[MaxLength(256)] public string? Name { get; set; }
[MaxLength(256)] public string? Nick { get; set; }
[MaxLength(4096)] public string? Bio { get; set; }
public string? PictureId { get; set; }
public string? BackgroundId { get; set; }
}
[HttpPost("individual")]
[Authorize]
public async Task<ActionResult<Publisher>> CreatePublisherIndividual(PublisherRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var takenName = request.Name ?? currentUser.Name;
var duplicateNameCount = await db.Publishers
.Where(p => p.Name == takenName)
.CountAsync();
if (duplicateNameCount > 0)
return BadRequest(
"The name you requested has already be taken, " +
"if it is your account name, " +
"you can request a taken down to the publisher which created with " +
"your name firstly to get your name back."
);
CloudFile? picture = null, background = null;
if (request.PictureId is not null)
{
picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
}
if (request.BackgroundId is not null)
{
background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
}
var publisher = await ps.CreateIndividualPublisher(
currentUser,
request.Name,
request.Nick,
request.Bio,
picture,
background
);
return Ok(publisher);
}
[HttpPatch("{name}")]
[Authorize]
public async Task<ActionResult<Publisher>> UpdatePublisher(string name, PublisherRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var publisher = await db.Publishers
.Where(p => p.Name == name)
.Include(publisher => publisher.Picture)
.Include(publisher => publisher.Background)
.FirstOrDefaultAsync();
if (publisher is null) return NotFound();
var member = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.PublisherId == publisher.Id)
.FirstOrDefaultAsync();
if (member is null) return StatusCode(403, "You are not even a member of the targeted publisher.");
if (member.Role < PublisherMemberRole.Manager)
return StatusCode(403, "You need at least be the manager to update the publisher profile.");
if (request.Name is not null) publisher.Name = request.Name;
if (request.Nick is not null) publisher.Nick = request.Nick;
if (request.Bio is not null) publisher.Bio = request.Bio;
if (request.PictureId is not null)
{
var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
if (picture is null) return BadRequest("Invalid picture id.");
if (publisher.Picture is not null) await fs.MarkUsageAsync(publisher.Picture, -1);
publisher.Picture = picture;
await fs.MarkUsageAsync(picture, 1);
}
if (request.BackgroundId is not null)
{
var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
if (background is null) return BadRequest("Invalid background id.");
if (publisher.Background is not null) await fs.MarkUsageAsync(publisher.Background, -1);
publisher.Background = background;
await fs.MarkUsageAsync(background, 1);
}
db.Update(publisher);
await db.SaveChangesAsync();
return Ok(publisher);
}
[HttpDelete("{name}")]
[Authorize]
public async Task<ActionResult<Publisher>> DeletePublisher(string name)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var publisher = await db.Publishers
.Where(p => p.Name == name)
.Include(publisher => publisher.Picture)
.Include(publisher => publisher.Background)
.FirstOrDefaultAsync();
if (publisher is null) return NotFound();
var member = await db.PublisherMembers
.Where(m => m.AccountId == userId)
.Where(m => m.PublisherId == publisher.Id)
.FirstOrDefaultAsync();
if (member is null) return StatusCode(403, "You are not even a member of the targeted publisher.");
if (member.Role < PublisherMemberRole.Owner)
return StatusCode(403, "You need to be the owner to delete the publisher.");
if (publisher.Picture is not null)
await fs.MarkUsageAsync(publisher.Picture, -1);
if (publisher.Background is not null)
await fs.MarkUsageAsync(publisher.Background, -1);
db.Publishers.Remove(publisher);
await db.SaveChangesAsync();
return NoContent();
}
}

View File

@ -0,0 +1,48 @@
using DysonNetwork.Sphere.Storage;
using NodaTime;
namespace DysonNetwork.Sphere.Post;
public class PublisherService(AppDatabase db, FileService fs)
{
public async Task<Publisher> CreateIndividualPublisher(
Account.Account account,
string? name,
string? nick,
string? bio,
CloudFile? picture,
CloudFile? background
)
{
var publisher = new Publisher
{
PublisherType = PublisherType.Individual,
Name = name ?? account.Name,
Nick = nick ?? account.Nick,
Bio = bio ?? account.Profile.Bio,
Picture = picture ?? account.Profile.Picture,
Background = background ?? account.Profile.Background,
Account = account,
Members = new List<PublisherMember>
{
new()
{
AccountId = account.Id,
Account = account,
Role = PublisherMemberRole.Owner,
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
}
}
};
db.Publishers.Add(publisher);
await db.SaveChangesAsync();
if (publisher.Picture is not null) await fs.MarkUsageAsync(publisher.Picture, 1);
if (publisher.Background is not null) await fs.MarkUsageAsync(publisher.Background, 1);
return publisher;
}
// TODO Able to create organizational publisher when the realm system is completed
}

View File

@ -7,6 +7,7 @@ using Casbin.Persist.Adapter.EFCore;
using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Auth;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
@ -29,6 +30,8 @@ builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
// Add services to the container.
builder.Services.AddDbContext<AppDatabase>();
builder.Services.AddMemoryCache();
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
@ -117,6 +120,8 @@ builder.Services.AddOpenApi();
builder.Services.AddScoped<AccountService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<FileService>();
builder.Services.AddScoped<PublisherService>();
builder.Services.AddScoped<PostService>();
// Timed task
@ -167,6 +172,7 @@ app.UseCors(opts =>
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseMiddleware<UserInfoMiddleware>();
app.MapControllers();

View File

@ -83,9 +83,8 @@ public class FileController(
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteFile(string id)
{
var userIdClaim = User.FindFirst("user_id")?.Value;
if (userIdClaim is null) return Unauthorized();
var userId = long.Parse(userIdClaim);
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var file = await db.Files
.Where(e => e.Id == id)

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationMiddleware_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003F2f_003F7ab1cc57_003FAuthenticationMiddleware_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthorizationAppBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2ff26593f91746d7a53418a46dc419d1f200_003F4b_003F56550da2_003FAuthorizationAppBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABucketArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd515fb889657fcdcace3fed90735057b458ff9e0bb60bded7c8fe8b3a4673c_003FBucketArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AChapterData_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fe6_003F64a6c0f7_003FChapterData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>