using Amazon; using Amazon.Runtime; using Amazon.S3; using Igdb; using MaxMind.GeoIP2; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Nostr.Client.Identifiers; using Nostr.Client.Json; using Nostr.Client.Keys; using Nostr.Client.Messages; using NostrStreamer.ApiModel; using NostrStreamer.Database; namespace NostrStreamer; public static class Extensions { public static NostrEvent? GetEvent(this UserStream us) { return JsonConvert.DeserializeObject(us.Event, NostrSerializer.Settings); } public static string GetPubKey(this Config cfg) { return NostrPrivateKey.FromBech32(cfg.PrivateKey).DerivePublicKey().Hex; } public static NostrPrivateKey GetPrivateKey(this Config cfg) { return NostrPrivateKey.FromBech32(cfg.PrivateKey); } public static List GetVariants(this IngestEndpoint ep) { return ep.Capabilities .Where(a => a.StartsWith("variant")) .Select(Variant.FromString).ToList(); } public static AmazonS3Client CreateClient(this S3BlobConfig c) { return new AmazonS3Client(new BasicAWSCredentials(c.AccessKey, c.SecretKey), new AmazonS3Config { RegionEndpoint = !string.IsNullOrEmpty(c.Region) ? RegionEndpoint.GetBySystemName(c.Region) : null, ServiceURL = c.ServiceUrl.ToString(), UseHttp = c.ServiceUrl.Scheme == "http", ForcePathStyle = true, }); } public static string[] SplitTags(this UserStream stream) { return !string.IsNullOrEmpty(stream.Tags) ? stream.Tags.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) : Array.Empty(); } public static (double lat, double lon)? GetLocation(this HttpContext ctx, IGeoIP2DatabaseReader db) { var ip = ctx.GetRealIp(); var loc = db.TryCity(ip, out var city) ? city?.Location : default; if ((loc?.Latitude.HasValue ?? false) && loc.Longitude.HasValue) { return (loc.Latitude.Value, loc.Longitude.Value); } return default; } public static string GetRealIp(this HttpContext ctx) { var cci = ctx.Request.Headers.TryGetValue("CF-Connecting-IP", out var xx) ? xx.ToString() : null; if (!string.IsNullOrEmpty(cci)) { return cci; } var xff = ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var x) ? x.ToString() : null; if (!string.IsNullOrEmpty(xff)) { return xff.Split(",", StringSplitOptions.RemoveEmptyEntries).First(); } return ctx.Connection.RemoteIpAddress!.ToString(); } public static double GetDistance(double longitude, double latitude, double otherLongitude, double otherLatitude) { var d1 = latitude * (Math.PI / 180.0); var num1 = longitude * (Math.PI / 180.0); var d2 = otherLatitude * (Math.PI / 180.0); var num2 = otherLongitude * (Math.PI / 180.0) - num1; var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0); return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3))); } public static string GetHost(this NostrEvent ev) { var hostTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[2] == "host") ?.AdditionalData[0]; if (!string.IsNullOrEmpty(hostTag)) { return hostTag; } return ev.Pubkey!; } public static NostrIdentifier ToIdentifier(this NostrEvent ev) { if ((long)ev.Kind is >= 30_000 and < 40_000) { return new NostrAddressIdentifier(ev.Tags!.FindFirstTagValue("d")!, ev.Pubkey!, null, ev.Kind); } return new NostrEventIdentifier(ev.Id!, ev.Pubkey, null, ev.Kind); } public static NostrIdentifier ToIdentifier(this UserStream stream) { var ev = NostrJson.Deserialize(stream.Event); return ev!.ToIdentifier(); } public static GameInfo ToGameInfo(this Game a) { return new GameInfo { Id = $"igdb:{a.Id}", Name = a.Name, Cover = $"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/{a.Cover?.ImageId}.jpg", Genres = a.Genres.Select(b => b.Name).ToList() }; } public static async Task CopyLastStreamDetails(this UserStream stream, StreamerContext db) { var lastStream = await db.Streams .AsNoTracking() .Where(a => a.PubKey == stream.PubKey) .OrderByDescending(a => a.Starts) .FirstOrDefaultAsync(); stream.Title = lastStream?.Title; stream.Summary = lastStream?.Summary; stream.Image = lastStream?.Image; stream.ContentWarning = lastStream?.ContentWarning; stream.Tags = lastStream?.Tags; stream.Goal = lastStream?.Goal; } public static void PatchStream(this UserStream stream, PatchEvent ev) { stream.Title = ev.Title; stream.Summary = ev.Summary; stream.Image = ev.Image; stream.ContentWarning = ev.ContentWarning; stream.Tags = ev.Tags is { Length: > 0 } ? string.Join(",", ev.Tags) : null; stream.Goal = ev.Goal; } } public class Variant { public int Width { get; init; } public int Height { get; init; } public int Bandwidth { get; init; } public string SourceName => Bandwidth == int.MaxValue ? "source" : $"{Height}h"; /// /// variant:{px}h:{bandwidth} /// /// /// /// public static Variant FromString(string str) { if (str.Equals("variant:source", StringComparison.InvariantCultureIgnoreCase)) { return new() { Width = 0, Height = 0, Bandwidth = int.MaxValue }; } var strSplit = str.Split(":"); if (strSplit.Length != 3 || !int.TryParse(strSplit[1][..^1], out var h) || !int.TryParse(strSplit[2], out var b)) { throw new FormatException(); } return new() { Height = h, Width = (int)Math.Ceiling(h / 9m * 16m), Bandwidth = b }; } public override string ToString() { if (Bandwidth == int.MaxValue) { return "variant:source"; } return $"variant:{SourceName}:{Bandwidth}"; } public string ToResolutionArg() { if (Bandwidth == int.MaxValue) { return string.Empty; } return $"RESOLUTION={Width}x{Height}"; } public string ToBandwidthArg() { if (Bandwidth == int.MaxValue) { return $"BANDWIDTH={20_000_000}"; } return $"BANDWIDTH={Bandwidth * 1000}"; } }