diff --git a/NostrStreamer/Database/UserStream.cs b/NostrStreamer/Database/UserStream.cs
index 6317efd..f166030 100644
--- a/NostrStreamer/Database/UserStream.cs
+++ b/NostrStreamer/Database/UserStream.cs
@@ -39,6 +39,12 @@ public class UserStream
public string ForwardClientId { get; set; } = null!;
public DateTime LastSegment { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Total sats charged during this stream
+ ///
+ public decimal MilliSatsCollected { get; set; }
+
public List Guests { get; init; } = new();
}
diff --git a/NostrStreamer/Migrations/20240221202747_TrackSatsConsumed.Designer.cs b/NostrStreamer/Migrations/20240221202747_TrackSatsConsumed.Designer.cs
new file mode 100644
index 0000000..e142ea6
--- /dev/null
+++ b/NostrStreamer/Migrations/20240221202747_TrackSatsConsumed.Designer.cs
@@ -0,0 +1,461 @@
+//
+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("20240221202747_TrackSatsConsumed")]
+ partial class TrackSatsConsumed
+ {
+ ///
+ 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("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.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("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("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.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()
+ .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");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/NostrStreamer/Migrations/20240221202747_TrackSatsConsumed.cs b/NostrStreamer/Migrations/20240221202747_TrackSatsConsumed.cs
new file mode 100644
index 0000000..0b66052
--- /dev/null
+++ b/NostrStreamer/Migrations/20240221202747_TrackSatsConsumed.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace NostrStreamer.Migrations
+{
+ ///
+ public partial class TrackSatsConsumed : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "MilliSatsCollected",
+ table: "Streams",
+ type: "numeric",
+ nullable: false,
+ defaultValue: 0m);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "MilliSatsCollected",
+ table: "Streams");
+ }
+ }
+}
diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs
index d07280d..5c9b836 100644
--- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs
+++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs
@@ -225,6 +225,9 @@ namespace NostrStreamer.Migrations
b.Property("LastSegment")
.HasColumnType("timestamp with time zone");
+ b.Property("MilliSatsCollected")
+ .HasColumnType("numeric");
+
b.Property("PubKey")
.IsRequired()
.HasColumnType("text");
diff --git a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs
index d4e7ea3..b9d06cd 100644
--- a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs
+++ b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs
@@ -21,7 +21,8 @@ public class NostrStreamManager : IStreamManager
private readonly NostrServicesClient _nostrApi;
private readonly IDataProtectionProvider _dataProtectionProvider;
- public NostrStreamManager(ILogger logger, StreamManagerContext context, IServiceProvider serviceProvider)
+ public NostrStreamManager(ILogger logger, StreamManagerContext context,
+ IServiceProvider serviceProvider)
{
_logger = logger;
_context = context;
@@ -87,10 +88,12 @@ public class NostrStreamManager : IStreamManager
{
try
{
- var profile = await _nostrApi.Profile(_context.User.PubKey);
- var name = profile?.Name ?? NostrConverter.ToBech32(_context.User.PubKey, "npub");
+ var npub = NostrConverter.ToBech32(_context.User.PubKey, "npub")!;
+ var profile = await _nostrApi.Profile(npub);
+ var name = profile?.Name ?? npub;
var id = ev.ToIdentifier();
- await _webhook.SendMessage(_config.DiscordLiveWebhook, $"{name} went live!\nhttps://zap.stream/{id.ToBech32()}");
+ await _webhook.SendMessage(_config.DiscordLiveWebhook,
+ $"{name} went live!\nhttps://zap.stream/{id.ToBech32()}");
}
catch (Exception ex)
{
@@ -112,12 +115,16 @@ public class NostrStreamManager : IStreamManager
var cost = (long)Math.Ceiling(_context.UserStream.Endpoint.Cost * (duration / 60d));
if (cost > 0)
{
- await _context.Db.Users
- .Where(a => a.PubKey == _context.User.PubKey)
- .ExecuteUpdateAsync(o => o.SetProperty(v => v.Balance, v => v.Balance - cost));
+ await _context.Db.Streams
+ .Include(a => a.User)
+ .Where(a => a.PubKey == _context.User.PubKey && a.Id == _context.UserStream.Id)
+ .ExecuteUpdateAsync(o =>
+ o.SetProperty(v => v.User.Balance, v => v.User.Balance - cost)
+ .SetProperty(v => v.MilliSatsCollected, v => v.MilliSatsCollected + cost));
}
- _logger.LogInformation("Stream consumed {n} seconds for {pubkey} costing {cost:#,##0} milli-sats", duration, _context.User.PubKey,
+ _logger.LogInformation("Stream produced {n} seconds for {pubkey} costing {cost:#,##0} milli-sats", duration,
+ _context.User.PubKey,
cost);
if (_context.User.Balance >= balanceAlertThreshold && _context.User.Balance - cost < balanceAlertThreshold)
@@ -168,7 +175,8 @@ public class NostrStreamManager : IStreamManager
UserStreamId = _context.UserStream.Id,
Url = result.Result.ToString(),
Duration = result.Duration,
- Timestamp = DateTime.UtcNow //DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime
+ Timestamp = DateTime
+ .UtcNow //DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime
});
await _context.Db.SaveChangesAsync();
@@ -212,7 +220,8 @@ public class NostrStreamManager : IStreamManager
if (newEvent != default && int.TryParse(oldViewers, out var a) && int.TryParse(newViewers, out var b) && a != b)
{
await _context.Db.Streams.Where(a => a.Id == _context.UserStream.Id)
- .ExecuteUpdateAsync(o => o.SetProperty(v => v.Event, JsonConvert.SerializeObject(newEvent, NostrSerializer.Settings)));
+ .ExecuteUpdateAsync(o =>
+ o.SetProperty(v => v.Event, JsonConvert.SerializeObject(newEvent, NostrSerializer.Settings)));
_eventBuilder.BroadcastEvent(newEvent);
}
@@ -233,4 +242,4 @@ public class NostrStreamManager : IStreamManager
_eventBuilder.BroadcastEvent(ev);
return ev;
}
-}
+}
\ No newline at end of file