Files
NostrServices/NostrServices/Services/RedisStore.cs

384 lines
10 KiB
C#

using NBitcoin;
using Nostr.Client.Identifiers;
using Nostr.Client.Json;
using Nostr.Client.Messages;
using Nostr.Client.Utils;
using NostrServices.Client;
using ProtoBuf;
using StackExchange.Redis;
namespace NostrServices.Services;
public class RedisStore
{
private static readonly TimeSpan DefaultExpire = TimeSpan.FromDays(90);
private readonly IDatabase _database;
private readonly ConnectionMultiplexer _connection;
private readonly HttpClient _client;
public RedisStore(ConnectionMultiplexer connection, HttpClient client)
{
_connection = connection;
_client = client;
_database = connection.GetDatabase();
}
public async Task<bool> StoreEvent(CompactEvent ev, TimeSpan? expiry = null)
{
return true;
}
public async Task<CompactEvent?> GetEvent(NostrIdentifier id)
{
try
{
var ev = await _client.GetAsync($"https://nostr-rs.api.v0l.io/event/{id.ToBech32()}");
if (ev.IsSuccessStatusCode)
{
var obj = NostrJson.Deserialize<NostrEvent>(await ev.Content.ReadAsStringAsync());
if (obj != default)
{
return CompactEvent.FromNostrEvent(obj);
}
}
}
catch
{
// ignored
}
return default;
}
public async Task<bool> StoreProfile(CompactProfile meta, TimeSpan? expiry = null)
{
return true;
}
public async Task<CompactProfile?> GetProfile(string id)
{
try
{
var ev = await _client.GetAsync($"https://nostr-rs.api.v0l.io/event/0/{id}");
if (ev.IsSuccessStatusCode)
{
var evo = NostrJson.Deserialize<NostrEvent>(await ev.Content.ReadAsStringAsync());
if (evo != default)
{
return CompactProfile.FromNostrEvent(evo);
}
}
}
catch
{
// ignored
}
return default;
}
public async Task<HashSet<Uri>> GetKnownRelays()
{
var ret = new HashSet<Uri>();
await foreach (var r in _database.SetScanAsync("relays"))
{
if (r.HasValue)
{
ret.Add(new Uri(r!));
}
}
return ret;
}
public async Task<bool> StoreRelay(RelayInfo cri)
{
return await _database.SetAsync(RelayKey(cri.Url), cri, DefaultExpire);
}
public async Task<RelayInfo?> GetRelay(Uri u)
{
return await _database.GetAsync<RelayInfo>(RelayKey(u));
}
public async Task<bool> StoreUserRelays(string pubkey, UserRelaySettings relays)
{
var old = await GetUserRelays(pubkey);
if ((old?.Created ?? 0) < relays.Created)
{
var removed = old?.Relays.Where(a => relays.Relays.All(b => b.Url != a.Url));
var added = old == default
? relays.Relays
: relays.Relays.Where(a => old.Relays.All(b => b.Url != a.Url)).ToList();
if (removed != default)
{
await Task.WhenAll(removed.Select(a =>
_database.SetRemoveAsync(RelayUsersKey(a.Url), Convert.FromHexString(pubkey))));
}
await Task.WhenAll(added.Select(a =>
_database.SetAddAsync(RelayUsersKey(a.Url), Convert.FromHexString(pubkey))));
await _database.SetAddAsync("relays", added.Select(a => (RedisValue)a.Url.ToString()).ToArray());
return await _database.SetAsync(UserRelaysKey(pubkey), relays, DefaultExpire);
}
return false;
}
public async Task<UserRelaySettings?> GetUserRelays(string pubkey)
{
return await _database.GetAsync<UserRelaySettings>(UserRelaysKey(pubkey));
}
public async Task<Dictionary<Uri, long>> CountRelayUsers()
{
var allRelays = await GetKnownRelays();
var tasks = allRelays.Select(a => (Url: a, Task: _database.SetLengthAsync(RelayUsersKey(a)))).ToList();
await Task.WhenAll(tasks.Select(a => a.Task));
return tasks.ToDictionary(a => a.Url, b => b.Task.Result);
}
public async Task<long> CountUserEvents(string pubkey)
{
return await _database.SetLengthAsync(UserEventsKey(pubkey));
}
public async Task StoreRelayPosition(Uri u, string ipAddress, double lat, double lon)
{
await _database.GeoAddAsync(RelayPositionKey(), lon, lat, $"{u}\x1{ipAddress}");
}
public async Task<List<RelayDistance>> FindCloseRelays(double lat, double lon, int radius = 50_000, int count = 10)
{
var ret = new Dictionary<Uri, RelayDistance>();
var geoRelays =
await _database.GeoSearchAsync(RelayPositionKey(), lon, lat, new GeoSearchCircle(radius), count * 2);
foreach (var gr in geoRelays)
{
if (ret.Count == count)
{
break;
}
var id = ((string)gr.Member!).Split('\x1');
var u = new Uri(id[0]);
var info = await GetRelay(u);
if (info != default && !ret.ContainsKey(u))
{
ret.Add(u, new()
{
Distance = gr.Distance.HasValue ? (long)gr.Distance.Value : 0,
Relay = info,
IpAddress = id[1]
});
}
}
return ret.Values.ToList();
}
public async IAsyncEnumerable<string> EnumerateProfiles()
{
var servers = _connection.GetServers();
foreach (var server in servers)
{
await foreach (var k in server.KeysAsync())
{
var stringKey = (string)k!;
if (stringKey.StartsWith("profile:") && stringKey.Length == 64 + 8)
{
yield return stringKey[8..];
}
}
}
}
public async Task<bool> StorePubkeyStats(List<PubKeyStat> stats)
{
return await _database.SetAsync(UserStatsKey(), stats);
}
public async Task<List<PubKeyStat>?> GetPubkeyStats()
{
return await _database.GetAsync<List<PubKeyStat>>(UserStatsKey());
}
private string EventKey(NostrIdentifier id)
{
if (id is NostrAddressIdentifier naddr)
{
return $"event:{naddr.Kind}:{naddr.Pubkey}:{naddr.Identifier}";
}
return $"event:{id.Special}";
}
private static string ProfileKey(string id) => $"profile:{id}";
private static string RelayKey(Uri relay) => $"relay:${relay}";
private static string RelayPositionKey() => $"relays:geo";
private static string RelayUsersKey(Uri relay) => $"relay:{relay}:users";
private static string UserRelaysKey(string pubkey) => $"profile:{pubkey}:relays";
private static string UserEventsKey(string pubkey) => $"profile:{pubkey}:events";
private static string UserStatsKey() => "profile-stats";
}
[ProtoContract]
public class CompactEventTag
{
[ProtoMember(1)]
public string Key { get; init; } = null!;
[ProtoMember(2)]
public List<string> Values { get; init; } = null!;
}
[ProtoContract]
public class CompactEvent
{
[ProtoMember(1)]
public byte[] Id { get; init; } = null!;
[ProtoMember(2)]
public byte[] PubKey { get; init; } = null!;
[ProtoMember(3)]
public long Created { get; init; }
[ProtoMember(4)]
public string Content { get; init; } = null!;
[ProtoMember(5)]
public List<CompactEventTag> Tags { get; init; } = new();
[ProtoMember(6)]
public byte[] Sig { get; init; } = null!;
[ProtoMember(7)]
public long Kind { get; init; }
public static CompactEvent FromNostrEvent(NostrEvent ev)
{
return new CompactEvent
{
Id = Convert.FromHexString(ev.Id!),
PubKey = Convert.FromHexString(ev.Pubkey!),
Kind = (long)ev.Kind,
Created = ev.CreatedAt!.Value.ToUnixTimestamp(),
Content = ev.Content!,
Tags = ev.Tags!.Select(a => new CompactEventTag
{
Key = a.TagIdentifier,
Values = a.AdditionalData.ToList()
}).ToList(),
Sig = Convert.FromHexString(ev.Sig!)
};
}
public NostrEvent ToNostrEvent()
{
return new NostrEvent()
{
Id = Id.ToHex(),
Pubkey = PubKey.ToHex(),
CreatedAt = DateTimeOffset.FromUnixTimeSeconds(Created).UtcDateTime,
Content = Content,
Tags = new(Tags.Select(a => new NostrEventTag(a.Key, a.Values.ToArray()))),
Sig = Sig.ToHex()
};
}
}
[ProtoContract]
public class RelayDistance
{
[ProtoMember(1)]
public long Distance { get; init; }
[ProtoMember(2)]
public string IpAddress { get; init; } = null!;
[ProtoMember(3)]
public RelayInfo Relay { get; init; } = null!;
}
[ProtoContract]
public class RelayInfo
{
[ProtoMember(1)]
public Uri Url { get; init; } = null!;
[ProtoMember(2)]
public long FirstSeen { get; init; }
[ProtoMember(3)]
public long LastSeen { get; init; }
[ProtoMember(4)]
public long Users { get; init; }
[ProtoMember(5)]
public List<Position> Positions { get; init; } = new();
[ProtoMember(6)]
public bool? IsPaid { get; init; }
[ProtoMember(7)]
public bool? IsWriteRestricted { get; init; }
[ProtoMember(8)]
public string? Description { get; init; }
}
[ProtoContract]
public class Position
{
[ProtoMember(1)]
public double Lat { get; init; }
[ProtoMember(2)]
public double Lon { get; init; }
[ProtoMember(3)]
public string? Country { get; init; }
[ProtoMember(4)]
public string? City { get; init; }
[ProtoMember(6)]
public string IpAddress { get; init; } = null!;
}
[ProtoContract]
public class RelayEndpoint
{
[ProtoMember(1)]
public Uri Relay { get; init; } = null!;
[ProtoMember(2)]
public string IpAddress { get; init; } = null!;
}
[ProtoContract]
public class UserRelaySettings
{
[ProtoMember(1)]
public long Created { get; init; }
[ProtoMember(2)]
public List<RelaySetting> Relays { get; init; } = new();
}
[ProtoContract]
public class RelaySetting
{
[ProtoMember(1)]
public Uri Url { get; init; } = null!;
[ProtoMember(2)]
public bool Read { get; init; }
[ProtoMember(3)]
public bool Write { get; init; }
}