Compare commits
No commits in common. "00e724ffa4b210ced1aff569d9a7ef80ee5c47e6" and "9ba3fc118d03538452d2e2b9bb13dce499b19c8d" have entirely different histories.
00e724ffa4
...
9ba3fc118d
@ -18,13 +18,12 @@ public class NostrServicesClient
|
||||
|
||||
public async Task<CompactProfile?> Profile(string id)
|
||||
{
|
||||
var ev = await Get<NostrEvent>(HttpMethod.Get, $"https://nostr-rs.api.v0l.io/event/0/{id}");
|
||||
return ev != default ? CompactProfile.FromNostrEvent(ev) : default;
|
||||
return await Get<CompactProfile>(HttpMethod.Get, $"/api/v1/export/profile/{id}");
|
||||
}
|
||||
|
||||
public async Task<NostrEvent?> Event(string id)
|
||||
{
|
||||
return await Get<NostrEvent>(HttpMethod.Get, $"https://nostr-rs.api.v0l.io/event/{id}");
|
||||
return await Get<NostrEvent>(HttpMethod.Get, $"/api/v1/export/{id}");
|
||||
}
|
||||
|
||||
public async Task<LinkPreviewData?> LinkPreview(string url)
|
||||
@ -34,7 +33,7 @@ public class NostrServicesClient
|
||||
|
||||
public async Task<List<RelayDistance>?> CloseRelays(double lat, double lon, int count = 5)
|
||||
{
|
||||
return await Get<List<RelayDistance>>(HttpMethod.Post, $"/api/v1/relays?count={count}", new { lat, lon });
|
||||
return await Get<List<RelayDistance>>(HttpMethod.Post, $"/api/v1/relays?count={count}", new {lat, lon});
|
||||
}
|
||||
|
||||
public async Task<List<RelayDistance>?> TopRelays(int count = 5)
|
||||
@ -65,4 +64,4 @@ public class NostrServicesClient
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace NostrServices.Controllers;
|
||||
|
||||
[Route("/api/v1/avatar")]
|
||||
public class AvatarController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Robohash avatar endpoints
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Available sets: `cyberpunks` / `robots` / `zombies`
|
||||
/// </remarks>
|
||||
/// <param name="set"></param>
|
||||
/// <param name="value">Any value, can contain a file extension, response is only image/webp</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{set}/{value}")]
|
||||
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 86400)]
|
||||
public IActionResult GetAvatar([FromRoute] string set, [FromRoute] string value)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(
|
||||
Encoding.UTF8.GetBytes(Path.GetFileNameWithoutExtension(value)));
|
||||
var hashInt = BitConverter.ToInt32(hash);
|
||||
var dir = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", set);
|
||||
var fileList = Directory.EnumerateFiles(dir, "*.webp").ToList();
|
||||
var pickImg = fileList[Math.Abs(hashInt % fileList.Count)];
|
||||
return PhysicalFile(pickImg, "image/webp");
|
||||
}
|
||||
}
|
@ -14,8 +14,17 @@ namespace NostrServices.Controllers;
|
||||
/// Add OpenGraph tags to html documents
|
||||
/// </summary>
|
||||
[Route("/api/v1/opengraph")]
|
||||
public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore redisStore) : Controller
|
||||
public class OpenGraphController : Controller
|
||||
{
|
||||
private readonly ILogger<OpenGraphController> _logger;
|
||||
private readonly RedisStore _redisStore;
|
||||
|
||||
public OpenGraphController(ILogger<OpenGraphController> logger, RedisStore redisStore)
|
||||
{
|
||||
_logger = logger;
|
||||
_redisStore = redisStore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject opengraph tags into provided html
|
||||
/// </summary>
|
||||
@ -54,7 +63,7 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
{
|
||||
if (nid.Hrp is "nevent" or "note" or "naddr")
|
||||
{
|
||||
var ev = await redisStore.GetEvent(nid);
|
||||
var ev = await _redisStore.GetEvent(nid);
|
||||
if (ev != default)
|
||||
{
|
||||
var tags = MetaTagsToElements(await GetEventTags(ev));
|
||||
@ -66,7 +75,7 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
}
|
||||
else if (nid.Hrp is "nprofile" or "npub")
|
||||
{
|
||||
var profile = await redisStore.GetProfile(nid.Special);
|
||||
var profile = await _redisStore.GetProfile(nid.Special);
|
||||
|
||||
var meta = await GetProfileMeta(profile);
|
||||
var tags = MetaTagsToElements([
|
||||
@ -89,7 +98,7 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
}
|
||||
catch (Exception ex) when (ex is not TaskCanceledException)
|
||||
{
|
||||
logger.LogWarning("Failed to inject event tags: {Message}", ex.ToString());
|
||||
_logger.LogWarning("Failed to inject event tags: {Message}", ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,20 +109,17 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
{
|
||||
var ret = new List<KeyValuePair<string, string>>();
|
||||
|
||||
var profile = await redisStore.GetProfile(ev.PubKey.ToHex());
|
||||
var profile = await _redisStore.GetProfile(ev.PubKey.ToHex());
|
||||
var name = profile?.Name ?? "Nostrich";
|
||||
switch (ev.Kind)
|
||||
{
|
||||
case (long)NostrKind.LiveEvent:
|
||||
{
|
||||
var host = ev.Tags.FirstOrDefault(a => a.Key == "p" && a.Values[^1] == "host")
|
||||
?.Values[0] ??
|
||||
ev.PubKey.ToHex();
|
||||
var hostProfile = await redisStore.GetProfile(host);
|
||||
var host = ev.Tags.FirstOrDefault(a => a.Key == "p" && a.Values[1] == "host")?.Values[0] ?? ev.PubKey.ToHex();
|
||||
var hostProfile = await _redisStore.GetProfile(host);
|
||||
var hostName = hostProfile?.Name ?? profile?.Name ?? "Nostrich";
|
||||
var stream = ev.GetFirstTagValue("streaming") ?? ev.GetFirstTagValue("recording") ?? "";
|
||||
var image = ev.GetFirstTagValue("image") ??
|
||||
hostProfile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
|
||||
var image = ev.GetFirstTagValue("image") ?? hostProfile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
|
||||
ret.AddRange(new KeyValuePair<string, string>[]
|
||||
{
|
||||
new("og:type", "video.other"),
|
||||
@ -139,8 +145,7 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
case 1_313: // stream clip
|
||||
{
|
||||
var stream = ev.GetFirstTagValue("r")!;
|
||||
var image = ev.GetFirstTagValue("image") ??
|
||||
profile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
|
||||
var image = ev.GetFirstTagValue("image") ?? profile?.Picture ?? $"https://robohash.v0l.io/{ev.PubKey.ToHex()}.png";
|
||||
ret.AddRange(new KeyValuePair<string, string>[]
|
||||
{
|
||||
new("og:type", "video.other"),
|
||||
@ -207,24 +212,15 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
|
||||
foreach (var ex in tags)
|
||||
{
|
||||
IElement tag = doc.Head?.Children.FirstOrDefault(a =>
|
||||
{
|
||||
if (ex.Element == "meta")
|
||||
{
|
||||
var metaPropertyA = a.Attributes.FirstOrDefault(b => b.Name == "property");
|
||||
var metaPropertyB = ex.Attributes.FirstOrDefault(b => b is { Key: "property" });
|
||||
return metaPropertyA?.Value == metaPropertyB.Value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}) ?? AddNewTag(ex);
|
||||
|
||||
var tag = doc.CreateElement(ex.Element);
|
||||
foreach (var attr in ex.Attributes)
|
||||
{
|
||||
tag.SetAttribute(attr.Key, attr.Value);
|
||||
}
|
||||
|
||||
var isOgTitle = ex.Attributes.Any(a => a is { Key: "property", Value: "og:title" });
|
||||
doc.Head?.AppendChild(tag);
|
||||
|
||||
var isOgTitle = ex.Attributes.Any(a => a is {Key: "property", Value: "og:title"});
|
||||
if (isOgTitle && doc.Head != default)
|
||||
{
|
||||
var titleTag = doc.Head.QuerySelector("title");
|
||||
@ -242,7 +238,7 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
}
|
||||
}
|
||||
|
||||
var isOgDesc = ex.Attributes.Any(a => a is { Key: "property", Value: "og:description" });
|
||||
var isOgDesc = ex.Attributes.Any(a => a is {Key: "property", Value: "og:description"});
|
||||
if (isOgDesc && doc.Head != default)
|
||||
{
|
||||
var ogDesc = ex.Attributes.FirstOrDefault(a => a.Key is "content");
|
||||
@ -260,15 +256,6 @@ public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore
|
||||
descriptionTag.SetAttribute("content", ogDesc.Value);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
IElement AddNewTag(HeadElement he)
|
||||
{
|
||||
var tx = doc!.CreateElement(he.Element);
|
||||
doc.Head?.AppendChild(tx);
|
||||
return tx;
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
@ -308,4 +295,4 @@ class CachedMeta
|
||||
|
||||
[ProtoMember(5)]
|
||||
public CompactEvent? Event { get; init; }
|
||||
}
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ public static class Program
|
||||
builder.Services.AddTransient<CacheRelay>();
|
||||
builder.Services.AddNostrEventHandlers();
|
||||
builder.Services.AddHostedService<NostrListener.NostrListenerLifetime>();
|
||||
//builder.Services.AddHostedService<RelayScraperService>();
|
||||
//builder.Services.AddHostedService<PubkeyStatsService>();
|
||||
builder.Services.AddHostedService<RelayScraperService>();
|
||||
builder.Services.AddHostedService<PubkeyStatsService>();
|
||||
|
||||
builder.Services.AddControllers().AddNewtonsoftJson(opt =>
|
||||
{
|
||||
|
@ -1,6 +1,5 @@
|
||||
using NBitcoin;
|
||||
using Nostr.Client.Identifiers;
|
||||
using Nostr.Client.Json;
|
||||
using Nostr.Client.Messages;
|
||||
using Nostr.Client.Utils;
|
||||
using NostrServices.Client;
|
||||
@ -14,67 +13,56 @@ 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)
|
||||
public RedisStore(ConnectionMultiplexer connection)
|
||||
{
|
||||
_connection = connection;
|
||||
_client = client;
|
||||
_database = connection.GetDatabase();
|
||||
}
|
||||
|
||||
public async Task<bool> StoreEvent(CompactEvent ev, TimeSpan? expiry = null)
|
||||
{
|
||||
return true;
|
||||
if (await _database.SetAsync(EventKey(ev.ToIdentifier()), ev, expiry ?? DefaultExpire))
|
||||
{
|
||||
return await _database.SetAddAsync(UserEventsKey(ev.PubKey.ToHex()), ev.Id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<CompactEvent?> GetEvent(NostrIdentifier id)
|
||||
{
|
||||
try
|
||||
var ek = EventKey(id);
|
||||
var ev = await _database.GetAsync<CompactEvent>(ek);
|
||||
if (ev != default)
|
||||
{
|
||||
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
|
||||
await _database.KeyExpireAsync(ek, DefaultExpire);
|
||||
}
|
||||
|
||||
return default;
|
||||
return ev;
|
||||
}
|
||||
|
||||
public async Task<bool> StoreProfile(CompactProfile meta, TimeSpan? expiry = null)
|
||||
{
|
||||
return true;
|
||||
var oldProfile = await GetProfile(meta.PubKey.ToHex());
|
||||
if ((oldProfile?.Created ?? new DateTime()) < meta.Created)
|
||||
{
|
||||
return await _database.SetAsync(ProfileKey(meta.PubKey.ToHex()), meta, expiry ?? DefaultExpire);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<CompactProfile?> GetProfile(string id)
|
||||
{
|
||||
try
|
||||
var pk = ProfileKey(id);
|
||||
var profile = await _database.GetAsync<CompactProfile>(pk);
|
||||
if (profile != default)
|
||||
{
|
||||
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
|
||||
await _database.KeyExpireAsync(pk, DefaultExpire);
|
||||
}
|
||||
|
||||
return default;
|
||||
return profile;
|
||||
}
|
||||
|
||||
public async Task<HashSet<Uri>> GetKnownRelays()
|
||||
@ -107,17 +95,13 @@ public class RedisStore
|
||||
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();
|
||||
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(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 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);
|
||||
@ -152,22 +136,16 @@ public class RedisStore
|
||||
|
||||
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);
|
||||
var ret = new List<RelayDistance>();
|
||||
var geoRelays = await _database.GeoSearchAsync(RelayPositionKey(), lon, lat, new GeoSearchCircle(radius), count);
|
||||
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))
|
||||
if (info != default)
|
||||
{
|
||||
ret.Add(u, new()
|
||||
ret.Add(new()
|
||||
{
|
||||
Distance = gr.Distance.HasValue ? (long)gr.Distance.Value : 0,
|
||||
Relay = info,
|
||||
@ -176,7 +154,7 @@ public class RedisStore
|
||||
}
|
||||
}
|
||||
|
||||
return ret.Values.ToList();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<string> EnumerateProfiles()
|
||||
@ -381,4 +359,4 @@ public class RelaySetting
|
||||
|
||||
[ProtoMember(3)]
|
||||
public bool Write { get; init; }
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 199 KiB |
Before Width: | Height: | Size: 203 KiB |
Before Width: | Height: | Size: 203 KiB |
Before Width: | Height: | Size: 265 KiB |
Before Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 195 KiB |
Before Width: | Height: | Size: 266 KiB |
Before Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 242 KiB |
Before Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 239 KiB |
Before Width: | Height: | Size: 254 KiB |
Before Width: | Height: | Size: 251 KiB |
Before Width: | Height: | Size: 249 KiB |
Before Width: | Height: | Size: 289 KiB |
Before Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 195 KiB |
Before Width: | Height: | Size: 217 KiB |
Before Width: | Height: | Size: 185 KiB |
Before Width: | Height: | Size: 298 KiB |
Before Width: | Height: | Size: 366 KiB |
Before Width: | Height: | Size: 330 KiB |
Before Width: | Height: | Size: 355 KiB |
Before Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 317 KiB |
Before Width: | Height: | Size: 349 KiB |
Before Width: | Height: | Size: 341 KiB |
Before Width: | Height: | Size: 281 KiB |
Before Width: | Height: | Size: 304 KiB |
Before Width: | Height: | Size: 428 KiB |
Before Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 211 KiB |
Before Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 248 KiB |
Before Width: | Height: | Size: 243 KiB |
Before Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 251 KiB |
Before Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 268 KiB |
Before Width: | Height: | Size: 245 KiB |
Before Width: | Height: | Size: 237 KiB |
Before Width: | Height: | Size: 219 KiB |
Before Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 201 KiB |
Before Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 172 KiB |
Before Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 235 KiB |
Before Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 299 KiB |
Before Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 261 KiB |
Before Width: | Height: | Size: 242 KiB |
Before Width: | Height: | Size: 296 KiB |
Before Width: | Height: | Size: 251 KiB |
Before Width: | Height: | Size: 290 KiB |
Before Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 315 KiB |
Before Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 187 KiB |
Before Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 242 KiB |
Before Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 219 KiB |
Before Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 225 KiB |