Files
NostrServices/PayForReactions/ZapperRelay.cs
2024-01-13 20:07:27 +00:00

156 lines
5.4 KiB
C#

using FASTER.core;
using Nostr.Client.Keys;
using Nostr.Client.Messages;
using Nostr.Client.Requests;
using NostrRelay;
using NostrServices.Client;
using EventSession =
FASTER.core.ClientSession<byte[], byte[], byte[], byte[], FASTER.core.Empty, FASTER.core.SimpleFunctions<byte[], byte[]>>;
namespace PayForReactions;
public class ZapperRelay : INostrRelay, IDisposable
{
private readonly Config _config;
private readonly Lnurl _lnurl;
private readonly AlbyApi _albyApi;
private readonly NostrServicesClient _nostrServices;
private readonly EventSession _session;
public ZapperRelay(Lnurl lnurl, AlbyApi albyApi, NostrServicesClient nostrServices, Config config, NostrStore store)
{
_lnurl = lnurl;
_albyApi = albyApi;
_nostrServices = nostrServices;
_config = config;
_session = store.MainStore.For(new SimpleFunctions<byte[], byte[]>()).NewSession<SimpleFunctions<byte[], byte[]>>();
}
public ValueTask<bool> AcceptConnection(NostrClientContext context)
{
return ValueTask.FromResult(true);
}
public async IAsyncEnumerable<NostrEvent> HandleRequest(NostrClientContext context, NostrRequest req)
{
yield break;
}
public async ValueTask<HandleEventResponse> HandleEvent(NostrClientContext context, NostrEvent ev)
{
var target = NostrPublicKey.FromBech32(_config.Target).Hex;
if (ev.Pubkey == target)
{
var id = Convert.FromHexString(ev.Id!);
var obj = NostrBuf.Encode(ev);
(await _session.UpsertAsync(ref id, ref obj)).Complete();
return new(true, "");
}
var amount = MapKindToAmount(ev.Kind);
if (amount != default)
{
var eTag = ev.Tags?.FirstOrDefault(a => a.TagIdentifier == "e");
var pTag = ev.Tags?.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[0] == target);
if (pTag == default || eTag == default)
{
return new(false, $"blocked: must be a reaction to {target}");
}
var idBytes = Convert.FromHexString(eTag.AdditionalData[0]);
var refEventResult = (await _session.ReadAsync(ref idBytes)).Complete();
if (refEventResult.status.NotFound)
{
return new(false, "blocked: parent event not found");
}
var seenId = Convert.FromHexString(ev.Id!);
var seenIdResult = (await _session.ReadAsync(ref seenId)).Complete();
if (seenIdResult.status.Found)
{
return new(false, "blocked: already zapped this one, how dare you!");
}
var refEvent = NostrBuf.Decode(refEventResult.output);
if (refEvent == default)
{
return new(false, "blocked: parent event not found");
}
if (refEvent.Pubkey != target)
{
return new(false, $"blocked: parent event not posted by {target}");
}
var sender = ev.Pubkey!;
var senderProfile = await _nostrServices.Profile(NostrPublicKey.FromHex(sender).Bech32);
if (senderProfile == default)
{
return new(false, "blocked: couldn't find your profile anon!");
}
var parsedTarget = Lnurl.ParseLnUrl(senderProfile.LightningAddress ?? "");
if (parsedTarget == default)
{
return new(false,
$"blocked: so sad... couldn't send a zap because you don't have a lightning address in your profile {senderProfile.Name}!");
}
var svc = await _lnurl.LoadAsync(parsedTarget.ToString());
if (svc == default)
{
return new(false, "blocked: wallet is down, no zap for you!");
}
var key = NostrPrivateKey.FromBech32(_config.PrivateKey);
var myPubkey = key.DerivePublicKey().Hex;
var zap = new NostrEvent
{
Kind = NostrKind.ZapRequest,
Content = "Thanks for your interaction!",
CreatedAt = DateTime.UtcNow,
Pubkey = myPubkey,
Tags = new NostrEventTags(
new NostrEventTag("e", ev.Id!),
new NostrEventTag("p", sender),
new NostrEventTag("amount", (amount * 1000).ToString()!)
)
};
var zapSigned = zap.Sign(key);
var invoice = await _lnurl.GetInvoiceAsync(svc, 5, "Thanks for your interaction!", zapSigned);
if (string.IsNullOrEmpty(invoice.Pr))
{
return new(false, $"blocked: failed to get invoice from {parsedTarget}");
}
if (!await _albyApi.PayInvoice(invoice.Pr))
{
return new(false, "blocked: failed to pay invoice!");
}
var seenEvent = NostrBuf.Encode(ev);
(await _session.UpsertAsync(ref seenId, ref seenEvent)).Complete();
return new(true, "Zapped!");
}
return new(false, "blocked: kind not accepted, no zap for you!");
}
public void Dispose()
{
}
private int? MapKindToAmount(NostrKind k)
{
switch (k)
{
case NostrKind.Reaction: return 5;
case NostrKind.GenericRepost: return 10;
}
return default;
}
}