✨ Organization publishers, subscriptions to publishers
This commit is contained in:
parent
b20bc3c443
commit
b275f06061
@ -44,6 +44,7 @@ public class AppDatabase(
|
|||||||
|
|
||||||
public DbSet<Post.Publisher> Publishers { get; set; }
|
public DbSet<Post.Publisher> Publishers { get; set; }
|
||||||
public DbSet<Post.PublisherMember> PublisherMembers { 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.Post> Posts { get; set; }
|
||||||
public DbSet<Post.PostReaction> PostReactions { get; set; }
|
public DbSet<Post.PostReaction> PostReactions { get; set; }
|
||||||
public DbSet<Post.PostTag> PostTags { get; set; }
|
public DbSet<Post.PostTag> PostTags { get; set; }
|
||||||
@ -148,6 +149,16 @@ public class AppDatabase(
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(pm => pm.AccountId)
|
.HasForeignKey(pm => pm.AccountId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.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>()
|
modelBuilder.Entity<Post.Post>()
|
||||||
.HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content })
|
.HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content })
|
||||||
|
2763
DysonNetwork.Sphere/Migrations/20250512132355_AddPublisherSubscription.Designer.cs
generated
Normal file
2763
DysonNetwork.Sphere/Migrations/20250512132355_AddPublisherSubscription.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2777
DysonNetwork.Sphere/Migrations/20250512133934_PublisherWithOrganization.Designer.cs
generated
Normal file
2777
DysonNetwork.Sphere/Migrations/20250512133934_PublisherWithOrganization.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1575,6 +1575,10 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasColumnName("publisher_type");
|
.HasColumnName("publisher_type");
|
||||||
|
|
||||||
|
b.Property<long?>("RealmId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("realm_id");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
@ -1595,6 +1599,9 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.HasIndex("PictureId")
|
b.HasIndex("PictureId")
|
||||||
.HasDatabaseName("ix_publishers_picture_id");
|
.HasDatabaseName("ix_publishers_picture_id");
|
||||||
|
|
||||||
|
b.HasIndex("RealmId")
|
||||||
|
.HasDatabaseName("ix_publishers_realm_id");
|
||||||
|
|
||||||
b.ToTable("publishers", (string)null);
|
b.ToTable("publishers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1637,6 +1644,53 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.ToTable("publisher_members", (string)null);
|
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 =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@ -2442,11 +2496,18 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasForeignKey("PictureId")
|
.HasForeignKey("PictureId")
|
||||||
.HasConstraintName("fk_publishers_files_picture_id");
|
.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("Account");
|
||||||
|
|
||||||
b.Navigation("Background");
|
b.Navigation("Background");
|
||||||
|
|
||||||
b.Navigation("Picture");
|
b.Navigation("Picture");
|
||||||
|
|
||||||
|
b.Navigation("Realm");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Post.PublisherMember", b =>
|
||||||
@ -2470,6 +2531,27 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.Navigation("Publisher");
|
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 =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||||
@ -2676,6 +2758,8 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.Navigation("Members");
|
b.Navigation("Members");
|
||||||
|
|
||||||
b.Navigation("Posts");
|
b.Navigation("Posts");
|
||||||
|
|
||||||
|
b.Navigation("Subscriptions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b =>
|
||||||
|
@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Post;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/posts")]
|
[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]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20)
|
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);
|
return BadRequest(err.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = factory.CreateScope();
|
||||||
|
var subs = scope.ServiceProvider.GetRequiredService<PublisherSubscriptionService>();
|
||||||
|
await subs.NotifySubscribersPostAsync(post);
|
||||||
|
});
|
||||||
|
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,9 +30,14 @@ public class Publisher : ModelBase
|
|||||||
[JsonIgnore] public ICollection<Post> Posts { get; set; } = new List<Post>();
|
[JsonIgnore] public ICollection<Post> Posts { get; set; } = new List<Post>();
|
||||||
[JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>();
|
[JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>();
|
||||||
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
|
[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; }
|
public long? AccountId { get; set; }
|
||||||
[JsonIgnore] public Account.Account? Account { get; set; }
|
[JsonIgnore] public Account.Account? Account { get; set; }
|
||||||
|
public long? RealmId { get; set; }
|
||||||
|
[JsonIgnore] public Realm.Realm? Realm { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PublisherMemberRole
|
public enum PublisherMemberRole
|
||||||
@ -52,4 +57,24 @@ public class PublisherMember : ModelBase
|
|||||||
|
|
||||||
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
|
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
|
||||||
public Instant? JoinedAt { get; set; }
|
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;
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
|
using DysonNetwork.Sphere.Realm;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -168,11 +169,11 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
|||||||
public string? PictureId { get; set; }
|
public string? PictureId { get; set; }
|
||||||
public string? BackgroundId { get; set; }
|
public string? BackgroundId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("individual")]
|
[HttpPost("individual")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "publishers.create")]
|
[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();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
@ -212,6 +213,55 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
|||||||
|
|
||||||
return Ok(publisher);
|
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}")]
|
[HttpPatch("{name}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
@ -45,7 +45,44 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
return publisher;
|
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
|
public class PublisherStats
|
||||||
{
|
{
|
||||||
@ -54,19 +91,20 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
public int StickersCreated { get; set; }
|
public int StickersCreated { get; set; }
|
||||||
public int UpvoteReceived { get; set; }
|
public int UpvoteReceived { get; set; }
|
||||||
public int DownvoteReceived { get; set; }
|
public int DownvoteReceived { get; set; }
|
||||||
|
public int SubscribersCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string PublisherStatsCacheKey = "PublisherStats_{0}";
|
private const string PublisherStatsCacheKey = "PublisherStats_{0}";
|
||||||
|
|
||||||
public async Task<PublisherStats?> GetPublisherStats(string name)
|
public async Task<PublisherStats?> GetPublisherStats(string name)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(PublisherStatsCacheKey, name);
|
var cacheKey = string.Format(PublisherStatsCacheKey, name);
|
||||||
if (cache.TryGetValue(cacheKey, out PublisherStats? stats))
|
if (cache.TryGetValue(cacheKey, out PublisherStats? stats))
|
||||||
return stats;
|
return stats;
|
||||||
|
|
||||||
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
||||||
if (publisher is null) return null;
|
if (publisher is null) return null;
|
||||||
|
|
||||||
var postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync();
|
var postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync();
|
||||||
var postsUpvotes = await db.PostReactions
|
var postsUpvotes = await db.PostReactions
|
||||||
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Positive)
|
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Positive)
|
||||||
@ -74,21 +112,25 @@ public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache
|
|||||||
var postsDownvotes = await db.PostReactions
|
var postsDownvotes = await db.PostReactions
|
||||||
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Negative)
|
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Negative)
|
||||||
.CountAsync();
|
.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 stickerPacksCount = stickerPacksId.Count;
|
||||||
|
|
||||||
var stickersCount = await db.Stickers.Where(e => stickerPacksId.Contains(e.PackId)).CountAsync();
|
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
|
stats = new PublisherStats
|
||||||
{
|
{
|
||||||
PostsCreated = postsCount,
|
PostsCreated = postsCount,
|
||||||
StickerPacksCreated = stickerPacksCount,
|
StickerPacksCreated = stickerPacksCount,
|
||||||
StickersCreated = stickersCount,
|
StickersCreated = stickersCount,
|
||||||
UpvoteReceived = postsUpvotes,
|
UpvoteReceived = postsUpvotes,
|
||||||
DownvoteReceived = postsDownvotes
|
DownvoteReceived = postsDownvotes,
|
||||||
|
SubscribersCount = subscribersCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.Set(cacheKey, stats, TimeSpan.FromMinutes(5));
|
cache.Set(cacheKey, stats, TimeSpan.FromMinutes(5));
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
126
DysonNetwork.Sphere/Post/PublisherSubscriptionController.cs
Normal file
126
DysonNetwork.Sphere/Post/PublisherSubscriptionController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
181
DysonNetwork.Sphere/Post/PublisherSubscriptionService.cs
Normal file
181
DysonNetwork.Sphere/Post/PublisherSubscriptionService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -157,6 +157,7 @@ builder.Services.AddScoped<NotificationService>();
|
|||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
builder.Services.AddScoped<FileService>();
|
builder.Services.AddScoped<FileService>();
|
||||||
builder.Services.AddScoped<PublisherService>();
|
builder.Services.AddScoped<PublisherService>();
|
||||||
|
builder.Services.AddScoped<PublisherSubscriptionService>();
|
||||||
builder.Services.AddScoped<ActivityService>();
|
builder.Services.AddScoped<ActivityService>();
|
||||||
builder.Services.AddScoped<ActivityReaderService>();
|
builder.Services.AddScoped<ActivityReaderService>();
|
||||||
builder.Services.AddScoped<PostService>();
|
builder.Services.AddScoped<PostService>();
|
||||||
|
@ -71,21 +71,21 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
|
|||||||
|
|
||||||
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
|
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)
|
||||||
{
|
{
|
||||||
// Try to get from cache first
|
identifier = identifier.ToLower();
|
||||||
string cacheKey = $"StickerLookup_{identifier}";
|
// Try to get from the cache first
|
||||||
|
var cacheKey = $"StickerLookup_{identifier}";
|
||||||
if (cache.TryGetValue(cacheKey, out Sticker? cachedSticker))
|
if (cache.TryGetValue(cacheKey, out Sticker? cachedSticker))
|
||||||
{
|
{
|
||||||
return cachedSticker;
|
return cachedSticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not in cache, fetch from database
|
// If not in cache, fetch from the database
|
||||||
IQueryable<Sticker> query = db.Stickers
|
IQueryable<Sticker> query = db.Stickers
|
||||||
.Include(e => e.Pack)
|
.Include(e => e.Pack)
|
||||||
.Include(e => e.Image);
|
.Include(e => e.Image);
|
||||||
|
|
||||||
query = Guid.TryParse(identifier, out var guid)
|
query = Guid.TryParse(identifier, out var guid)
|
||||||
? query.Where(e => e.Id == 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();
|
var sticker = await query.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user