384 lines
10 KiB
C#
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; }
|
|
} |