feat: stream tickets

This commit is contained in:
2024-08-19 10:10:59 +01:00
parent 3148b2cf6f
commit 015b75f894
9 changed files with 654 additions and 13 deletions

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace NostrStreamer.Database.Configuration;
public class StreamTicketsConfiguration : IEntityTypeConfiguration<StreamTickets>
{
public void Configure(EntityTypeBuilder<StreamTickets> 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);
}
}

View File

@ -32,7 +32,9 @@ public class UserStreamConfiguration : IEntityTypeConfiguration<UserStream>
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<UserStream>
.WithMany(a => a.Streams)
.HasForeignKey(a => a.PubKey);
}
}
}

View File

@ -30,8 +30,9 @@ public class Payment
public enum PaymentType
{
Topup = 0,
TopUp = 0,
Zap = 1,
Credit = 2,
Withdrawal = 3,
AdmissionFee = 4,
}

View File

@ -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();
}

View File

@ -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; }
/// <summary>
/// Nostr Event for this stream
/// </summary>
public string Event { get; set; } = null!;
/// <summary>
/// URL of auto-generated thumbnail
/// </summary>
@ -32,24 +32,29 @@ public class UserStream
/// Publisher edge IP
/// </summary>
public string EdgeIp { get; set; } = null!;
/// <summary>
/// Publisher edge client id
/// </summary>
public string ForwardClientId { get; set; } = null!;
public DateTime LastSegment { get; set; } = DateTime.UtcNow;
/// <summary>
/// Total sats charged during this stream
/// </summary>
public decimal MilliSatsCollected { get; set; }
/// <summary>
/// Total seconds produced in HLS segments
/// </summary>
public decimal Length { get; set; }
/// <summary>
/// Cost to view stream, tickets in <see cref="StreamTickets"/>
/// </summary>
public decimal? AdmissionCost { get; set; }
public List<UserStreamGuest> Guests { get; init; } = new();
public List<UserStreamRecording> Recordings { get; init; } = new();

View File

@ -0,0 +1,505 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("App")
.IsRequired()
.HasColumnType("text");
b.Property<List<string>>("Capabilities")
.IsRequired()
.HasColumnType("text[]");
b.Property<int>("Cost")
.HasColumnType("integer");
b.Property<string>("Forward")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("App")
.IsUnique();
b.ToTable("Endpoints");
});
modelBuilder.Entity("NostrStreamer.Database.Payment", b =>
{
b.Property<string>("PaymentHash")
.HasColumnType("text");
b.Property<decimal>("Amount")
.HasColumnType("numeric(20,0)");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Fee")
.HasColumnType("numeric(20,0)");
b.Property<string>("Invoice")
.HasColumnType("text");
b.Property<bool>("IsPaid")
.HasColumnType("boolean");
b.Property<string>("Nostr")
.HasColumnType("text");
b.Property<string>("PubKey")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("PaymentHash");
b.HasIndex("PubKey");
b.ToTable("Payments");
});
modelBuilder.Entity("NostrStreamer.Database.PushSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("LastUsed")
.HasColumnType("timestamp with time zone");
b.Property<string>("Pubkey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Scope")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PushSubscriptions");
});
modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("SubscriberPubkey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("Token")
.HasColumnType("uuid");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("StreamTickets");
});
modelBuilder.Entity("NostrStreamer.Database.User", b =>
{
b.Property<string>("PubKey")
.HasColumnType("text");
b.Property<long>("Balance")
.HasColumnType("bigint");
b.Property<string>("ContentWarning")
.HasColumnType("text");
b.Property<string>("Goal")
.HasColumnType("text");
b.Property<string>("Image")
.HasColumnType("text");
b.Property<string>("StreamKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.HasColumnType("text");
b.Property<string>("Tags")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<DateTime?>("TosAccepted")
.HasColumnType("timestamp with time zone");
b.Property<uint>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("PubKey");
b.ToTable("Users");
});
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("AdmissionCost")
.HasColumnType("numeric");
b.Property<string>("EdgeIp")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("EndpointId")
.HasColumnType("uuid");
b.Property<DateTime?>("Ends")
.HasColumnType("timestamp with time zone");
b.Property<string>("Event")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ForwardClientId")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("LastSegment")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Length")
.HasColumnType("numeric");
b.Property<decimal>("MilliSatsCollected")
.HasColumnType("numeric");
b.Property<string>("PubKey")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Starts")
.HasColumnType("timestamp with time zone");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("StreamId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Thumbnail")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("EndpointId");
b.HasIndex("PubKey");
b.ToTable("Streams");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("TakenByPubkey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("Clips");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Target")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UserPubkey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserPubkey");
b.ToTable("Forwards");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("PubKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Relay")
.HasColumnType("text");
b.Property<string>("Role")
.HasColumnType("text");
b.Property<string>("Sig")
.HasColumnType("text");
b.Property<Guid>("StreamId")
.HasColumnType("uuid");
b.Property<decimal>("ZapSplit")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("StreamId", "PubKey")
.IsUnique();
b.ToTable("Guests");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<double>("Duration")
.HasColumnType("double precision");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("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
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NostrStreamer.Migrations
{
/// <inheritdoc />
public partial class StreamTickets : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "AdmissionCost",
table: "Streams",
type: "numeric",
nullable: true);
migrationBuilder.CreateTable(
name: "StreamTickets",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserStreamId = table.Column<Guid>(type: "uuid", nullable: false),
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Token = table.Column<Guid>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StreamTickets");
migrationBuilder.DropColumn(
name: "AdmissionCost",
table: "Streams");
}
}
}

View File

@ -157,6 +157,28 @@ namespace NostrStreamer.Migrations
b.ToTable("PushSubscriptionTargets");
});
modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("Token")
.HasColumnType("uuid");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("StreamTickets");
});
modelBuilder.Entity("NostrStreamer.Database.User", b =>
{
b.Property<string>("PubKey")
@ -213,6 +235,9 @@ namespace NostrStreamer.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("AdmissionCost")
.HasColumnType("numeric");
b.Property<string>("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")

View File

@ -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();