Push notifications
This commit is contained in:
24
NostrStreamer/ApiModel/PushMessage.cs
Normal file
24
NostrStreamer/ApiModel/PushMessage.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NostrStreamer.ApiModel;
|
||||||
|
|
||||||
|
public enum PushMessageType
|
||||||
|
{
|
||||||
|
StreamStarted = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PushMessage
|
||||||
|
{
|
||||||
|
[JsonProperty("type")]
|
||||||
|
public PushMessageType Type { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("pubkey")]
|
||||||
|
public string Pubkey { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("avatar")]
|
||||||
|
public string? Avatar { get; init; }
|
||||||
|
|
||||||
|
}
|
18
NostrStreamer/ApiModel/PushSubscriptionRequest.cs
Normal file
18
NostrStreamer/ApiModel/PushSubscriptionRequest.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NostrStreamer.ApiModel;
|
||||||
|
|
||||||
|
public class PushSubscriptionRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("endpoint")]
|
||||||
|
public string Endpoint { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("auth")]
|
||||||
|
public string Auth { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("key")]
|
||||||
|
public string Key { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("scope")]
|
||||||
|
public string Scope { get; init; } = null!;
|
||||||
|
}
|
@ -48,6 +48,18 @@ public class Config
|
|||||||
public TwitchApi Twitch { get; init; } = null!;
|
public TwitchApi Twitch { get; init; } = null!;
|
||||||
|
|
||||||
public string DataProtectionKeyPath { get; init; } = null!;
|
public string DataProtectionKeyPath { get; init; } = null!;
|
||||||
|
|
||||||
|
public VapidKeyDetails VapidKey { get; init; } = null!;
|
||||||
|
|
||||||
|
public string Redis { get; init; } = null!;
|
||||||
|
|
||||||
|
public Uri SnortApi { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VapidKeyDetails
|
||||||
|
{
|
||||||
|
public string PublicKey { get; init; } = null!;
|
||||||
|
public string PrivateKey { get; init; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwitchApi
|
public class TwitchApi
|
||||||
|
@ -5,11 +5,13 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Nostr.Client.Json;
|
using Nostr.Client.Json;
|
||||||
|
using Nostr.Client.Messages;
|
||||||
using NostrStreamer.ApiModel;
|
using NostrStreamer.ApiModel;
|
||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
using NostrStreamer.Services;
|
using NostrStreamer.Services;
|
||||||
using NostrStreamer.Services.Clips;
|
using NostrStreamer.Services.Clips;
|
||||||
using NostrStreamer.Services.StreamManager;
|
using NostrStreamer.Services.StreamManager;
|
||||||
|
using WebPush;
|
||||||
|
|
||||||
namespace NostrStreamer.Controllers;
|
namespace NostrStreamer.Controllers;
|
||||||
|
|
||||||
@ -23,15 +25,19 @@ public class NostrController : Controller
|
|||||||
private readonly StreamManagerFactory _streamManagerFactory;
|
private readonly StreamManagerFactory _streamManagerFactory;
|
||||||
private readonly UserService _userService;
|
private readonly UserService _userService;
|
||||||
private readonly IClipService _clipService;
|
private readonly IClipService _clipService;
|
||||||
|
private readonly ILogger<NostrController> _logger;
|
||||||
|
private readonly PushSender _pushSender;
|
||||||
|
|
||||||
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, UserService userService,
|
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, UserService userService,
|
||||||
IClipService clipService)
|
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_config = config;
|
_config = config;
|
||||||
_streamManagerFactory = streamManager;
|
_streamManagerFactory = streamManager;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_clipService = clipService;
|
_clipService = clipService;
|
||||||
|
_logger = logger;
|
||||||
|
_pushSender = pushSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("account")]
|
[HttpGet("account")]
|
||||||
@ -206,6 +212,145 @@ public class NostrController : Controller
|
|||||||
return File(fs, "video/mp4", enableRangeProcessing: true);
|
return File(fs, "video/mp4", enableRangeProcessing: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("notifications/info")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public IActionResult GetInfo()
|
||||||
|
{
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
publicKey = _config.VapidKey.PublicKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
[HttpGet("notifications/generate-keys")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public IActionResult GenerateKeys()
|
||||||
|
{
|
||||||
|
var vapidKeys = VapidHelper.GenerateVapidKeys();
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
publicKey = vapidKeys.PublicKey,
|
||||||
|
privateKey = vapidKeys.PrivateKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("notifications/test")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public void TestNotification([FromBody] NostrEvent ev)
|
||||||
|
{
|
||||||
|
_pushSender.Add(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
[HttpPost("notifications/register")]
|
||||||
|
public async Task<IActionResult> Register([FromBody] PushSubscriptionRequest sub)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sub.Endpoint) || string.IsNullOrEmpty(sub.Auth) || string.IsNullOrEmpty(sub.Key))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var pubkey = GetPubKey();
|
||||||
|
if (string.IsNullOrEmpty(pubkey))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var count = await _db.PushSubscriptions.CountAsync(a => a.Pubkey == pubkey);
|
||||||
|
if (count >= 5)
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
error = "Too many active subscriptions"
|
||||||
|
});
|
||||||
|
|
||||||
|
var existing = await _db.PushSubscriptions.FirstOrDefaultAsync(a => a.Key == sub.Key);
|
||||||
|
if (existing != default)
|
||||||
|
{
|
||||||
|
return Json(new {id = existing.Id});
|
||||||
|
}
|
||||||
|
|
||||||
|
var newId = Guid.NewGuid();
|
||||||
|
_db.PushSubscriptions.Add(new()
|
||||||
|
{
|
||||||
|
Id = newId,
|
||||||
|
Pubkey = pubkey,
|
||||||
|
Endpoint = sub.Endpoint,
|
||||||
|
Key = sub.Key,
|
||||||
|
Auth = sub.Auth,
|
||||||
|
Scope = sub.Scope
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("{pubkey} registered for notifications", pubkey);
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
id = newId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("notifications")]
|
||||||
|
public async Task<IActionResult> ListNotifications([FromQuery] string auth)
|
||||||
|
{
|
||||||
|
var userPubkey = GetPubKey();
|
||||||
|
if (string.IsNullOrEmpty(userPubkey))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var sub = await _db.PushSubscriptionTargets
|
||||||
|
.Join(_db.PushSubscriptions, a => a.SubscriberPubkey, b => b.Pubkey,
|
||||||
|
(a, b) => new {a.SubscriberPubkey, a.TargetPubkey, b.Auth})
|
||||||
|
.Where(a => a.SubscriberPubkey == userPubkey && a.Auth == auth)
|
||||||
|
.Select(a => a.TargetPubkey)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Json(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("notifications")]
|
||||||
|
public async Task<IActionResult> RegisterForStreamer([FromQuery] string pubkey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(pubkey)) return BadRequest();
|
||||||
|
|
||||||
|
var userPubkey = GetPubKey();
|
||||||
|
if (string.IsNullOrEmpty(userPubkey))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var sub = await _db.PushSubscriptionTargets
|
||||||
|
.CountAsync(a => a.SubscriberPubkey == userPubkey && a.TargetPubkey == pubkey);
|
||||||
|
|
||||||
|
if (sub > 0) return Ok();
|
||||||
|
|
||||||
|
_db.PushSubscriptionTargets.Add(new()
|
||||||
|
{
|
||||||
|
SubscriberPubkey = userPubkey,
|
||||||
|
TargetPubkey = pubkey
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("notifications")]
|
||||||
|
public async Task<IActionResult> UnregisterForStreamer([FromQuery] string pubkey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(pubkey)) return BadRequest();
|
||||||
|
|
||||||
|
var userPubkey = GetPubKey();
|
||||||
|
if (string.IsNullOrEmpty(userPubkey))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var sub = await _db.PushSubscriptionTargets
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(a => a.SubscriberPubkey == userPubkey && a.TargetPubkey == pubkey);
|
||||||
|
|
||||||
|
if (sub == default) return NotFound();
|
||||||
|
|
||||||
|
await _db.PushSubscriptionTargets
|
||||||
|
.Where(a => a.Id == sub.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<User?> GetUser()
|
private async Task<User?> GetUser()
|
||||||
{
|
{
|
||||||
var pk = GetPubKey();
|
var pk = GetPubKey();
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Database.Configuration;
|
||||||
|
|
||||||
|
public class PushSubscriptionConfiguration : IEntityTypeConfiguration<PushSubscription>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PushSubscription> builder)
|
||||||
|
{
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
builder.Property(a => a.Created)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.LastUsed)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.Pubkey)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.Endpoint)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.Auth)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.Key)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.Scope)
|
||||||
|
.IsRequired();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Database.Configuration;
|
||||||
|
|
||||||
|
public class PushSubscriptionTargetConfiguration : IEntityTypeConfiguration<PushSubscriptionTarget>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PushSubscriptionTarget> builder)
|
||||||
|
{
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
|
||||||
|
builder.Property(a => a.TargetPubkey)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.SubscriberPubkey)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.HasIndex(a => a.TargetPubkey);
|
||||||
|
|
||||||
|
builder.HasIndex(a => new {a.SubscriberPubkey, a.TargetPubkey})
|
||||||
|
.IsUnique();
|
||||||
|
}
|
||||||
|
}
|
23
NostrStreamer/Database/PushSubscription.cs
Normal file
23
NostrStreamer/Database/PushSubscription.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Database;
|
||||||
|
|
||||||
|
public class PushSubscription
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public DateTime Created { get; init; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public DateTime LastUsed { get; init; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string Pubkey { get; init; } = null!;
|
||||||
|
|
||||||
|
public string Endpoint { get; init; } = null!;
|
||||||
|
|
||||||
|
public string Key { get; init; } = null!;
|
||||||
|
|
||||||
|
public string Auth { get; init; } = null!;
|
||||||
|
|
||||||
|
public string Scope { get; init; } = null!;
|
||||||
|
}
|
14
NostrStreamer/Database/PushSubscriptionTarget.cs
Normal file
14
NostrStreamer/Database/PushSubscriptionTarget.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Database;
|
||||||
|
|
||||||
|
public class PushSubscriptionTarget
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string SubscriberPubkey { get; init; } = null!;
|
||||||
|
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string TargetPubkey { get; init; } = null!;
|
||||||
|
}
|
@ -33,4 +33,8 @@ public class StreamerContext : DbContext
|
|||||||
public DbSet<UserStreamForwards> Forwards => Set<UserStreamForwards>();
|
public DbSet<UserStreamForwards> Forwards => Set<UserStreamForwards>();
|
||||||
|
|
||||||
public DbSet<UserStreamClip> Clips => Set<UserStreamClip>();
|
public DbSet<UserStreamClip> Clips => Set<UserStreamClip>();
|
||||||
|
|
||||||
|
public DbSet<PushSubscription> PushSubscriptions => Set<PushSubscription>();
|
||||||
|
|
||||||
|
public DbSet<PushSubscriptionTarget> PushSubscriptionTargets => Set<PushSubscriptionTarget>();
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,17 @@ public static class Extensions
|
|||||||
|
|
||||||
return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
|
return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetHost(this NostrEvent ev)
|
||||||
|
{
|
||||||
|
var hostTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[2] == "host")?.AdditionalData[0];
|
||||||
|
if (!string.IsNullOrEmpty(hostTag))
|
||||||
|
{
|
||||||
|
return hostTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev.Pubkey!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Variant
|
public class Variant
|
||||||
|
458
NostrStreamer/Migrations/20231218112430_PushSubscriptions.Designer.cs
generated
Normal file
458
NostrStreamer/Migrations/20231218112430_PushSubscriptions.Designer.cs
generated
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
// <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("20231218112430_PushSubscriptions")]
|
||||||
|
partial class PushSubscriptions
|
||||||
|
{
|
||||||
|
/// <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<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.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<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<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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
NostrStreamer/Migrations/20231218112430_PushSubscriptions.cs
Normal file
67
NostrStreamer/Migrations/20231218112430_PushSubscriptions.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NostrStreamer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PushSubscriptions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PushSubscriptions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
LastUsed = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Pubkey = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
Endpoint = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Auth = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Scope = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PushSubscriptions", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PushSubscriptionTargets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
SubscriberPubkey = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
TargetPubkey = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PushSubscriptionTargets", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PushSubscriptionTargets_SubscriberPubkey_TargetPubkey",
|
||||||
|
table: "PushSubscriptionTargets",
|
||||||
|
columns: new[] { "SubscriberPubkey", "TargetPubkey" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PushSubscriptionTargets_TargetPubkey",
|
||||||
|
table: "PushSubscriptionTargets",
|
||||||
|
column: "TargetPubkey");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PushSubscriptions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PushSubscriptionTargets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -90,6 +90,70 @@ namespace NostrStreamer.Migrations
|
|||||||
b.ToTable("Payments");
|
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.User", b =>
|
modelBuilder.Entity("NostrStreamer.Database.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("PubKey")
|
b.Property<string>("PubKey")
|
||||||
|
@ -51,5 +51,8 @@
|
|||||||
<PackageReference Include="Nostr.Client" Version="2.0.0" />
|
<PackageReference Include="Nostr.Client" Version="2.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="8.0.1" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.7.10" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
<PackageReference Include="WebPush" Version="1.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -4,6 +4,9 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
using Nostr.Client.Client;
|
using Nostr.Client.Client;
|
||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
using NostrStreamer.Services;
|
using NostrStreamer.Services;
|
||||||
@ -13,11 +16,26 @@ using NostrStreamer.Services.Dvr;
|
|||||||
using NostrStreamer.Services.StreamManager;
|
using NostrStreamer.Services.StreamManager;
|
||||||
using NostrStreamer.Services.Thumbnail;
|
using NostrStreamer.Services.Thumbnail;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace NostrStreamer;
|
namespace NostrStreamer;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
|
private static void ConfigureSerializer(JsonSerializerSettings s)
|
||||||
|
{
|
||||||
|
s.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||||
|
s.Formatting = Formatting.None;
|
||||||
|
s.NullValueHandling = NullValueHandling.Ignore;
|
||||||
|
s.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
|
||||||
|
s.Converters = new List<JsonConverter>()
|
||||||
|
{
|
||||||
|
new UnixDateTimeConverter()
|
||||||
|
};
|
||||||
|
|
||||||
|
s.ContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@ -30,9 +48,16 @@ internal static class Program
|
|||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
services.AddRazorPages();
|
services.AddRazorPages();
|
||||||
services.AddControllers().AddNewtonsoftJson();
|
services.AddControllers().AddNewtonsoftJson(opt => { ConfigureSerializer(opt.SerializerSettings); });
|
||||||
|
|
||||||
|
services.AddSwaggerGen();
|
||||||
services.AddSingleton(config);
|
services.AddSingleton(config);
|
||||||
|
|
||||||
|
// Redis
|
||||||
|
var cx = await ConnectionMultiplexer.ConnectAsync(config.Redis);
|
||||||
|
services.AddSingleton(cx);
|
||||||
|
services.AddTransient<IDatabase>(svc => svc.GetRequiredService<ConnectionMultiplexer>().GetDatabase());
|
||||||
|
|
||||||
// GeoIP
|
// GeoIP
|
||||||
services.AddSingleton<IGeoIP2DatabaseReader>(_ => new DatabaseReader(config.GeoIpDatabase));
|
services.AddSingleton<IGeoIP2DatabaseReader>(_ => new DatabaseReader(config.GeoIpDatabase));
|
||||||
services.AddTransient<EdgeSteering>();
|
services.AddTransient<EdgeSteering>();
|
||||||
@ -74,7 +99,7 @@ internal static class Program
|
|||||||
|
|
||||||
// dvr services
|
// dvr services
|
||||||
services.AddTransient<IDvrStore, S3DvrStore>();
|
services.AddTransient<IDvrStore, S3DvrStore>();
|
||||||
|
|
||||||
// thumbnail services
|
// thumbnail services
|
||||||
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
||||||
services.AddHostedService<ThumbnailGenerator>();
|
services.AddHostedService<ThumbnailGenerator>();
|
||||||
@ -89,6 +114,14 @@ internal static class Program
|
|||||||
// clip services
|
// clip services
|
||||||
services.AddTransient<ClipGenerator>();
|
services.AddTransient<ClipGenerator>();
|
||||||
services.AddTransient<IClipService, S3ClipService>();
|
services.AddTransient<IClipService, S3ClipService>();
|
||||||
|
|
||||||
|
// notifications services
|
||||||
|
services.AddSingleton<PushSender>();
|
||||||
|
services.AddHostedService<PushSenderService>();
|
||||||
|
services.AddHostedService<EventStream>();
|
||||||
|
|
||||||
|
// snort api
|
||||||
|
services.AddTransient<SnortApi>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@ -106,6 +139,8 @@ internal static class Program
|
|||||||
app.MapRazorPages();
|
app.MapRazorPages();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapMetrics();
|
app.MapMetrics();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
}
|
}
|
||||||
|
49
NostrStreamer/Services/EventStream.cs
Normal file
49
NostrStreamer/Services/EventStream.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Nostr.Client.Json;
|
||||||
|
using Nostr.Client.Messages;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Services;
|
||||||
|
|
||||||
|
public class EventStream : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<EventStream> _logger;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
|
public EventStream(ILogger<EventStream> logger, IServiceScopeFactory scopeFactory)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var redis = scope.ServiceProvider.GetRequiredService<ConnectionMultiplexer>();
|
||||||
|
var push = scope.ServiceProvider.GetRequiredService<PushSender>();
|
||||||
|
var queue = await redis.GetSubscriber().SubscribeAsync("event-stream");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var msg = await queue.ReadAsync(stoppingToken);
|
||||||
|
|
||||||
|
var ev = JsonConvert.DeserializeObject<NostrEvent>(msg.Message!, NostrSerializer.Settings);
|
||||||
|
if (ev is {Kind: NostrKind.LiveEvent})
|
||||||
|
{
|
||||||
|
push.Add(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "failed {msg}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
133
NostrStreamer/Services/PushSender.cs
Normal file
133
NostrStreamer/Services/PushSender.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks.Dataflow;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Nostr.Client.Json;
|
||||||
|
using Nostr.Client.Messages;
|
||||||
|
using NostrStreamer.ApiModel;
|
||||||
|
using NostrStreamer.Database;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using WebPush;
|
||||||
|
using PushSubscription = NostrStreamer.Database.PushSubscription;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Services;
|
||||||
|
|
||||||
|
public record PushNotificationQueue(PushMessage Notification, PushSubscription Subscription);
|
||||||
|
|
||||||
|
public class PushSender
|
||||||
|
{
|
||||||
|
private readonly BufferBlock<NostrEvent> _queue = new();
|
||||||
|
|
||||||
|
public void Add(NostrEvent ev)
|
||||||
|
{
|
||||||
|
_queue.Post(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<NostrEvent> Next()
|
||||||
|
{
|
||||||
|
return _queue.ReceiveAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PushSenderService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly PushSender _sender;
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly Config _config;
|
||||||
|
private readonly ILogger<PushSenderService> _logger;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IDatabase _redis;
|
||||||
|
private readonly SnortApi _snort;
|
||||||
|
|
||||||
|
public PushSenderService(PushSender sender, HttpClient client, Config config, IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<PushSenderService> logger, SnortApi snort, IDatabase redis)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
_client = client;
|
||||||
|
_config = config;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_snort = snort;
|
||||||
|
_redis = redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
await using var db = scope.ServiceProvider.GetRequiredService<StreamerContext>();
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ev = await _sender.Next();
|
||||||
|
foreach (var (msg, sub) in await ComputeNotifications(db, ev))
|
||||||
|
{
|
||||||
|
var vapid = new VapidDetails(sub.Scope, _config.VapidKey.PublicKey, _config.VapidKey.PrivateKey);
|
||||||
|
using var webPush = new WebPushClient(_client);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pushMsg = JsonConvert.SerializeObject(msg, NostrSerializer.Settings);
|
||||||
|
_logger.LogInformation("Sending notification {msg}", pushMsg);
|
||||||
|
var webSub = new WebPush.PushSubscription(sub.Endpoint, sub.Key, sub.Auth);
|
||||||
|
await webPush.SendNotificationAsync(webSub, pushMsg, vapid, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to send push for {pubkey} {error}", sub.Pubkey, ex.Message);
|
||||||
|
if (ex is WebPushException {StatusCode: HttpStatusCode.Gone})
|
||||||
|
{
|
||||||
|
await db.PushSubscriptions.Where(a => a.Id == sub.Id)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in PushSender {message}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<PushNotificationQueue>> ComputeNotifications(StreamerContext db, NostrEvent ev)
|
||||||
|
{
|
||||||
|
var ret = new List<PushNotificationQueue>();
|
||||||
|
var notification = await MakeNotificationFromEvent(ev);
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
foreach (var sub in await db.PushSubscriptions
|
||||||
|
.AsNoTracking()
|
||||||
|
.Join(db.PushSubscriptionTargets, a => a.Pubkey, b => b.SubscriberPubkey,
|
||||||
|
(a, b) => new {Subscription = a, Target = b})
|
||||||
|
.Where(a => a.Target.TargetPubkey == notification.Pubkey)
|
||||||
|
.ToListAsync())
|
||||||
|
{
|
||||||
|
ret.Add(new(notification, sub.Subscription));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PushMessage?> MakeNotificationFromEvent(NostrEvent ev)
|
||||||
|
{
|
||||||
|
if (ev.Kind != NostrKind.LiveEvent) return default;
|
||||||
|
|
||||||
|
var dTag = ev.Tags!.FindFirstTagValue("d");
|
||||||
|
var key = $"live-event-seen:{ev.Pubkey}:{dTag}";
|
||||||
|
if (await _redis.KeyExistsAsync(key)) return default;
|
||||||
|
|
||||||
|
await _redis.StringSetAsync(key, ev.Id!, TimeSpan.FromDays(7));
|
||||||
|
|
||||||
|
var host = ev.GetHost();
|
||||||
|
var profile = await _snort.Profile(host);
|
||||||
|
return new PushMessage
|
||||||
|
{
|
||||||
|
Type = PushMessageType.StreamStarted,
|
||||||
|
Pubkey = host,
|
||||||
|
Name = profile?.Name,
|
||||||
|
Avatar = profile?.Picture
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
50
NostrStreamer/Services/SnortApi.cs
Normal file
50
NostrStreamer/Services/SnortApi.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Services;
|
||||||
|
|
||||||
|
public class SnortApi
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
public SnortApi(HttpClient client, Config config)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_client.BaseAddress = config.SnortApi;
|
||||||
|
_client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SnortProfile?> Profile(string pubkey)
|
||||||
|
{
|
||||||
|
var json = await _client.GetStringAsync($"/api/v1/raw/p/{pubkey}");
|
||||||
|
if (!string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<SnortProfile>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnortProfile
|
||||||
|
{
|
||||||
|
[JsonProperty("pubKey")]
|
||||||
|
public string PubKey { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("about")]
|
||||||
|
public string? About { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("picture")]
|
||||||
|
public string? Picture { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("nip05")]
|
||||||
|
public string? Nip05 { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("lud16")]
|
||||||
|
public string? Lud16 { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("banner")]
|
||||||
|
public string? Banner { get; init; }
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
"Database": "User ID=postgres;Password=postgres;Database=streaming;Pooling=true;Host=127.0.0.1:5431"
|
"Database": "User ID=postgres;Password=postgres;Database=streaming;Pooling=true;Host=127.0.0.1:5431"
|
||||||
},
|
},
|
||||||
"Config": {
|
"Config": {
|
||||||
|
"Redis": "localhost:6666",
|
||||||
"RtmpHost": "rtmp://localhost:9005",
|
"RtmpHost": "rtmp://localhost:9005",
|
||||||
"SrsHttpHost": "http://localhost:9003",
|
"SrsHttpHost": "http://localhost:9003",
|
||||||
"SrsApiHost": "http://localhost:9002",
|
"SrsApiHost": "http://localhost:9002",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"SecretKey": "p7EK4qew6DBkBPqrpRPuJgTOc6ChUlfIcEdAwE7K",
|
"SecretKey": "p7EK4qew6DBkBPqrpRPuJgTOc6ChUlfIcEdAwE7K",
|
||||||
"PublicHost": "http://localhost:9010"
|
"PublicHost": "http://localhost:9010"
|
||||||
},
|
},
|
||||||
|
"SnortApi": "https://api.snort.social",
|
||||||
"GeoIpDatabase": "/Users/kieran/Downloads/GeoLite2-City_20230801/GeoLite2-City.mmdb",
|
"GeoIpDatabase": "/Users/kieran/Downloads/GeoLite2-City_20230801/GeoLite2-City.mmdb",
|
||||||
"Edges": [
|
"Edges": [
|
||||||
{
|
{
|
||||||
@ -52,6 +54,10 @@
|
|||||||
"ClientId": "123",
|
"ClientId": "123",
|
||||||
"ClientSecret": "aaa"
|
"ClientSecret": "aaa"
|
||||||
},
|
},
|
||||||
"DataProtectionKeyPath": "./keys"
|
"DataProtectionKeyPath": "./keys",
|
||||||
|
"VapidKey": {
|
||||||
|
"PublicKey": "BOlCzqQENSe0TR8wCfQmTW2p_QhaOSqLLVMqKduTNcZKuebLHQuXjh17Ewo_g-Q4iDTnKVj2BdxBqxf5Dc6FhvU",
|
||||||
|
"PrivateKey": "JL5_OHhNaD9SzYdOfLYd9W_G-4V-J22TANpbD4JXEkI"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user