From 3cabc7ac16c8c76dabc0cd7ddb669e5c106e4637 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 30 May 2025 12:42:41 +0100 Subject: [PATCH] feat: restore default stream details --- NostrStreamer/ApiModel/Account.cs | 3 + NostrStreamer/ApiModel/PatchEvent.cs | 10 +- NostrStreamer/Controllers/NostrController.cs | 23 +- .../Configuration/UserConfiguration.cs | 11 +- NostrStreamer/Database/User.cs | 30 + NostrStreamer/Extensions.cs | 2 +- ...29205437_RestoreStreamDefaults.Designer.cs | 580 ++++++++++++++++++ .../20250529205437_RestoreStreamDefaults.cs | 78 +++ .../StreamerContextModelSnapshot.cs | 18 + .../StreamManager/StreamManagerFactory.cs | 9 +- NostrStreamer/Services/UserService.cs | 13 +- 11 files changed, 754 insertions(+), 23 deletions(-) create mode 100644 NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.Designer.cs create mode 100644 NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.cs diff --git a/NostrStreamer/ApiModel/Account.cs b/NostrStreamer/ApiModel/Account.cs index a4ee9e2..bfca50f 100644 --- a/NostrStreamer/ApiModel/Account.cs +++ b/NostrStreamer/ApiModel/Account.cs @@ -15,6 +15,9 @@ public class Account [JsonProperty("forwards")] public List Forwards { get; init; } = new(); + + [JsonProperty("details")] + public PatchEvent? Details { get; init; } } public class AccountEndpoint diff --git a/NostrStreamer/ApiModel/PatchEvent.cs b/NostrStreamer/ApiModel/PatchEvent.cs index 81f7136..ab42f8a 100644 --- a/NostrStreamer/ApiModel/PatchEvent.cs +++ b/NostrStreamer/ApiModel/PatchEvent.cs @@ -5,19 +5,19 @@ namespace NostrStreamer.ApiModel; public class PatchEvent { [JsonProperty("id")] - public Guid Id { get; init; } + public Guid? Id { get; init; } [JsonProperty("title")] - public string Title { get; init; } = null!; + public string? Title { get; init; } [JsonProperty("summary")] - public string Summary { get; init; } = null!; + public string? Summary { get; init; } [JsonProperty("image")] - public string Image { get; init; } = null!; + public string? Image { get; init; } [JsonProperty("tags")] - public string[] Tags { get; init; } = Array.Empty(); + public string[]? Tags { get; init; } = []; [JsonProperty("content_warning")] public string? ContentWarning { get; init; } diff --git a/NostrStreamer/Controllers/NostrController.cs b/NostrStreamer/Controllers/NostrController.cs index 042bd9f..0674e4a 100644 --- a/NostrStreamer/Controllers/NostrController.cs +++ b/NostrStreamer/Controllers/NostrController.cs @@ -82,7 +82,16 @@ public class NostrController : Controller { Id = a.Id, Name = a.Name - }).ToList() + }).ToList(), + Details = new() + { + Title = user.Title, + Summary = user.Summary, + Image = user.Image, + ContentWarning = user.ContentWarning, + Tags = user.Tags?.Split(","), + Goal = user.Goal, + } }; return Content(JsonConvert.SerializeObject(account, NostrSerializer.Settings), "application/json"); @@ -93,17 +102,7 @@ public class NostrController : Controller { var pubkey = GetPubKey(); if (string.IsNullOrEmpty(pubkey)) return Unauthorized(); - - try - { - await _userService.UpdateStreamInfo(pubkey, req); - var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey); - await streamManager.UpdateEvent(); - } - catch - { - //ignore - } + await _userService.SetStreamInfoDefaults(pubkey, req); return Accepted(); } diff --git a/NostrStreamer/Database/Configuration/UserConfiguration.cs b/NostrStreamer/Database/Configuration/UserConfiguration.cs index ab566ff..0a9e5ec 100644 --- a/NostrStreamer/Database/Configuration/UserConfiguration.cs +++ b/NostrStreamer/Database/Configuration/UserConfiguration.cs @@ -15,11 +15,18 @@ public class UserConfiguration : IEntityTypeConfiguration .IsRequired(); builder.Property(a => a.TosAccepted); - + builder.Property(a => a.Version) .IsRowVersion(); builder.Property(a => a.IsAdmin); builder.Property(a => a.IsBlocked); + + builder.Property(a => a.Title); + builder.Property(a => a.Summary); + builder.Property(a => a.Image); + builder.Property(a => a.ContentWarning); + builder.Property(a => a.Goal); + builder.Property(a => a.Tags); } -} +} \ No newline at end of file diff --git a/NostrStreamer/Database/User.cs b/NostrStreamer/Database/User.cs index a280d0c..2fab726 100644 --- a/NostrStreamer/Database/User.cs +++ b/NostrStreamer/Database/User.cs @@ -34,6 +34,36 @@ public class User /// public bool IsBlocked { get; set; } + /// + /// Stream title + /// + public string? Title { get; set; } + + /// + /// Stream summary + /// + public string? Summary { get; set; } + + /// + /// Stream cover image + /// + public string? Image { get; set; } + + /// + /// Comma seperated tags + /// + public string? Tags { get; set; } + + /// + /// Any content warning tag (NIP-36) + /// + public string? ContentWarning { get; set; } + + /// + /// Stream goal + /// + public string? Goal { get; set; } + public List Payments { get; init; } = new(); public List Streams { get; init; } = new(); public List Forwards { get; init; } = new(); diff --git a/NostrStreamer/Extensions.cs b/NostrStreamer/Extensions.cs index ad2254f..cdfb7af 100644 --- a/NostrStreamer/Extensions.cs +++ b/NostrStreamer/Extensions.cs @@ -160,7 +160,7 @@ public static class Extensions stream.Summary = ev.Summary; stream.Image = ev.Image; stream.ContentWarning = ev.ContentWarning; - stream.Tags = ev.Tags.Length > 0 ? string.Join(',', ev.Tags) : null; + stream.Tags = ev.Tags is { Length: > 0 } ? string.Join(",", ev.Tags) : null; stream.Goal = ev.Goal; } } diff --git a/NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.Designer.cs b/NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.Designer.cs new file mode 100644 index 0000000..4d88b3c --- /dev/null +++ b/NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.Designer.cs @@ -0,0 +1,580 @@ +// +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("20250529205437_RestoreStreamDefaults")] + partial class RestoreStreamDefaults + { + /// + 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("IsAdmin") + .HasColumnType("boolean"); + + b.Property("IsBlocked") + .HasColumnType("boolean"); + + 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("ContentWarning") + .HasColumnType("text"); + + 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("Goal") + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("LastSegment") + .IsRequired() + .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("Summary") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.Property("Title") + .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.UserStreamKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("StreamId") + .HasColumnType("uuid"); + + b.Property("UserPubkey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("StreamId") + .IsUnique(); + + b.HasIndex("UserPubkey"); + + b.ToTable("StreamKeys"); + }); + + 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"); + + 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.UserStreamKey", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithOne("StreamKey") + .HasForeignKey("NostrStreamer.Database.UserStreamKey", "StreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("StreamKeys") + .HasForeignKey("UserPubkey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserStream"); + }); + + 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("StreamKeys"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Navigation("Guests"); + + b.Navigation("Recordings"); + + b.Navigation("StreamKey"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.cs b/NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.cs new file mode 100644 index 0000000..13cc326 --- /dev/null +++ b/NostrStreamer/Migrations/20250529205437_RestoreStreamDefaults.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + /// + public partial class RestoreStreamDefaults : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentWarning", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Goal", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Image", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Summary", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Tags", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Title", + table: "Users", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContentWarning", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Goal", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Image", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Summary", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Tags", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Title", + table: "Users"); + } + } +} diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs index 6d0f5d7..eebb9fa 100644 --- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs +++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs @@ -187,6 +187,15 @@ namespace NostrStreamer.Migrations b.Property("Balance") .HasColumnType("bigint"); + b.Property("ContentWarning") + .HasColumnType("text"); + + b.Property("Goal") + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + b.Property("IsAdmin") .HasColumnType("boolean"); @@ -197,6 +206,15 @@ namespace NostrStreamer.Migrations .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"); diff --git a/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs b/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs index b7b2964..6137591 100644 --- a/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs +++ b/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs @@ -69,12 +69,17 @@ public class StreamManagerFactory State = UserStreamState.Live, EdgeIp = info.EdgeIp, ForwardClientId = info.ClientId, + Title = user.Title, + Summary = user.Summary, + Image = user.Image, + ContentWarning = user.ContentWarning, + Tags = user.Tags, + Goal = user.Goal }; // add new stream - if (existingLive == default) + if (existingLive == null) { - await stream.CopyLastStreamDetails(_db); var ev = _eventBuilder.CreateStreamEvent(stream); stream.Event = NostrJson.Serialize(ev) ?? ""; _db.Streams.Add(stream); diff --git a/NostrStreamer/Services/UserService.cs b/NostrStreamer/Services/UserService.cs index 324445c..7b34dfe 100644 --- a/NostrStreamer/Services/UserService.cs +++ b/NostrStreamer/Services/UserService.cs @@ -261,7 +261,18 @@ public class UserService .ExecuteUpdateAsync(o => o.SetProperty(v => v.Title, req.Title) .SetProperty(v => v.Summary, req.Summary) .SetProperty(v => v.Image, req.Image) - .SetProperty(v => v.Tags, req.Tags.Length > 0 ? string.Join(",", req.Tags) : null) + .SetProperty(v => v.Tags, req.Tags != null && req.Tags.Length > 0 ? string.Join(",", req.Tags) : null) + .SetProperty(v => v.ContentWarning, req.ContentWarning) + .SetProperty(v => v.Goal, req.Goal)); + } + + public async Task SetStreamInfoDefaults(string pubkey, PatchEvent req) + { + await _db.Users.Where(a => a.PubKey == pubkey) + .ExecuteUpdateAsync(o => o.SetProperty(v => v.Title, req.Title) + .SetProperty(v => v.Summary, req.Summary) + .SetProperty(v => v.Image, req.Image) + .SetProperty(v => v.Tags, req.Tags != null && req.Tags.Length > 0 ? string.Join(",", req.Tags) : null) .SetProperty(v => v.ContentWarning, req.ContentWarning) .SetProperty(v => v.Goal, req.Goal)); }