Organization publishers, subscriptions to publishers

This commit is contained in:
LittleSheep 2025-05-12 21:48:16 +08:00
parent b20bc3c443
commit b275f06061
14 changed files with 6197 additions and 19 deletions

View File

@ -44,6 +44,7 @@ public class AppDatabase(
public DbSet<Post.Publisher> Publishers { get; set; }
public DbSet<Post.PublisherMember> PublisherMembers { get; set; }
public DbSet<Post.PublisherSubscription> PublisherSubscriptions { get; set; }
public DbSet<Post.Post> Posts { get; set; }
public DbSet<Post.PostReaction> PostReactions { get; set; }
public DbSet<Post.PostTag> PostTags { get; set; }
@ -148,6 +149,16 @@ public class AppDatabase(
.WithMany()
.HasForeignKey(pm => pm.AccountId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.PublisherSubscription>()
.HasOne(ps => ps.Publisher)
.WithMany(p => p.Subscriptions)
.HasForeignKey(ps => ps.PublisherId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.PublisherSubscription>()
.HasOne(ps => ps.Account)
.WithMany()
.HasForeignKey(ps => ps.AccountId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.Post>()
.HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content })

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class AddPublisherSubscription : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "publisher_subscriptions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
publisher_id = table.Column<long>(type: "bigint", nullable: false),
account_id = table.Column<long>(type: "bigint", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
tier = table.Column<int>(type: "integer", 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_publisher_subscriptions", x => x.id);
table.ForeignKey(
name: "fk_publisher_subscriptions_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_publisher_subscriptions_publishers_publisher_id",
column: x => x.publisher_id,
principalTable: "publishers",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_publisher_subscriptions_account_id",
table: "publisher_subscriptions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_publisher_subscriptions_publisher_id",
table: "publisher_subscriptions",
column: "publisher_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "publisher_subscriptions");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class PublisherWithOrganization : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "realm_id",
table: "publishers",
type: "bigint",
nullable: true);
migrationBuilder.CreateIndex(
name: "ix_publishers_realm_id",
table: "publishers",
column: "realm_id");
migrationBuilder.AddForeignKey(
name: "fk_publishers_realms_realm_id",
table: "publishers",
column: "realm_id",
principalTable: "realms",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_publishers_realms_realm_id",
table: "publishers");
migrationBuilder.DropIndex(
name: "ix_publishers_realm_id",
table: "publishers");
migrationBuilder.DropColumn(
name: "realm_id",
table: "publishers");
}
}
}

View File

@ -1575,6 +1575,10 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("integer")
.HasColumnName("publisher_type");
b.Property<long?>("RealmId")
.HasColumnType("bigint")
.HasColumnName("realm_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
@ -1595,6 +1599,9 @@ namespace DysonNetwork.Sphere.Migrations
b.HasIndex("PictureId")
.HasDatabaseName("ix_publishers_picture_id");
b.HasIndex("RealmId")
.HasDatabaseName("ix_publishers_realm_id");
b.ToTable("publishers", (string)null);
});
@ -1637,6 +1644,53 @@ namespace DysonNetwork.Sphere.Migrations
b.ToTable("publisher_members", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.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<long>("PublisherId")
.HasColumnType("bigint")
.HasColumnName("publisher_id");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<int>("Tier")
.HasColumnType("integer")
.HasColumnName("tier");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_publisher_subscriptions");
b.HasIndex("AccountId")
.HasDatabaseName("ix_publisher_subscriptions_account_id");
b.HasIndex("PublisherId")
.HasDatabaseName("ix_publisher_subscriptions_publisher_id");
b.ToTable("publisher_subscriptions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>
{
b.Property<long>("Id")
@ -2442,11 +2496,18 @@ namespace DysonNetwork.Sphere.Migrations
.HasForeignKey("PictureId")
.HasConstraintName("fk_publishers_files_picture_id");
b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm")
.WithMany()
.HasForeignKey("RealmId")
.HasConstraintName("fk_publishers_realms_realm_id");
b.Navigation("Account");
b.Navigation("Background");
b.Navigation("Picture");
b.Navigation("Realm");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
@ -2470,6 +2531,27 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Publisher");
});
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherSubscription", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
.WithMany()
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_publisher_subscriptions_accounts_account_id");
b.HasOne("DysonNetwork.Sphere.Post.Publisher", "Publisher")
.WithMany("Subscriptions")
.HasForeignKey("PublisherId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_publisher_subscriptions_publishers_publisher_id");
b.Navigation("Account");
b.Navigation("Publisher");
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>
{
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
@ -2676,6 +2758,8 @@ namespace DysonNetwork.Sphere.Migrations
b.Navigation("Members");
b.Navigation("Posts");
b.Navigation("Subscriptions");
});
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>

View File

@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Post;
[ApiController]
[Route("/posts")]
public class PostController(AppDatabase db, PostService ps, RelationshipService rels) : ControllerBase
public class PostController(AppDatabase db, PostService ps, RelationshipService rels, IServiceScopeFactory factory) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20)
@ -205,6 +205,13 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService
return BadRequest(err.Message);
}
_ = Task.Run(async () =>
{
using var scope = factory.CreateScope();
var subs = scope.ServiceProvider.GetRequiredService<PublisherSubscriptionService>();
await subs.NotifySubscribersPostAsync(post);
});
return post;
}

View File

@ -31,8 +31,13 @@ public class Publisher : ModelBase
[JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>();
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
[JsonIgnore]
public ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>();
public long? AccountId { get; set; }
[JsonIgnore] public Account.Account? Account { get; set; }
public long? RealmId { get; set; }
[JsonIgnore] public Realm.Realm? Realm { get; set; }
}
public enum PublisherMemberRole
@ -53,3 +58,23 @@ public class PublisherMember : ModelBase
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
public Instant? JoinedAt { get; set; }
}
public enum SubscriptionStatus
{
Active,
Expired,
Cancelled
}
public class PublisherSubscription : ModelBase
{
public Guid Id { get; set; }
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 SubscriptionStatus Status { get; set; } = SubscriptionStatus.Active;
public int Tier { get; set; } = 0;
}

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -172,7 +173,7 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
[HttpPost("individual")]
[Authorize]
[RequiredPermission("global", "publishers.create")]
public async Task<ActionResult<Publisher>> CreatePublisherIndividual(PublisherRequest request)
public async Task<ActionResult<Publisher>> CreatePublisherIndividual([FromBody] PublisherRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
@ -213,6 +214,55 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
return Ok(publisher);
}
[HttpPost("organization/{realmSlug}")]
[Authorize]
[RequiredPermission("global", "publishers.create")]
public async Task<ActionResult<Publisher>> CreatePublisherOrganization(string realmSlug, [FromBody] PublisherRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var realm = await db.Realms.FirstOrDefaultAsync(r => r.Slug == realmSlug);
if (realm == null) return NotFound("Realm not found");
var isAdmin = await db.RealmMembers
.AnyAsync(m => m.RealmId == realm.Id && m.AccountId == currentUser.Id && m.Role >= RealmMemberRole.Moderator);
if (!isAdmin) return StatusCode(403, "You need to be a moderator of the realm to create an organization publisher");
var takenName = request.Name ?? realm.Slug;
var duplicateNameCount = await db.Publishers
.Where(p => p.Name == takenName)
.CountAsync();
if (duplicateNameCount > 0)
return BadRequest("The name you requested has already been taken");
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.CreateOrganizationPublisher(
realm,
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)

View File

@ -45,7 +45,44 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
return publisher;
}
// TODO Able to create organizational publisher when the realm system is completed
public async Task<Publisher> CreateOrganizationPublisher(
Realm.Realm realm,
Account.Account account,
string? name,
string? nick,
string? bio,
CloudFile? picture,
CloudFile? background
)
{
var publisher = new Publisher
{
PublisherType = PublisherType.Organizational,
Name = name ?? realm.Slug,
Nick = nick ?? realm.Name,
Bio = bio ?? realm.Description,
Picture = picture ?? realm.Picture,
Background = background ?? realm.Background,
RealmId = realm.Id,
Members = new List<PublisherMember>
{
new()
{
AccountId = account.Id,
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;
}
public class PublisherStats
{
@ -54,6 +91,7 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
public int StickersCreated { get; set; }
public int UpvoteReceived { get; set; }
public int DownvoteReceived { get; set; }
public int SubscribersCount { get; set; }
}
private const string PublisherStatsCacheKey = "PublisherStats_{0}";
@ -75,18 +113,22 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Negative)
.CountAsync();
var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id).ToListAsync();
var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id)
.ToListAsync();
var stickerPacksCount = stickerPacksId.Count;
var stickersCount = await db.Stickers.Where(e => stickerPacksId.Contains(e.PackId)).CountAsync();
var subscribersCount = await db.PublisherSubscriptions.Where(e => e.PublisherId == publisher.Id).CountAsync();
stats = new PublisherStats
{
PostsCreated = postsCount,
StickerPacksCreated = stickerPacksCount,
StickersCreated = stickersCount,
UpvoteReceived = postsUpvotes,
DownvoteReceived = postsDownvotes
DownvoteReceived = postsDownvotes,
SubscribersCount = subscribersCount,
};
cache.Set(cacheKey, stats, TimeSpan.FromMinutes(5));

View File

@ -0,0 +1,126 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Post;
[ApiController]
[Route("api/[controller]")]
public class PublisherSubscriptionController(
PublisherSubscriptionService subs,
AppDatabase db,
ILogger<PublisherSubscriptionController> logger
)
: ControllerBase
{
public class SubscriptionStatusResponse
{
public bool IsSubscribed { get; set; }
public long PublisherId { get; set; }
public string PublisherName { get; set; } = string.Empty;
}
public class SubscribeRequest
{
public int? Tier { get; set; }
}
/// <summary>
/// Check if the current user is subscribed to a publisher
/// </summary>
/// <param name="publisherId">Publisher ID to check</param>
/// <returns>Subscription status</returns>
[HttpGet("{publisherId}/status")]
[Authorize]
public async Task<ActionResult<SubscriptionStatusResponse>> CheckSubscriptionStatus(long publisherId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
// Check if the publisher exists
var publisher = await db.Publishers.FindAsync(publisherId);
if (publisher == null)
return NotFound("Publisher not found");
var isSubscribed = await subs.SubscriptionExistsAsync(currentUser.Id, publisherId);
return new SubscriptionStatusResponse
{
IsSubscribed = isSubscribed,
PublisherId = publisherId,
PublisherName = publisher.Name
};
}
/// <summary>
/// Create or activate a subscription to a publisher
/// </summary>
/// <param name="publisherId">Publisher ID to subscribe to</param>
/// <param name="request">Subscription details</param>
/// <returns>The created or activated subscription</returns>
[HttpPost("{publisherId}/subscribe")]
[Authorize]
public async Task<ActionResult<PublisherSubscription>> Subscribe(
long publisherId,
[FromBody] SubscribeRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
// Check if the publisher exists
var publisher = await db.Publishers.FindAsync(publisherId);
if (publisher == null)
return NotFound("Publisher not found");
try
{
var subscription = await subs.CreateSubscriptionAsync(
currentUser.Id,
publisherId,
request.Tier ?? 0
);
return subscription;
}
catch (Exception ex)
{
logger.LogError(ex, "Error subscribing to publisher {PublisherId}", publisherId);
return StatusCode(500, "Failed to create subscription");
}
}
/// <summary>
/// Cancel a subscription to a publisher
/// </summary>
/// <param name="publisherId">Publisher ID to unsubscribe from</param>
/// <returns>Success status</returns>
[HttpPost("{publisherId}/unsubscribe")]
[Authorize]
public async Task<ActionResult> Unsubscribe(long publisherId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
// Check if the publisher exists
var publisher = await db.Publishers.FindAsync(publisherId);
if (publisher == null)
return NotFound("Publisher not found");
var success = await subs.CancelSubscriptionAsync(currentUser.Id, publisherId);
if (success)
return Ok(new { message = "Subscription cancelled successfully" });
return NotFound("Active subscription not found");
}
/// <summary>
/// Get all subscriptions for the current user
/// </summary>
/// <returns>List of active subscriptions</returns>
[HttpGet("current")]
[Authorize]
public async Task<ActionResult<List<PublisherSubscription>>> GetCurrentSubscriptions()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var subscriptions = await subs.GetAccountSubscriptionsAsync(currentUser.Id);
return subscriptions;
}
}

View File

@ -0,0 +1,181 @@
using DysonNetwork.Sphere.Account;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Post;
public class PublisherSubscriptionService(AppDatabase db, NotificationService nty)
{
/// <summary>
/// Checks if a subscription exists between the account and publisher
/// </summary>
/// <param name="accountId">The account ID</param>
/// <param name="publisherId">The publisher ID</param>
/// <returns>True if a subscription exists, false otherwise</returns>
public async Task<bool> SubscriptionExistsAsync(long accountId, long publisherId)
{
return await db.PublisherSubscriptions
.AnyAsync(ps => ps.AccountId == accountId &&
ps.PublisherId == publisherId &&
ps.Status == SubscriptionStatus.Active);
}
/// <summary>
/// Gets a subscription by account and publisher ID
/// </summary>
/// <param name="accountId">The account ID</param>
/// <param name="publisherId">The publisher ID</param>
/// <returns>The subscription or null if not found</returns>
public async Task<PublisherSubscription?> GetSubscriptionAsync(long accountId, long publisherId)
{
return await db.PublisherSubscriptions
.Include(ps => ps.Publisher)
.FirstOrDefaultAsync(ps => ps.AccountId == accountId && ps.PublisherId == publisherId);
}
/// <summary>
/// Notifies all subscribers about a new post from a publisher
/// </summary>
/// <param name="post">The new post</param>
/// <returns>The number of subscribers notified</returns>
public async Task<int> NotifySubscribersPostAsync(Post post)
{
var subscribers = await db.PublisherSubscriptions
.Include(ps => ps.Account)
.Where(ps => ps.PublisherId == post.Publisher.Id &&
ps.Status == SubscriptionStatus.Active)
.ToListAsync();
if (subscribers.Count == 0)
return 0;
// Create notification data
var title = $"@{post.Publisher.Name} Posted";
var message = !string.IsNullOrEmpty(post.Title)
? post.Title
: (post.Content?.Length > 100
? string.Concat(post.Content.AsSpan(0, 97), "...")
: post.Content);
// Data to include with the notification
var data = new Dictionary<string, object>
{
{ "post_id", post.Id.ToString() },
{ "publisher_id", post.Publisher.Id.ToString() }
};
// Notify each subscriber
var notifiedCount = 0;
foreach (var subscription in subscribers)
{
try
{
await nty.SendNotification(
subscription.Account,
"posts.new",
title,
post.Description?.Length > 40 ? post.Description[..37] + "..." : post.Description,
message,
data
);
notifiedCount++;
}
catch (Exception)
{
// Log the error but continue with other notifications
// We don't want one failed notification to stop the others
}
}
return notifiedCount;
}
/// <summary>
/// Gets all active subscriptions for an account
/// </summary>
/// <param name="accountId">The account ID</param>
/// <returns>A list of active subscriptions</returns>
public async Task<List<PublisherSubscription>> GetAccountSubscriptionsAsync(long accountId)
{
return await db.PublisherSubscriptions
.Include(ps => ps.Publisher)
.Where(ps => ps.AccountId == accountId && ps.Status == SubscriptionStatus.Active)
.ToListAsync();
}
/// <summary>
/// Gets all active subscribers for a publisher
/// </summary>
/// <param name="publisherId">The publisher ID</param>
/// <returns>A list of active subscriptions</returns>
public async Task<List<PublisherSubscription>> GetPublisherSubscribersAsync(long publisherId)
{
return await db.PublisherSubscriptions
.Include(ps => ps.Account)
.Where(ps => ps.PublisherId == publisherId && ps.Status == SubscriptionStatus.Active)
.ToListAsync();
}
/// <summary>
/// Creates a new subscription between an account and a publisher
/// </summary>
/// <param name="accountId">The account ID</param>
/// <param name="publisherId">The publisher ID</param>
/// <param name="tier">Optional subscription tier</param>
/// <returns>The created subscription</returns>
public async Task<PublisherSubscription> CreateSubscriptionAsync(
long accountId,
long publisherId,
int tier = 0
)
{
// Check if a subscription already exists
var existingSubscription = await GetSubscriptionAsync(accountId, publisherId);
if (existingSubscription != null)
{
// If it exists but is not active, reactivate it
if (existingSubscription.Status == SubscriptionStatus.Active) return existingSubscription;
existingSubscription.Status = SubscriptionStatus.Active;
existingSubscription.Tier = tier;
await db.SaveChangesAsync();
return existingSubscription;
// If it's already active, just return it
}
// Create a new subscription
var subscription = new PublisherSubscription
{
AccountId = accountId,
PublisherId = publisherId,
Status = SubscriptionStatus.Active,
Tier = tier,
};
db.PublisherSubscriptions.Add(subscription);
await db.SaveChangesAsync();
return subscription;
}
/// <summary>
/// Cancels a subscription
/// </summary>
/// <param name="accountId">The account ID</param>
/// <param name="publisherId">The publisher ID</param>
/// <returns>True if the subscription was cancelled, false if it wasn't found</returns>
public async Task<bool> CancelSubscriptionAsync(long accountId, long publisherId)
{
var subscription = await GetSubscriptionAsync(accountId, publisherId);
if (subscription is not { Status: SubscriptionStatus.Active })
{
return false;
}
subscription.Status = SubscriptionStatus.Cancelled;
await db.SaveChangesAsync();
return true;
}
}

View File

@ -157,6 +157,7 @@ builder.Services.AddScoped<NotificationService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<FileService>();
builder.Services.AddScoped<PublisherService>();
builder.Services.AddScoped<PublisherSubscriptionService>();
builder.Services.AddScoped<ActivityService>();
builder.Services.AddScoped<ActivityReaderService>();
builder.Services.AddScoped<PostService>();

View File

@ -71,21 +71,21 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
{
// Try to get from cache first
string cacheKey = $"StickerLookup_{identifier}";
identifier = identifier.ToLower();
// Try to get from the cache first
var cacheKey = $"StickerLookup_{identifier}";
if (cache.TryGetValue(cacheKey, out Sticker? cachedSticker))
{
return cachedSticker;
}
// If not in cache, fetch from database
// If not in cache, fetch from the database
IQueryable<Sticker> query = db.Stickers
.Include(e => e.Pack)
.Include(e => e.Image);
query = Guid.TryParse(identifier, out var guid)
? query.Where(e => e.Id == guid)
: query.Where(e => e.Pack.Prefix + e.Slug == identifier);
: query.Where(e => (e.Pack.Prefix + e.Slug).ToLower() == identifier);
var sticker = await query.FirstOrDefaultAsync();