From 015b75f8943dc6df5d76359ed1a24b22a3a29963 Mon Sep 17 00:00:00 2001 From: kieran Date: Mon, 19 Aug 2024 10:10:59 +0100 Subject: [PATCH] feat: stream tickets --- .../StreamTicketsConfiguration.cs | 22 + .../Configuration/UserStreamConfiguration.cs | 6 +- NostrStreamer/Database/Payment.cs | 3 +- NostrStreamer/Database/StreamTickets.cs | 13 + NostrStreamer/Database/UserStream.cs | 23 +- .../20240819091025_StreamTickets.Designer.cs | 505 ++++++++++++++++++ .../20240819091025_StreamTickets.cs | 57 ++ .../StreamerContextModelSnapshot.cs | 36 ++ NostrStreamer/Services/UserService.cs | 2 +- 9 files changed, 654 insertions(+), 13 deletions(-) create mode 100644 NostrStreamer/Database/Configuration/StreamTicketsConfiguration.cs create mode 100644 NostrStreamer/Database/StreamTickets.cs create mode 100644 NostrStreamer/Migrations/20240819091025_StreamTickets.Designer.cs create mode 100644 NostrStreamer/Migrations/20240819091025_StreamTickets.cs diff --git a/NostrStreamer/Database/Configuration/StreamTicketsConfiguration.cs b/NostrStreamer/Database/Configuration/StreamTicketsConfiguration.cs new file mode 100644 index 0000000..0399d77 --- /dev/null +++ b/NostrStreamer/Database/Configuration/StreamTicketsConfiguration.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace NostrStreamer.Database.Configuration; + +public class StreamTicketsConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(a => a.Id); + + builder.Property(a => a.Created) + .IsRequired(); + + builder.Property(a => a.Token) + .IsRequired(); + + builder.HasOne(a => a.UserStream) + .WithMany() + .HasForeignKey(a => a.UserStreamId); + } +} \ No newline at end of file diff --git a/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs b/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs index 7cd3881..d85da20 100644 --- a/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs +++ b/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs @@ -32,7 +32,9 @@ public class UserStreamConfiguration : IEntityTypeConfiguration builder.Property(a => a.LastSegment) .IsRequired(); - + + builder.Property(a => a.AdmissionCost); + builder.HasOne(a => a.Endpoint) .WithMany() .HasForeignKey(a => a.EndpointId); @@ -41,4 +43,4 @@ public class UserStreamConfiguration : IEntityTypeConfiguration .WithMany(a => a.Streams) .HasForeignKey(a => a.PubKey); } -} +} \ No newline at end of file diff --git a/NostrStreamer/Database/Payment.cs b/NostrStreamer/Database/Payment.cs index 5c20ae0..53ead0c 100644 --- a/NostrStreamer/Database/Payment.cs +++ b/NostrStreamer/Database/Payment.cs @@ -30,8 +30,9 @@ public class Payment public enum PaymentType { - Topup = 0, + TopUp = 0, Zap = 1, Credit = 2, Withdrawal = 3, + AdmissionFee = 4, } \ No newline at end of file diff --git a/NostrStreamer/Database/StreamTickets.cs b/NostrStreamer/Database/StreamTickets.cs new file mode 100644 index 0000000..b678477 --- /dev/null +++ b/NostrStreamer/Database/StreamTickets.cs @@ -0,0 +1,13 @@ +namespace NostrStreamer.Database; + +public class StreamTickets +{ + public Guid Id { get; init; } = Guid.NewGuid(); + + public Guid UserStreamId { get; init; } + public UserStream UserStream { get; init; } = null!; + + public DateTime Created { get; init; } = DateTime.UtcNow; + + public Guid Token { get; init; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/NostrStreamer/Database/UserStream.cs b/NostrStreamer/Database/UserStream.cs index 3df9a8a..1f215f7 100644 --- a/NostrStreamer/Database/UserStream.cs +++ b/NostrStreamer/Database/UserStream.cs @@ -8,18 +8,18 @@ public class UserStream public User User { get; init; } = null!; public string StreamId { get; init; } = null!; - + public DateTime Starts { get; init; } = DateTime.UtcNow; - + public DateTime? Ends { get; set; } - + public UserStreamState State { get; set; } - + /// /// Nostr Event for this stream /// public string Event { get; set; } = null!; - + /// /// URL of auto-generated thumbnail /// @@ -32,24 +32,29 @@ public class UserStream /// Publisher edge IP /// public string EdgeIp { get; set; } = null!; - + /// /// Publisher edge client id /// public string ForwardClientId { get; set; } = null!; public DateTime LastSegment { get; set; } = DateTime.UtcNow; - + /// /// Total sats charged during this stream /// public decimal MilliSatsCollected { get; set; } - + /// /// Total seconds produced in HLS segments /// public decimal Length { get; set; } - + + /// + /// Cost to view stream, tickets in + /// + public decimal? AdmissionCost { get; set; } + public List Guests { get; init; } = new(); public List Recordings { get; init; } = new(); diff --git a/NostrStreamer/Migrations/20240819091025_StreamTickets.Designer.cs b/NostrStreamer/Migrations/20240819091025_StreamTickets.Designer.cs new file mode 100644 index 0000000..5c741fb --- /dev/null +++ b/NostrStreamer/Migrations/20240819091025_StreamTickets.Designer.cs @@ -0,0 +1,505 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NostrStreamer.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + [DbContext(typeof(StreamerContext))] + [Migration("20240819091025_StreamTickets")] + partial class StreamTickets + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NostrStreamer.Database.IngestEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("App") + .IsRequired() + .HasColumnType("text"); + + b.Property>("Capabilities") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Cost") + .HasColumnType("integer"); + + b.Property("Forward") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("App") + .IsUnique(); + + b.ToTable("Endpoints"); + }); + + modelBuilder.Entity("NostrStreamer.Database.Payment", b => + { + b.Property("PaymentHash") + .HasColumnType("text"); + + b.Property("Amount") + .HasColumnType("numeric(20,0)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Fee") + .HasColumnType("numeric(20,0)"); + + b.Property("Invoice") + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("Nostr") + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("PaymentHash"); + + b.HasIndex("PubKey"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastUsed") + .HasColumnType("timestamp with time zone"); + + b.Property("Pubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SubscriberPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TargetPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TargetPubkey"); + + b.HasIndex("SubscriberPubkey", "TargetPubkey") + .IsUnique(); + + b.ToTable("PushSubscriptionTargets"); + }); + + modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasColumnType("uuid"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("StreamTickets"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Property("PubKey") + .HasColumnType("text"); + + b.Property("Balance") + .HasColumnType("bigint"); + + b.Property("ContentWarning") + .HasColumnType("text"); + + b.Property("Goal") + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TosAccepted") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("PubKey"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdmissionCost") + .HasColumnType("numeric"); + + b.Property("EdgeIp") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndpointId") + .HasColumnType("uuid"); + + b.Property("Ends") + .HasColumnType("timestamp with time zone"); + + b.Property("Event") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForwardClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastSegment") + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasColumnType("numeric"); + + b.Property("MilliSatsCollected") + .HasColumnType("numeric"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Starts") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StreamId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId"); + + b.HasIndex("PubKey"); + + b.ToTable("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TakenByPubkey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Clips"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserPubkey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserPubkey"); + + b.ToTable("Forwards"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Relay") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("Sig") + .HasColumnType("text"); + + b.Property("StreamId") + .HasColumnType("uuid"); + + b.Property("ZapSplit") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("StreamId", "PubKey") + .IsUnique(); + + b.ToTable("Guests"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Duration") + .HasColumnType("double precision"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Recordings"); + }); + + modelBuilder.Entity("NostrStreamer.Database.Payment", b => + { + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Payments") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithMany() + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserStream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Streams") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithMany() + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserStream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Forwards") + .HasForeignKey("UserPubkey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "Stream") + .WithMany("Guests") + .HasForeignKey("StreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "Stream") + .WithMany("Recordings") + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Navigation("Forwards"); + + b.Navigation("Payments"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Navigation("Guests"); + + b.Navigation("Recordings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/NostrStreamer/Migrations/20240819091025_StreamTickets.cs b/NostrStreamer/Migrations/20240819091025_StreamTickets.cs new file mode 100644 index 0000000..391fc35 --- /dev/null +++ b/NostrStreamer/Migrations/20240819091025_StreamTickets.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + /// + public partial class StreamTickets : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AdmissionCost", + table: "Streams", + type: "numeric", + nullable: true); + + migrationBuilder.CreateTable( + name: "StreamTickets", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserStreamId = table.Column(type: "uuid", nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + Token = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StreamTickets", x => x.Id); + table.ForeignKey( + name: "FK_StreamTickets_Streams_UserStreamId", + column: x => x.UserStreamId, + principalTable: "Streams", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_StreamTickets_UserStreamId", + table: "StreamTickets", + column: "UserStreamId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StreamTickets"); + + migrationBuilder.DropColumn( + name: "AdmissionCost", + table: "Streams"); + } + } +} diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs index ae483ce..b2940da 100644 --- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs +++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs @@ -157,6 +157,28 @@ namespace NostrStreamer.Migrations b.ToTable("PushSubscriptionTargets"); }); + modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasColumnType("uuid"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("StreamTickets"); + }); + modelBuilder.Entity("NostrStreamer.Database.User", b => { b.Property("PubKey") @@ -213,6 +235,9 @@ namespace NostrStreamer.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AdmissionCost") + .HasColumnType("numeric"); + b.Property("EdgeIp") .IsRequired() .HasColumnType("text"); @@ -388,6 +413,17 @@ namespace NostrStreamer.Migrations b.Navigation("User"); }); + modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithMany() + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserStream"); + }); + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => { b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint") diff --git a/NostrStreamer/Services/UserService.cs b/NostrStreamer/Services/UserService.cs index 8969d3a..4b8accf 100644 --- a/NostrStreamer/Services/UserService.cs +++ b/NostrStreamer/Services/UserService.cs @@ -73,7 +73,7 @@ public class UserService Invoice = invoice.PaymentRequest, PaymentHash = invoice.RHash.ToByteArray().ToHex(), Nostr = nostr, - Type = string.IsNullOrEmpty(nostr) ? PaymentType.Topup : PaymentType.Zap + Type = string.IsNullOrEmpty(nostr) ? PaymentType.TopUp : PaymentType.Zap }); await _db.SaveChangesAsync();