Web feed subcription

This commit is contained in:
2025-08-20 14:21:25 +08:00
parent 85fef30c7f
commit 3ee5e5367d
6 changed files with 2247 additions and 1 deletions

View File

@@ -55,6 +55,7 @@ public class AppDatabase(
public DbSet<WebReader.WebArticle> WebArticles { get; set; } = null!; public DbSet<WebReader.WebArticle> WebArticles { get; set; } = null!;
public DbSet<WebReader.WebFeed> WebFeeds { get; set; } = null!; public DbSet<WebReader.WebFeed> WebFeeds { get; set; } = null!;
public DbSet<WebReader.WebFeedSubscription> WebFeedSubscriptions { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class AddWebFeedSubscription : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "web_feed_subscriptions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
feed_id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", 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_web_feed_subscriptions", x => x.id);
table.ForeignKey(
name: "fk_web_feed_subscriptions_web_feeds_feed_id",
column: x => x.feed_id,
principalTable: "web_feeds",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_web_feed_subscriptions_feed_id",
table: "web_feed_subscriptions",
column: "feed_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "web_feed_subscriptions");
}
}
}

View File

@@ -1458,6 +1458,42 @@ namespace DysonNetwork.Sphere.Migrations
b.ToTable("web_feeds", (string)null); b.ToTable("web_feeds", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Sphere.WebReader.WebFeedSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.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<Guid>("FeedId")
.HasColumnType("uuid")
.HasColumnName("feed_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_web_feed_subscriptions");
b.HasIndex("FeedId")
.HasDatabaseName("ix_web_feed_subscriptions_feed_id");
b.ToTable("web_feed_subscriptions", (string)null);
});
modelBuilder.Entity("PostPostCategory", b => modelBuilder.Entity("PostPostCategory", b =>
{ {
b.Property<Guid>("CategoriesId") b.Property<Guid>("CategoriesId")
@@ -1808,7 +1844,7 @@ namespace DysonNetwork.Sphere.Migrations
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPackOwnership", b => modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPackOwnership", b =>
{ {
b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack") b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack")
.WithMany() .WithMany("Ownerships")
.HasForeignKey("PackId") .HasForeignKey("PackId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired() .IsRequired()
@@ -1841,6 +1877,18 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Publisher"); b.Navigation("Publisher");
}); });
modelBuilder.Entity("DysonNetwork.Sphere.WebReader.WebFeedSubscription", b =>
{
b.HasOne("DysonNetwork.Sphere.WebReader.WebFeed", "Feed")
.WithMany()
.HasForeignKey("FeedId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_web_feed_subscriptions_web_feeds_feed_id");
b.Navigation("Feed");
});
modelBuilder.Entity("PostPostCategory", b => modelBuilder.Entity("PostPostCategory", b =>
{ {
b.HasOne("DysonNetwork.Sphere.Post.PostCategory", null) b.HasOne("DysonNetwork.Sphere.Post.PostCategory", null)
@@ -1936,6 +1984,8 @@ namespace DysonNetwork.Sphere.Migrations
modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPack", b => modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPack", b =>
{ {
b.Navigation("Ownerships");
b.Navigation("Stickers"); b.Navigation("Stickers");
}); });

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Pass.Account;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
namespace DysonNetwork.Sphere.WebReader; namespace DysonNetwork.Sphere.WebReader;
@@ -44,4 +45,14 @@ public class WebFeed : ModelBase
public Publisher.Publisher Publisher { get; set; } = null!; public Publisher.Publisher Publisher { get; set; } = null!;
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>(); [JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
}
public class WebFeedSubscription : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid FeedId { get; set; }
public WebFeed Feed { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public Account Account { get; set; } = null!;
} }

View File

@@ -0,0 +1,132 @@
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.WebReader;
[ApiController]
[Route("/api/feeds")]
public class WebFeedPublicController(
AppDatabase db
) : ControllerBase
{
/// <summary>
/// Subscribe to a web feed
/// </summary>
/// <param name="feedId">The ID of the feed to subscribe to</param>
/// <returns>Subscription details</returns>
[HttpPost("{feedId:guid}/subscribe")]
[Authorize]
public async Task<IActionResult> Subscribe(Guid feedId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
// Check if feed exists
var feed = await db.WebFeeds.FindAsync(feedId);
if (feed == null)
return NotFound("Feed not found");
// Check if already subscribed
var existingSubscription = await db.WebFeedSubscriptions
.FirstOrDefaultAsync(s => s.FeedId == feedId && s.AccountId == accountId);
if (existingSubscription != null)
return Ok(existingSubscription);
// Create new subscription
var subscription = new WebFeedSubscription
{
FeedId = feedId,
AccountId = accountId
};
db.WebFeedSubscriptions.Add(subscription);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(GetSubscriptionStatus), new { feedId }, subscription);
}
/// <summary>
/// Unsubscribe from a web feed
/// </summary>
/// <param name="feedId">The ID of the feed to unsubscribe from</param>
[HttpDelete("{feedId:guid}/subscribe")]
[Authorize]
public async Task<IActionResult> Unsubscribe(Guid feedId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var subscription = await db.WebFeedSubscriptions
.FirstOrDefaultAsync(s => s.FeedId == feedId && s.AccountId == accountId);
if (subscription == null)
return NoContent();
db.WebFeedSubscriptions.Remove(subscription);
await db.SaveChangesAsync();
return NoContent();
}
/// <summary>
/// Get subscription status for the current user and a specific feed
/// </summary>
/// <param name="feedId">The ID of the feed to check</param>
/// <returns>Subscription status</returns>
[HttpGet("{feedId:guid}/subscription")]
[Authorize]
public async Task<ActionResult<WebFeedSubscription>> GetSubscriptionStatus(Guid feedId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var subscription = await db.WebFeedSubscriptions
.Where(s => s.FeedId == feedId && s.AccountId == accountId)
.FirstOrDefaultAsync();
if (subscription is null)
return NotFound();
return Ok(subscription);
}
/// <summary>
/// List all feeds the current user is subscribed to
/// </summary>
/// <returns>List of subscribed feeds</returns>
[HttpGet("me")]
[Authorize]
public async Task<IActionResult> GetMySubscriptions(
[FromQuery] int offset = 0,
[FromQuery] int take = 20
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var query = db.WebFeedSubscriptions
.Where(s => s.AccountId == accountId)
.Include(s => s.Feed)
.ThenInclude(f => f.Publisher)
.OrderByDescending(s => s.CreatedAt);
var totalCount = await query.CountAsync();
var subscriptions = await query
.Skip(offset)
.Take(take)
.ToListAsync();
Response.Headers["X-Total"] = totalCount.ToString();
return Ok(subscriptions);
}
}