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 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
|
||||
|
@ -5,11 +5,13 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using Nostr.Client.Json;
|
||||
using Nostr.Client.Messages;
|
||||
using NostrStreamer.ApiModel;
|
||||
using NostrStreamer.Database;
|
||||
using NostrStreamer.Services;
|
||||
using NostrStreamer.Services.Clips;
|
||||
using NostrStreamer.Services.StreamManager;
|
||||
using WebPush;
|
||||
|
||||
namespace NostrStreamer.Controllers;
|
||||
|
||||
@ -23,15 +25,19 @@ public class NostrController : Controller
|
||||
private readonly StreamManagerFactory _streamManagerFactory;
|
||||
private readonly UserService _userService;
|
||||
private readonly IClipService _clipService;
|
||||
private readonly ILogger<NostrController> _logger;
|
||||
private readonly PushSender _pushSender;
|
||||
|
||||
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, UserService userService,
|
||||
IClipService clipService)
|
||||
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
_streamManagerFactory = streamManager;
|
||||
_userService = userService;
|
||||
_clipService = clipService;
|
||||
_logger = logger;
|
||||
_pushSender = pushSender;
|
||||
}
|
||||
|
||||
[HttpGet("account")]
|
||||
@ -206,6 +212,145 @@ public class NostrController : Controller
|
||||
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()
|
||||
{
|
||||
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<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)));
|
||||
}
|
||||
|
||||
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
|
||||
|
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");
|
||||
});
|
||||
|
||||
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")
|
||||
|
@ -51,5 +51,8 @@
|
||||
<PackageReference Include="Nostr.Client" Version="2.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||
<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>
|
||||
</Project>
|
||||
|
@ -4,6 +4,9 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Nostr.Client.Client;
|
||||
using NostrStreamer.Database;
|
||||
using NostrStreamer.Services;
|
||||
@ -13,11 +16,26 @@ using NostrStreamer.Services.Dvr;
|
||||
using NostrStreamer.Services.StreamManager;
|
||||
using NostrStreamer.Services.Thumbnail;
|
||||
using Prometheus;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace NostrStreamer;
|
||||
|
||||
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)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -30,9 +48,16 @@ internal static class Program
|
||||
services.AddMemoryCache();
|
||||
services.AddHttpClient();
|
||||
services.AddRazorPages();
|
||||
services.AddControllers().AddNewtonsoftJson();
|
||||
services.AddControllers().AddNewtonsoftJson(opt => { ConfigureSerializer(opt.SerializerSettings); });
|
||||
|
||||
services.AddSwaggerGen();
|
||||
services.AddSingleton(config);
|
||||
|
||||
// Redis
|
||||
var cx = await ConnectionMultiplexer.ConnectAsync(config.Redis);
|
||||
services.AddSingleton(cx);
|
||||
services.AddTransient<IDatabase>(svc => svc.GetRequiredService<ConnectionMultiplexer>().GetDatabase());
|
||||
|
||||
// GeoIP
|
||||
services.AddSingleton<IGeoIP2DatabaseReader>(_ => new DatabaseReader(config.GeoIpDatabase));
|
||||
services.AddTransient<EdgeSteering>();
|
||||
@ -74,7 +99,7 @@ internal static class Program
|
||||
|
||||
// dvr services
|
||||
services.AddTransient<IDvrStore, S3DvrStore>();
|
||||
|
||||
|
||||
// thumbnail services
|
||||
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
||||
services.AddHostedService<ThumbnailGenerator>();
|
||||
@ -89,6 +114,14 @@ internal static class Program
|
||||
// clip services
|
||||
services.AddTransient<ClipGenerator>();
|
||||
services.AddTransient<IClipService, S3ClipService>();
|
||||
|
||||
// notifications services
|
||||
services.AddSingleton<PushSender>();
|
||||
services.AddHostedService<PushSenderService>();
|
||||
services.AddHostedService<EventStream>();
|
||||
|
||||
// snort api
|
||||
services.AddTransient<SnortApi>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@ -106,6 +139,8 @@ internal static class Program
|
||||
app.MapRazorPages();
|
||||
app.MapControllers();
|
||||
app.MapMetrics();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
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"
|
||||
},
|
||||
"Config": {
|
||||
"Redis": "localhost:6666",
|
||||
"RtmpHost": "rtmp://localhost:9005",
|
||||
"SrsHttpHost": "http://localhost:9003",
|
||||
"SrsApiHost": "http://localhost:9002",
|
||||
@ -33,6 +34,7 @@
|
||||
"SecretKey": "p7EK4qew6DBkBPqrpRPuJgTOc6ChUlfIcEdAwE7K",
|
||||
"PublicHost": "http://localhost:9010"
|
||||
},
|
||||
"SnortApi": "https://api.snort.social",
|
||||
"GeoIpDatabase": "/Users/kieran/Downloads/GeoLite2-City_20230801/GeoLite2-City.mmdb",
|
||||
"Edges": [
|
||||
{
|
||||
@ -52,6 +54,10 @@
|
||||
"ClientId": "123",
|
||||
"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