Implement nostr api
This commit is contained in:
@ -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; }
|
||||
}
|
15
NostrStreamer/ApiModel/PatchEvent.cs
Normal file
15
NostrStreamer/ApiModel/PatchEvent.cs
Normal file
@ -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!;
|
||||
}
|
@ -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<NostrEvent>(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<IActionResult> 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<User?> GetUser()
|
||||
{
|
||||
var pk = GetPubKey();
|
||||
|
@ -12,30 +12,30 @@ public class User
|
||||
/// <summary>
|
||||
/// Most recent nostr event published
|
||||
/// </summary>
|
||||
public string? Event { get; init; }
|
||||
public string? Event { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sats balance
|
||||
/// </summary>
|
||||
public long Balance { get; init; }
|
||||
public long Balance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stream title
|
||||
/// </summary>
|
||||
public string? Title { get; init; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stream summary
|
||||
/// </summary>
|
||||
public string? Summary { get; init; }
|
||||
public string? Summary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stream cover image
|
||||
/// </summary>
|
||||
public string? Image { get; init; }
|
||||
public string? Image { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma seperated tags
|
||||
/// </summary>
|
||||
public string? Tags { get; init; }
|
||||
public string? Tags { get; set; }
|
||||
}
|
||||
|
@ -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<NostrAuthOptions>
|
||||
return AuthenticateResult.Fail("Invalid token");
|
||||
}
|
||||
|
||||
var ev = JsonConvert.DeserializeObject<NostrEvent>(Encoding.UTF8.GetString(bToken));
|
||||
var ev = JsonConvert.DeserializeObject<NostrEvent>(Encoding.UTF8.GetString(bToken), NostrSerializer.Settings);
|
||||
if (ev == default)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid nostr event");
|
||||
@ -66,7 +67,7 @@ public class NostrAuthHandler : AuthenticationHandler<NostrAuthOptions>
|
||||
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))
|
||||
|
@ -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<NostrAuthHandler>();
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultChallengeScheme = NostrAuth.Scheme;
|
||||
o.AddScheme<NostrAuthHandler>(NostrAuth.Scheme, "Nostr");
|
||||
});
|
||||
|
||||
services.AddAuthorization(o =>
|
||||
{
|
||||
o.DefaultPolicy = new AuthorizationPolicy(new[]
|
||||
{
|
||||
new ClaimsAuthorizationRequirement(ClaimTypes.Name, null)
|
||||
}, new[] {NostrAuth.Scheme});
|
||||
});
|
||||
|
||||
// nostr services
|
||||
services.AddSingleton<NostrMultiWebsocketClient>();
|
||||
services.AddSingleton<INostrClient>(s => s.GetRequiredService<NostrMultiWebsocketClient>());
|
||||
@ -39,6 +58,7 @@ internal static class Program
|
||||
}
|
||||
|
||||
app.UseCors(o => o.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
await app.RunAsync();
|
||||
|
@ -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<NostrEvent>(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<NostrEventTag>()
|
||||
@ -100,7 +130,7 @@ public class StreamManager
|
||||
var ub = new Uri(_config.DataHost, $"{u.PubKey}.m3u8");
|
||||
return ub.ToString();
|
||||
}
|
||||
|
||||
|
||||
private async Task<User?> GetUserFromStreamKey(string streamKey)
|
||||
{
|
||||
return await _db.Users.SingleOrDefaultAsync(a => a.StreamKey == streamKey);
|
||||
|
@ -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",
|
||||
|
@ -6,7 +6,7 @@ services:
|
||||
ports:
|
||||
- "1935:1935"
|
||||
- "1985:1985"
|
||||
- "8080:8080"
|
||||
- "8082:8080"
|
||||
nostr:
|
||||
image: scsibug/nostr-rs-relay
|
||||
ports:
|
||||
|
Reference in New Issue
Block a user