Compare commits

..

10 Commits

Author SHA1 Message Date
00e724ffa4
Fix profile request
All checks were successful
continuous-integration/drone Build is passing
2024-09-19 12:55:04 +01:00
2cd43ee2d6
Disable pubkey stats 2024-09-05 10:12:40 +01:00
b72957fc4b
Use rust nostr-services for events/profiles 2024-09-05 10:11:20 +01:00
4e89ba6775
Use new API for profiles/events 2024-09-05 10:11:20 +01:00
fd12f5e5ba
Disable relay scraper 2024-08-08 15:49:25 +01:00
0a42894a6f fix: load more close relays to allow for duplicate results 2024-03-15 16:30:22 +00:00
4ec40984d6 fix: remove duplicates from FindCloseRelays 2024-03-15 16:29:09 +00:00
7ad353fdcd Update sets 2024-03-07 19:42:15 +00:00
03a805e622 Add avatars 2024-03-07 19:41:53 +00:00
4e1c62ac90 Fix opengraph 2024-03-07 11:37:16 +00:00
231 changed files with 129 additions and 63 deletions

View File

@ -18,12 +18,13 @@ public class NostrServicesClient
public async Task<CompactProfile?> Profile(string id) public async Task<CompactProfile?> Profile(string id)
{ {
return await Get<CompactProfile>(HttpMethod.Get, $"/api/v1/export/profile/{id}"); var ev = await Get<NostrEvent>(HttpMethod.Get, $"https://nostr-rs.api.v0l.io/event/0/{id}");
return ev != default ? CompactProfile.FromNostrEvent(ev) : default;
} }
public async Task<NostrEvent?> Event(string id) public async Task<NostrEvent?> Event(string id)
{ {
return await Get<NostrEvent>(HttpMethod.Get, $"/api/v1/export/{id}"); return await Get<NostrEvent>(HttpMethod.Get, $"https://nostr-rs.api.v0l.io/event/{id}");
} }
public async Task<LinkPreviewData?> LinkPreview(string url) public async Task<LinkPreviewData?> LinkPreview(string url)

View File

@ -0,0 +1,30 @@
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");
}
}

View File

@ -14,17 +14,8 @@ namespace NostrServices.Controllers;
/// Add OpenGraph tags to html documents /// Add OpenGraph tags to html documents
/// </summary> /// </summary>
[Route("/api/v1/opengraph")] [Route("/api/v1/opengraph")]
public class OpenGraphController : Controller public class OpenGraphController(ILogger<OpenGraphController> logger, RedisStore redisStore) : Controller
{ {
private readonly ILogger<OpenGraphController> _logger;
private readonly RedisStore _redisStore;
public OpenGraphController(ILogger<OpenGraphController> logger, RedisStore redisStore)
{
_logger = logger;
_redisStore = redisStore;
}
/// <summary> /// <summary>
/// Inject opengraph tags into provided html /// Inject opengraph tags into provided html
/// </summary> /// </summary>
@ -63,7 +54,7 @@ public class OpenGraphController : Controller
{ {
if (nid.Hrp is "nevent" or "note" or "naddr") if (nid.Hrp is "nevent" or "note" or "naddr")
{ {
var ev = await _redisStore.GetEvent(nid); var ev = await redisStore.GetEvent(nid);
if (ev != default) if (ev != default)
{ {
var tags = MetaTagsToElements(await GetEventTags(ev)); var tags = MetaTagsToElements(await GetEventTags(ev));
@ -75,7 +66,7 @@ public class OpenGraphController : Controller
} }
else if (nid.Hrp is "nprofile" or "npub") 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 meta = await GetProfileMeta(profile);
var tags = MetaTagsToElements([ var tags = MetaTagsToElements([
@ -98,7 +89,7 @@ public class OpenGraphController : Controller
} }
catch (Exception ex) when (ex is not TaskCanceledException) 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());
} }
} }
@ -109,17 +100,20 @@ public class OpenGraphController : Controller
{ {
var ret = new List<KeyValuePair<string, string>>(); 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"; var name = profile?.Name ?? "Nostrich";
switch (ev.Kind) switch (ev.Kind)
{ {
case (long)NostrKind.LiveEvent: case (long)NostrKind.LiveEvent:
{ {
var host = ev.Tags.FirstOrDefault(a => a.Key == "p" && a.Values[1] == "host")?.Values[0] ?? ev.PubKey.ToHex(); var host = ev.Tags.FirstOrDefault(a => a.Key == "p" && a.Values[^1] == "host")
var hostProfile = await _redisStore.GetProfile(host); ?.Values[0] ??
ev.PubKey.ToHex();
var hostProfile = await redisStore.GetProfile(host);
var hostName = hostProfile?.Name ?? profile?.Name ?? "Nostrich"; var hostName = hostProfile?.Name ?? profile?.Name ?? "Nostrich";
var stream = ev.GetFirstTagValue("streaming") ?? ev.GetFirstTagValue("recording") ?? ""; 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>[] ret.AddRange(new KeyValuePair<string, string>[]
{ {
new("og:type", "video.other"), new("og:type", "video.other"),
@ -145,7 +139,8 @@ public class OpenGraphController : Controller
case 1_313: // stream clip case 1_313: // stream clip
{ {
var stream = ev.GetFirstTagValue("r")!; 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>[] ret.AddRange(new KeyValuePair<string, string>[]
{ {
new("og:type", "video.other"), new("og:type", "video.other"),
@ -212,14 +207,23 @@ public class OpenGraphController : Controller
foreach (var ex in tags) foreach (var ex in tags)
{ {
var tag = doc.CreateElement(ex.Element); 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);
foreach (var attr in ex.Attributes) foreach (var attr in ex.Attributes)
{ {
tag.SetAttribute(attr.Key, attr.Value); tag.SetAttribute(attr.Key, attr.Value);
} }
doc.Head?.AppendChild(tag);
var isOgTitle = ex.Attributes.Any(a => a is { Key: "property", Value: "og:title" }); var isOgTitle = ex.Attributes.Any(a => a is { Key: "property", Value: "og:title" });
if (isOgTitle && doc.Head != default) if (isOgTitle && doc.Head != default)
{ {
@ -256,6 +260,15 @@ public class OpenGraphController : Controller
descriptionTag.SetAttribute("content", ogDesc.Value); descriptionTag.SetAttribute("content", ogDesc.Value);
} }
} }
continue;
IElement AddNewTag(HeadElement he)
{
var tx = doc!.CreateElement(he.Element);
doc.Head?.AppendChild(tx);
return tx;
}
} }
return doc; return doc;

View File

@ -35,8 +35,8 @@ public static class Program
builder.Services.AddTransient<CacheRelay>(); builder.Services.AddTransient<CacheRelay>();
builder.Services.AddNostrEventHandlers(); builder.Services.AddNostrEventHandlers();
builder.Services.AddHostedService<NostrListener.NostrListenerLifetime>(); builder.Services.AddHostedService<NostrListener.NostrListenerLifetime>();
builder.Services.AddHostedService<RelayScraperService>(); //builder.Services.AddHostedService<RelayScraperService>();
builder.Services.AddHostedService<PubkeyStatsService>(); //builder.Services.AddHostedService<PubkeyStatsService>();
builder.Services.AddControllers().AddNewtonsoftJson(opt => builder.Services.AddControllers().AddNewtonsoftJson(opt =>
{ {

View File

@ -1,5 +1,6 @@
using NBitcoin; using NBitcoin;
using Nostr.Client.Identifiers; using Nostr.Client.Identifiers;
using Nostr.Client.Json;
using Nostr.Client.Messages; using Nostr.Client.Messages;
using Nostr.Client.Utils; using Nostr.Client.Utils;
using NostrServices.Client; using NostrServices.Client;
@ -13,56 +14,67 @@ public class RedisStore
private static readonly TimeSpan DefaultExpire = TimeSpan.FromDays(90); private static readonly TimeSpan DefaultExpire = TimeSpan.FromDays(90);
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly ConnectionMultiplexer _connection; private readonly ConnectionMultiplexer _connection;
private readonly HttpClient _client;
public RedisStore(ConnectionMultiplexer connection) public RedisStore(ConnectionMultiplexer connection, HttpClient client)
{ {
_connection = connection; _connection = connection;
_client = client;
_database = connection.GetDatabase(); _database = connection.GetDatabase();
} }
public async Task<bool> StoreEvent(CompactEvent ev, TimeSpan? expiry = null) public async Task<bool> StoreEvent(CompactEvent ev, TimeSpan? expiry = null)
{ {
if (await _database.SetAsync(EventKey(ev.ToIdentifier()), ev, expiry ?? DefaultExpire)) return true;
{
return await _database.SetAddAsync(UserEventsKey(ev.PubKey.ToHex()), ev.Id);
}
return false;
} }
public async Task<CompactEvent?> GetEvent(NostrIdentifier id) public async Task<CompactEvent?> GetEvent(NostrIdentifier id)
{ {
var ek = EventKey(id); try
var ev = await _database.GetAsync<CompactEvent>(ek);
if (ev != default)
{ {
await _database.KeyExpireAsync(ek, DefaultExpire); 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 ev; return default;
} }
public async Task<bool> StoreProfile(CompactProfile meta, TimeSpan? expiry = null) public async Task<bool> StoreProfile(CompactProfile meta, TimeSpan? expiry = null)
{ {
var oldProfile = await GetProfile(meta.PubKey.ToHex()); return true;
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) public async Task<CompactProfile?> GetProfile(string id)
{ {
var pk = ProfileKey(id); try
var profile = await _database.GetAsync<CompactProfile>(pk);
if (profile != default)
{ {
await _database.KeyExpireAsync(pk, DefaultExpire); 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 profile; return default;
} }
public async Task<HashSet<Uri>> GetKnownRelays() public async Task<HashSet<Uri>> GetKnownRelays()
@ -95,13 +107,17 @@ public class RedisStore
if ((old?.Created ?? 0) < relays.Created) if ((old?.Created ?? 0) < relays.Created)
{ {
var removed = old?.Relays.Where(a => relays.Relays.All(b => b.Url != a.Url)); 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) 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()); await _database.SetAddAsync("relays", added.Select(a => (RedisValue)a.Url.ToString()).ToArray());
return await _database.SetAsync(UserRelaysKey(pubkey), relays, DefaultExpire); return await _database.SetAsync(UserRelaysKey(pubkey), relays, DefaultExpire);
@ -136,16 +152,22 @@ public class RedisStore
public async Task<List<RelayDistance>> FindCloseRelays(double lat, double lon, int radius = 50_000, int count = 10) public async Task<List<RelayDistance>> FindCloseRelays(double lat, double lon, int radius = 50_000, int count = 10)
{ {
var ret = new List<RelayDistance>(); var ret = new Dictionary<Uri, RelayDistance>();
var geoRelays = await _database.GeoSearchAsync(RelayPositionKey(), lon, lat, new GeoSearchCircle(radius), count); var geoRelays =
await _database.GeoSearchAsync(RelayPositionKey(), lon, lat, new GeoSearchCircle(radius), count * 2);
foreach (var gr in geoRelays) foreach (var gr in geoRelays)
{ {
if (ret.Count == count)
{
break;
}
var id = ((string)gr.Member!).Split('\x1'); var id = ((string)gr.Member!).Split('\x1');
var u = new Uri(id[0]); var u = new Uri(id[0]);
var info = await GetRelay(u); var info = await GetRelay(u);
if (info != default) if (info != default && !ret.ContainsKey(u))
{ {
ret.Add(new() ret.Add(u, new()
{ {
Distance = gr.Distance.HasValue ? (long)gr.Distance.Value : 0, Distance = gr.Distance.HasValue ? (long)gr.Distance.Value : 0,
Relay = info, Relay = info,
@ -154,7 +176,7 @@ public class RedisStore
} }
} }
return ret; return ret.Values.ToList();
} }
public async IAsyncEnumerable<string> EnumerateProfiles() public async IAsyncEnumerable<string> EnumerateProfiles()

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Some files were not shown because too many files have changed in this diff Show More