using System.Threading.Tasks.Dataflow; using FASTER.core; using Microsoft.Extensions.Caching.Memory; using Nostr.Client.Json; using Nostr.Client.Keys; using Nostr.Client.Messages; using NostrRelay; using NostrServices.Client; using EventSession = FASTER.core.ClientSession>; namespace PayForReactions; public class ReactionQueue { public readonly BufferBlock Queue = new(); } public class ReactionQueueWorker : BackgroundService { private readonly ILogger _logger; private readonly ReactionQueue _queue; private readonly IMemoryCache _cache; private readonly Config _config; private readonly Lnurl _lnurl; private readonly AlbyApi _albyApi; private readonly NostrServicesClient _nostrServices; private readonly EventSession _session; public ReactionQueueWorker(ILogger logger, ReactionQueue queue, IMemoryCache cache, Config config, Lnurl lnurl, AlbyApi albyApi, NostrServicesClient nostrServices, NostrStore store) { _logger = logger; _queue = queue; _cache = cache; _config = config; _lnurl = lnurl; _albyApi = albyApi; _nostrServices = nostrServices; _session = store.MainStore.For(new SimpleFunctions()).NewSession>(); ; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var target = NostrPublicKey.FromBech32(_config.Target).Hex; var ev = await _queue.Queue.ReceiveAsync(stoppingToken); if (ev.Pubkey == target) { var id = Convert.FromHexString(ev.Id!); var obj = NostrBuf.Encode(ev); (await _session.UpsertAsync(ref id, ref obj, token: stoppingToken)).Complete(); _logger.LogInformation("Got targets event, saving {ev}", NostrJson.Serialize(ev)); continue; } var amount = MapKindToAmount(ev.Kind); if (amount == default) continue; _logger.LogInformation("Got reaction {json}", NostrJson.Serialize(ev)); 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) { _logger.LogInformation("blocked: must be a reaction to {target}", target); continue; } var idBytes = Convert.FromHexString(eTag.AdditionalData[0]); var refEventResult = (await _session.ReadAsync(ref idBytes)).Complete(); if (refEventResult.status.NotFound) { _logger.LogInformation("blocked: parent event not found"); continue; } var seenId = Convert.FromHexString(ev.Id!); var seenIdResult = (await _session.ReadAsync(ref seenId)).Complete(); if (seenIdResult.status.Found) { _logger.LogInformation("blocked: already zapped this one, how dare you!"); continue; } var refEvent = NostrBuf.Decode(refEventResult.output); if (refEvent == default) { _logger.LogInformation("blocked: parent event not found"); continue; } if (refEvent.Pubkey != target) { _logger.LogInformation("blocked: parent event not posted by {target}", target); continue; } var sender = ev.Pubkey!; var senderProfile = await _nostrServices.Profile(NostrPublicKey.FromHex(sender).Bech32); if (senderProfile == default) { _logger.LogInformation("blocked: couldn't find your profile anon!"); continue; } var parsedTarget = Lnurl.ParseLnUrl(senderProfile.Lud16 ?? ""); if (parsedTarget == default) { _logger.LogInformation( "blocked: so sad... couldn't send a zap because you don't have a lightning address in your profile {name}!", senderProfile.Name); continue; } _logger.LogInformation("Starting payment for {name} - {addr}", senderProfile.Name, senderProfile.Lud16); var svc = await _lnurl.LoadAsync(parsedTarget.ToString()); if (svc == default) { _logger.LogInformation("blocked: wallet is down, no zap for you!"); continue; } var keyEventWalletSeen = $"zapaped:{refEvent.Id}:{parsedTarget}"; var eventWalletZapTry = _cache.Get(keyEventWalletSeen); if (eventWalletZapTry > 0) { _logger.LogInformation("blocked: hey i already zapped you! (count={count})", eventWalletZapTry); continue; } _cache.Set(keyEventWalletSeen, ++eventWalletZapTry); 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("relays", "wss://relay.snort.social", "wss://nos.lol", "wss://relay.damus.io"), new NostrEventTag("amount", (amount.Value * 1000).ToString()) ) }; var zapSigned = zap.Sign(key); try { var invoice = await _lnurl.GetInvoiceAsync(svc, amount.Value, "Thanks for your interaction!", zapSigned); if (string.IsNullOrEmpty(invoice.Pr)) { _logger.LogInformation("blocked: failed to get invoice from {target}", parsedTarget); } _logger.LogInformation("Paying invoice {pr}", invoice.Pr); if (!await _albyApi.PayInvoice(invoice.Pr)) { _logger.LogInformation("blocked: failed to pay invoice!"); } var seenEvent = NostrBuf.Encode(ev); (await _session.UpsertAsync(ref seenId, ref seenEvent, token: stoppingToken)).Complete(); _logger.LogInformation("Zapped {name}!", senderProfile.Name); } catch (Exception e) { _logger.LogError(e.ToString()); } } } private int? MapKindToAmount(NostrKind k) { switch (k) { case NostrKind.Reaction: return 50; case NostrKind.GenericRepost: return 100; } return default; } }