Simple relay import
This commit is contained in:
parent
17df7e73d1
commit
a0e59757b5
35
NostrRelay/INostrRelay.cs
Normal file
35
NostrRelay/INostrRelay.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using Nostr.Client.Messages;
|
||||
using Nostr.Client.Requests;
|
||||
|
||||
namespace NostrRelay;
|
||||
|
||||
public record NostrClientContext(WebSocket WebSocket, IPAddress Ip, string UserAgent);
|
||||
public record HandleEventResponse(bool Ok, string? Message);
|
||||
|
||||
public interface INostrRelay
|
||||
{
|
||||
/// <summary>
|
||||
/// If we should handle this connection
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<bool> AcceptConnection(NostrClientContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Respond to a request for content
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
IAsyncEnumerable<NostrEvent> HandleRequest(NostrClientContext context, NostrRequest req);
|
||||
|
||||
/// <summary>
|
||||
/// Handle new event publish
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="ev"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<HandleEventResponse> HandleEvent(NostrClientContext context, NostrEvent ev);
|
||||
}
|
13
NostrRelay/NostrRelay.csproj
Normal file
13
NostrRelay/NostrRelay.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nostr.Client" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
95
NostrRelay/Relay.cs
Normal file
95
NostrRelay/Relay.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System.Buffers;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Nostr.Client.Json;
|
||||
using Nostr.Client.Requests;
|
||||
using Nostr.Client.Responses;
|
||||
|
||||
namespace NostrRelay;
|
||||
|
||||
public class NostrRelay<THandler> where THandler : INostrRelay
|
||||
{
|
||||
private readonly ILogger<NostrRelay<THandler>> _logger;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly THandler _handler;
|
||||
private readonly NostrClientContext _ctx;
|
||||
|
||||
public NostrRelay(THandler handler, NostrClientContext ctx, CancellationToken ct, ILogger<NostrRelay<THandler>> logger)
|
||||
{
|
||||
_handler = handler;
|
||||
_ctx = ctx;
|
||||
_logger = logger;
|
||||
ct.Register(() => _cts.Cancel());
|
||||
}
|
||||
|
||||
private async Task WriteResponse<T>(T obj)
|
||||
{
|
||||
var rspJson = JsonConvert.SerializeObject(obj, NostrSerializer.Settings);
|
||||
_logger.LogDebug("Sending {msg}", rspJson);
|
||||
await _ctx.WebSocket.SendAsync(Encoding.UTF8.GetBytes(rspJson), WebSocketMessageType.Text, true, _cts.Token);
|
||||
}
|
||||
|
||||
public async Task Read()
|
||||
{
|
||||
if (!await _handler.AcceptConnection(_ctx)) return;
|
||||
|
||||
var offset = 0;
|
||||
var mem = MemoryPool<byte>.Shared.Rent(1024 * 1024);
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
var msg = await _ctx.WebSocket.ReceiveAsync(mem.Memory[offset..], _cts.Token);
|
||||
if (msg.MessageType is WebSocketMessageType.Text)
|
||||
{
|
||||
var buff = mem.Memory[..(offset + msg.Count)];
|
||||
offset = !msg.EndOfMessage ? offset + msg.Count : 0;
|
||||
if (!msg.EndOfMessage) continue;
|
||||
|
||||
var str = Encoding.UTF8.GetString(buff.Span);
|
||||
_logger.LogDebug("Got msg {msg}", str);
|
||||
if (str.StartsWith("[\"REQ\""))
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<NostrRequest>(str, NostrSerializer.Settings);
|
||||
if (req != default)
|
||||
{
|
||||
await foreach (var ev in _handler.HandleRequest(_ctx, req))
|
||||
{
|
||||
await WriteResponse(new NostrEventResponse()
|
||||
{
|
||||
MessageType = "EVENT",
|
||||
Subscription = req.Subscription,
|
||||
Event = ev
|
||||
});
|
||||
}
|
||||
|
||||
var rsp = new NostrEoseResponse
|
||||
{
|
||||
MessageType = "EOSE",
|
||||
Subscription = req.Subscription
|
||||
};
|
||||
|
||||
await WriteResponse(rsp);
|
||||
}
|
||||
}
|
||||
else if (str.StartsWith("[\"EVENT\""))
|
||||
{
|
||||
var req = JsonConvert.DeserializeObject<NostrEventRequest>(str, NostrSerializer.Settings);
|
||||
if (req != default)
|
||||
{
|
||||
var result = await _handler.HandleEvent(_ctx, req.Event);
|
||||
var rsp = new NostrOkResponse()
|
||||
{
|
||||
MessageType = "OK",
|
||||
EventId = req.Event.Id,
|
||||
Accepted = result.Ok,
|
||||
Message = result.Message ?? ""
|
||||
};
|
||||
|
||||
await WriteResponse(rsp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NostrRelay", "NostrRelay\NostrRelay.csproj", "{FBCF209E-9C58-45EB-BC59-99569B28D811}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -19,5 +21,9 @@ Global
|
||||
{51CD83E4-2CEC-4852-B285-2327EF86C6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51CD83E4-2CEC-4852-B285-2327EF86C6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51CD83E4-2CEC-4852-B285-2327EF86C6E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FBCF209E-9C58-45EB-BC59-99569B28D811}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@ -25,6 +25,10 @@ public class ImportController : Controller
|
||||
/// POST events in ND-JSON format here to add them to the cache which is used in <see cref="OpenGraphController"/>
|
||||
///
|
||||
/// `nak req -k 1 -l 100 wss://nos.lol | curl -v -X POST --data-binary @- https://nostr.api.v0l.io/api/v1/import`
|
||||
///
|
||||
/// You can also write events directly to this API as nostr websocket
|
||||
///
|
||||
/// `nak req -k 1 -l 100 wss://nos.lol | nak event --envelope | websocat -tn wss://nostr.api.v0l.io/`
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.Net.WebSockets;
|
||||
using Nostr.Client.Identifiers;
|
||||
using Nostr.Client.Messages;
|
||||
using Nostr.Client.Utils;
|
||||
using NostrRelay;
|
||||
using NostrServices.Services;
|
||||
using ProtoBuf;
|
||||
using StackExchange.Redis;
|
||||
@ -56,4 +58,32 @@ public static class Extensions
|
||||
{
|
||||
return new NostrProfileIdentifier(px.PubKey.ToHex(), []);
|
||||
}
|
||||
|
||||
public static void MapNostrRelay<THandler>(this IEndpointRouteBuilder app, string path) where THandler : INostrRelay
|
||||
{
|
||||
app.MapGet(path, async ctx =>
|
||||
{
|
||||
if (ctx.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
var logger = app.ServiceProvider.GetRequiredService<ILogger<NostrRelay<THandler>>>();
|
||||
var handler = app.ServiceProvider.GetRequiredService<THandler>();
|
||||
try
|
||||
{
|
||||
var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||
var wsCtx = new NostrClientContext(ws, ctx.Connection.RemoteIpAddress!,
|
||||
ctx.Request.Headers.UserAgent.FirstOrDefault() ?? string.Empty);
|
||||
|
||||
var nostrRelay = new NostrRelay<THandler>(handler, wsCtx, ctx.RequestAborted, logger);
|
||||
|
||||
await nostrRelay.Read();
|
||||
|
||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, ctx.RequestAborted);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex.Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -33,4 +33,8 @@
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NostrRelay\NostrRelay.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -67,9 +67,11 @@ public static class Program
|
||||
Url = new Uri("https://git.v0l.io/Kieran/NostrServices")
|
||||
}
|
||||
});
|
||||
|
||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
opt.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddResponseCaching();
|
||||
@ -77,6 +79,7 @@ public static class Program
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddHostedService<NostrListener.NostrListenerLifetime>();
|
||||
builder.Services.AddTransient<CacheRelay>();
|
||||
|
||||
ConfigureDb(builder.Services, builder.Configuration);
|
||||
|
||||
@ -92,6 +95,7 @@ public static class Program
|
||||
app.UseSwaggerUI();
|
||||
app.UseHttpMetrics();
|
||||
app.UseHealthChecks("/healthz");
|
||||
app.UseWebSockets();
|
||||
app.UseCors(o =>
|
||||
{
|
||||
o.AllowAnyOrigin();
|
||||
@ -102,6 +106,7 @@ public static class Program
|
||||
app.UseRouting();
|
||||
app.MapControllers();
|
||||
app.MapMetrics();
|
||||
app.MapNostrRelay<CacheRelay>("/");
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
|
40
NostrServices/Services/CacheRelay.cs
Normal file
40
NostrServices/Services/CacheRelay.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Nostr.Client.Messages;
|
||||
using Nostr.Client.Requests;
|
||||
using NostrRelay;
|
||||
|
||||
namespace NostrServices.Services;
|
||||
|
||||
public class CacheRelay : INostrRelay
|
||||
{
|
||||
private readonly ILogger<CacheRelay> _logger;
|
||||
private readonly RedisStore _redisStore;
|
||||
|
||||
public CacheRelay(RedisStore redisStore, ILogger<CacheRelay> logger)
|
||||
{
|
||||
_redisStore = redisStore;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ValueTask<bool> AcceptConnection(NostrClientContext context)
|
||||
{
|
||||
_logger.LogInformation("New connection {ip} {ua}", context.Ip, context.UserAgent);
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<NostrEvent> HandleRequest(NostrClientContext context, NostrRequest req)
|
||||
{
|
||||
//no results yet
|
||||
yield break;
|
||||
}
|
||||
|
||||
public async ValueTask<HandleEventResponse> HandleEvent(NostrClientContext context, NostrEvent ev)
|
||||
{
|
||||
if (RelayListener.AcceptedKinds.Contains(ev.Kind))
|
||||
{
|
||||
await _redisStore.StoreEvent(CompactEvent.FromNostrEvent(ev));
|
||||
return new(true, null);
|
||||
}
|
||||
|
||||
return new(false, "blocked: kind not accepted");
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user