diff --git a/NostrStreamer/ApiModel/Account.cs b/NostrStreamer/ApiModel/Account.cs index 7ce7760..8842ba4 100644 --- a/NostrStreamer/ApiModel/Account.cs +++ b/NostrStreamer/ApiModel/Account.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Nostr.Client.Messages; namespace NostrStreamer.ApiModel; @@ -9,4 +10,23 @@ public class Account [JsonProperty("key")] public string Key { get; init; } = null!; + + [JsonProperty("event")] + public NostrEvent? Event { get; init; } + + [JsonProperty("quota")] + public AccountQuota Quota { get; init; } = null!; } + + +public class AccountQuota +{ + [JsonProperty("rate")] + public double Rate { get; init; } + + [JsonProperty("unit")] + public string Unit { get; init; } = null!; + + [JsonProperty("remaining")] + public long Remaining { get; init; } +} \ No newline at end of file diff --git a/NostrStreamer/ApiModel/PatchEvent.cs b/NostrStreamer/ApiModel/PatchEvent.cs new file mode 100644 index 0000000..1bb7831 --- /dev/null +++ b/NostrStreamer/ApiModel/PatchEvent.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace NostrStreamer.ApiModel; + +public class PatchEvent +{ + [JsonProperty("title")] + public string Title { get; init; } = null!; + + [JsonProperty("summary")] + public string Summary { get; init; } = null!; + + [JsonProperty("image")] + public string Image { get; init; } = null!; +} diff --git a/NostrStreamer/Controllers/NostrController.cs b/NostrStreamer/Controllers/NostrController.cs index a0e6d95..52f068b 100644 --- a/NostrStreamer/Controllers/NostrController.cs +++ b/NostrStreamer/Controllers/NostrController.cs @@ -2,8 +2,12 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; 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; namespace NostrStreamer.Controllers; @@ -13,11 +17,13 @@ public class NostrController : Controller { private readonly StreamerContext _db; private readonly Config _config; + private readonly StreamManager _streamManager; - public NostrController(StreamerContext db, Config config) + public NostrController(StreamerContext db, Config config, StreamManager streamManager) { _db = db; _config = config; + _streamManager = streamManager; } [HttpGet("account")] @@ -39,13 +45,33 @@ public class NostrController : Controller await _db.SaveChangesAsync(); } - return Json(new Account + var account = new Account { Url = new Uri(_config.RtmpHost, _config.App).ToString(), - Key = user.StreamKey - }); + Key = user.StreamKey, + Event = !string.IsNullOrEmpty(user.Event) ? JsonConvert.DeserializeObject(user.Event, NostrSerializer.Settings) : + null, + Quota = new() + { + Unit = "min", + Rate = 21, + Remaining = user.Balance + } + }; + + return Content(JsonConvert.SerializeObject(account, NostrSerializer.Settings), "application/json"); } + [HttpPatch("event")] + public async Task UpdateStreamInfo([FromBody]PatchEvent req) + { + var pubkey = GetPubKey(); + if (string.IsNullOrEmpty(pubkey)) return Unauthorized(); + + await _streamManager.PatchEvent(pubkey, req.Title, req.Summary, req.Image); + return Accepted(); + } + private async Task GetUser() { var pk = GetPubKey(); diff --git a/NostrStreamer/Database/User.cs b/NostrStreamer/Database/User.cs index 4e1ea7a..d3892a3 100644 --- a/NostrStreamer/Database/User.cs +++ b/NostrStreamer/Database/User.cs @@ -12,30 +12,30 @@ public class User /// /// Most recent nostr event published /// - public string? Event { get; init; } + public string? Event { get; set; } /// /// Sats balance /// - public long Balance { get; init; } + public long Balance { get; set; } /// /// Stream title /// - public string? Title { get; init; } + public string? Title { get; set; } /// /// Stream summary /// - public string? Summary { get; init; } + public string? Summary { get; set; } /// /// Stream cover image /// - public string? Image { get; init; } + public string? Image { get; set; } /// /// Comma seperated tags /// - public string? Tags { get; init; } + public string? Tags { get; set; } } diff --git a/NostrStreamer/NostrAuth.cs b/NostrStreamer/NostrAuth.cs index 125cacb..3391265 100644 --- a/NostrStreamer/NostrAuth.cs +++ b/NostrStreamer/NostrAuth.cs @@ -4,6 +4,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Nostr.Client.Json; using Nostr.Client.Messages; namespace NostrStreamer; @@ -44,7 +45,7 @@ public class NostrAuthHandler : AuthenticationHandler return AuthenticateResult.Fail("Invalid token"); } - var ev = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bToken)); + var ev = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bToken), NostrSerializer.Settings); if (ev == default) { return AuthenticateResult.Fail("Invalid nostr event"); @@ -66,7 +67,7 @@ public class NostrAuthHandler : AuthenticationHandler return AuthenticateResult.Fail("Invalid nostr event, timestamp out of range"); } - var urlTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "url"); + var urlTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "u"); var methodTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "method"); if (string.IsNullOrEmpty(urlTag?.AdditionalData[0] as string) || !new Uri((urlTag.AdditionalData[0] as string)!).AbsolutePath.Equals(Request.Path, StringComparison.InvariantCultureIgnoreCase)) diff --git a/NostrStreamer/Program.cs b/NostrStreamer/Program.cs index 44e1311..178977d 100644 --- a/NostrStreamer/Program.cs +++ b/NostrStreamer/Program.cs @@ -1,3 +1,6 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.EntityFrameworkCore; using Nostr.Client.Client; using NostrStreamer.Database; @@ -21,6 +24,22 @@ internal static class Program services.AddControllers(); services.AddSingleton(config); + // nostr auth + services.AddTransient(); + services.AddAuthentication(o => + { + o.DefaultChallengeScheme = NostrAuth.Scheme; + o.AddScheme(NostrAuth.Scheme, "Nostr"); + }); + + services.AddAuthorization(o => + { + o.DefaultPolicy = new AuthorizationPolicy(new[] + { + new ClaimsAuthorizationRequirement(ClaimTypes.Name, null) + }, new[] {NostrAuth.Scheme}); + }); + // nostr services services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); @@ -39,6 +58,7 @@ internal static class Program } app.UseCors(o => o.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); + app.UseAuthorization(); app.MapControllers(); await app.RunAsync(); diff --git a/NostrStreamer/Services/StreamManager.cs b/NostrStreamer/Services/StreamManager.cs index 093693c..f0cdc58 100644 --- a/NostrStreamer/Services/StreamManager.cs +++ b/NostrStreamer/Services/StreamManager.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using Nostr.Client.Client; +using Nostr.Client.Json; using Nostr.Client.Keys; using Nostr.Client.Messages; using Nostr.Client.Requests; @@ -33,8 +35,9 @@ public class StreamManager { throw new Exception("User balance empty"); } + var ev = CreateStreamEvent(user, "live"); - _nostr.Send(new NostrEventRequest(ev)); + await PublishEvent(user, ev); } public async Task StreamStopped(string streamKey) @@ -45,7 +48,7 @@ public class StreamManager _logger.LogInformation("Stream stopped for: {pubkey}", user.PubKey); var ev = CreateStreamEvent(user, "ended"); - _nostr.Send(new NostrEventRequest(ev)); + await PublishEvent(user, ev); } public async Task ConsumeQuota(string streamKey, double duration) @@ -66,6 +69,33 @@ public class StreamManager } } + public async Task PatchEvent(string pubkey, string? title, string? summary, string? image) + { + var user = await _db.Users.SingleOrDefaultAsync(a => a.PubKey == pubkey); + if (user == default) throw new Exception("User not found"); + + user.Title = title; + user.Summary = summary; + user.Image = image; + + var existingEvent = user.Event != default ? JsonConvert.DeserializeObject(user.Event, NostrSerializer.Settings) : null; + var ev = CreateStreamEvent(user, existingEvent?.Tags?.FindFirstTagValue("status") ?? "planned"); + user.Event = JsonConvert.SerializeObject(ev, NostrSerializer.Settings); + + await _db.SaveChangesAsync(); + + _nostr.Send(new NostrEventRequest(ev)); + } + + private async Task PublishEvent(User user, NostrEvent ev) + { + await _db.Users + .Where(a => a.PubKey == user.PubKey) + .ExecuteUpdateAsync(o => o.SetProperty(v => v.Event, JsonConvert.SerializeObject(ev, NostrSerializer.Settings))); + + _nostr.Send(new NostrEventRequest(ev)); + } + private NostrEvent CreateStreamEvent(User user, string state) { var tags = new List() @@ -100,7 +130,7 @@ public class StreamManager var ub = new Uri(_config.DataHost, $"{u.PubKey}.m3u8"); return ub.ToString(); } - + private async Task GetUserFromStreamKey(string streamKey) { return await _db.Users.SingleOrDefaultAsync(a => a.StreamKey == streamKey); diff --git a/NostrStreamer/appsettings.json b/NostrStreamer/appsettings.json index d8c7420..5da3b48 100644 --- a/NostrStreamer/appsettings.json +++ b/NostrStreamer/appsettings.json @@ -12,7 +12,7 @@ }, "Config": { "RtmpHost": "rtmp://localhost:1935", - "SrsHttpHost": "http://localhost:8080", + "SrsHttpHost": "http://localhost:8082", "SrsApiHost": "http://localhost:1985", "DataHost": "http://localhost:5295/api/playlist/", "App": "test", diff --git a/docker-compose.yaml b/docker-compose.yaml index 8b9ee99..a114b31 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,7 +6,7 @@ services: ports: - "1935:1935" - "1985:1985" - - "8080:8080" + - "8082:8080" nostr: image: scsibug/nostr-rs-relay ports: