initial commit
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run

This commit is contained in:
i2p
2026-08-27 10:57:58 -06:00
commit 773d05f8f1
1038 changed files with 109261 additions and 0 deletions
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
namespace Pulsar.Server.Statistics
{
public sealed class ClientGeoSnapshot
{
public ClientGeoSnapshot(
int totalClients,
int mappedClients,
int unknownClients,
IReadOnlyList<GeoCountryCount> countries,
DateTime generatedAtUtc,
string? errorMessage = null)
{
TotalClients = totalClients;
MappedClients = mappedClients;
UnknownClients = unknownClients;
Countries = countries ?? Array.Empty<GeoCountryCount>();
GeneratedAtUtc = generatedAtUtc;
ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? null : errorMessage;
}
public int TotalClients { get; }
public int MappedClients { get; }
public int UnknownClients { get; }
public IReadOnlyList<GeoCountryCount> Countries { get; }
public DateTime GeneratedAtUtc { get; }
public string? ErrorMessage { get; }
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
public int UniqueCountryCount => Countries?.Count ?? 0;
public static ClientGeoSnapshot CreateError(string message)
{
return new ClientGeoSnapshot(
0,
0,
0,
Array.Empty<GeoCountryCount>(),
DateTime.UtcNow,
message);
}
}
public sealed class GeoCountryCount
{
public GeoCountryCount(string countryCode2, string countryCode3, string name, int count, double share)
{
CountryCode2 = string.IsNullOrWhiteSpace(countryCode2) ? "" : countryCode2;
CountryCode3 = string.IsNullOrWhiteSpace(countryCode3) ? "" : countryCode3;
Name = string.IsNullOrWhiteSpace(name) ? "Unknown" : name;
Count = count;
Share = share;
}
public string CountryCode2 { get; }
public string CountryCode3 { get; }
public string Name { get; }
public int Count { get; }
public double Share { get; }
}
}
@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Pulsar.Server.Persistence;
namespace Pulsar.Server.Statistics
{
public static class ClientGeoStatisticsService
{
private static readonly IReadOnlyDictionary<string, CountryInfo> CountriesByAlpha2;
private static readonly IReadOnlyDictionary<string, CountryInfo> CountriesByAlpha3;
private static readonly IReadOnlyDictionary<string, CountryInfo> CountriesByName;
private static readonly IReadOnlyDictionary<string, CountryInfo> ManualOverrides;
static ClientGeoStatisticsService()
{
CountriesByAlpha2 = BuildAlpha2Map();
CountriesByAlpha3 = BuildAlpha3Map(CountriesByAlpha2);
CountriesByName = BuildNameMap(CountriesByAlpha2);
ManualOverrides = BuildManualOverrides();
}
public static ClientGeoSnapshot CreateSnapshot(IEnumerable<OfflineClientRecord>? records, DateTime? generatedAtUtc = null)
{
try
{
var list = records?.Where(r => r != null).ToList() ?? new List<OfflineClientRecord>();
if (list.Count == 0)
{
return new ClientGeoSnapshot(0, 0, 0, Array.Empty<GeoCountryCount>(), generatedAtUtc ?? DateTime.UtcNow);
}
var total = list.Count;
var mappedCount = 0;
var grouped = new Dictionary<string, CountryAggregate>(StringComparer.OrdinalIgnoreCase);
foreach (var record in list)
{
var info = ResolveCountry(record.CountryCode, record.Country);
if (info == null)
{
continue;
}
mappedCount++;
var key = info.Value.Alpha3;
if (!grouped.TryGetValue(key, out var aggregate))
{
grouped[key] = new CountryAggregate(info.Value)
{
DisplayName = !string.IsNullOrWhiteSpace(record.Country) ? record.Country.Trim() : info.Value.EnglishName,
Count = 1
};
}
else
{
aggregate.Count++;
}
}
var unknown = total - mappedCount;
var countries = grouped.Values
.Select(aggregate => new GeoCountryCount(
aggregate.Info.Alpha2,
aggregate.Info.Alpha3,
aggregate.DisplayName,
aggregate.Count,
total > 0 ? (double)aggregate.Count / total : 0))
.OrderByDescending(c => c.Count)
.ThenBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
return new ClientGeoSnapshot(total, mappedCount, unknown, countries, generatedAtUtc ?? DateTime.UtcNow);
}
catch (Exception ex)
{
return ClientGeoSnapshot.CreateError(ex.Message);
}
}
private static CountryInfo? ResolveCountry(string? countryCode, string? countryName)
{
if (!string.IsNullOrWhiteSpace(countryCode))
{
var normalized = countryCode.Trim();
if (TryResolveByCode(normalized, out var byCode))
{
return byCode;
}
}
if (!string.IsNullOrWhiteSpace(countryName))
{
var normalizedName = NormalizeName(countryName);
if (CountriesByName.TryGetValue(normalizedName, out var byName))
{
return byName;
}
if (ManualOverrides.TryGetValue(normalizedName, out var manual))
{
return manual;
}
}
return null;
}
private static bool TryResolveByCode(string code, out CountryInfo info)
{
if (code.Length == 2)
{
if (CountriesByAlpha2.TryGetValue(code.ToUpperInvariant(), out info))
{
return true;
}
if (ManualOverrides.TryGetValue($"__ALPHA2__{code.ToUpperInvariant()}", out info))
{
return true;
}
}
else if (code.Length == 3)
{
if (CountriesByAlpha3.TryGetValue(code.ToUpperInvariant(), out info))
{
return true;
}
if (ManualOverrides.TryGetValue($"__ALPHA3__{code.ToUpperInvariant()}", out info))
{
return true;
}
}
info = default;
return false;
}
private static IReadOnlyDictionary<string, CountryInfo> BuildAlpha2Map()
{
var map = new Dictionary<string, CountryInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
{
try
{
var region = new RegionInfo(culture.Name);
if (!map.ContainsKey(region.TwoLetterISORegionName))
{
map[region.TwoLetterISORegionName] = new CountryInfo(region.TwoLetterISORegionName, region.ThreeLetterISORegionName, region.EnglishName);
}
}
catch
{
// Ignore cultures without associated region information.
}
}
return new ReadOnlyDictionary<string, CountryInfo>(map);
}
private static IReadOnlyDictionary<string, CountryInfo> BuildAlpha3Map(IReadOnlyDictionary<string, CountryInfo> alpha2Map)
{
var map = new Dictionary<string, CountryInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in alpha2Map.Values)
{
if (!map.ContainsKey(entry.Alpha3))
{
map[entry.Alpha3] = entry;
}
}
return new ReadOnlyDictionary<string, CountryInfo>(map);
}
private static IReadOnlyDictionary<string, CountryInfo> BuildNameMap(IReadOnlyDictionary<string, CountryInfo> alpha2Map)
{
var map = new Dictionary<string, CountryInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in alpha2Map.Values)
{
var englishName = NormalizeName(entry.EnglishName);
if (!map.ContainsKey(englishName))
{
map[englishName] = entry;
}
}
return new ReadOnlyDictionary<string, CountryInfo>(map);
}
private static IReadOnlyDictionary<string, CountryInfo> BuildManualOverrides()
{
var map = new Dictionary<string, CountryInfo>(StringComparer.OrdinalIgnoreCase)
{
{ NormalizeName("United States"), new CountryInfo("US", "USA", "United States") },
{ NormalizeName("United States of America"), new CountryInfo("US", "USA", "United States") },
{ NormalizeName("United Kingdom"), new CountryInfo("GB", "GBR", "United Kingdom") },
{ NormalizeName("Great Britain"), new CountryInfo("GB", "GBR", "United Kingdom") },
{ NormalizeName("Russia"), new CountryInfo("RU", "RUS", "Russia") },
{ NormalizeName("South Korea"), new CountryInfo("KR", "KOR", "South Korea") },
{ NormalizeName("North Korea"), new CountryInfo("KP", "PRK", "North Korea") },
{ NormalizeName("Viet Nam"), new CountryInfo("VN", "VNM", "Vietnam") },
{ NormalizeName("Czech Republic"), new CountryInfo("CZ", "CZE", "Czech Republic") },
{ NormalizeName("Ivory Coast"), new CountryInfo("CI", "CIV", "Côte d'Ivoire") },
{ NormalizeName("Bolivia"), new CountryInfo("BO", "BOL", "Bolivia") },
{ NormalizeName("Tanzania"), new CountryInfo("TZ", "TZA", "Tanzania") },
{ NormalizeName("Syria"), new CountryInfo("SY", "SYR", "Syria") },
{ NormalizeName("Moldova"), new CountryInfo("MD", "MDA", "Moldova") },
{ NormalizeName("Macau"), new CountryInfo("MO", "MAC", "Macau") },
{ NormalizeName("Hong Kong"), new CountryInfo("HK", "HKG", "Hong Kong") },
{ NormalizeName("Taiwan"), new CountryInfo("TW", "TWN", "Taiwan") },
{ NormalizeName("Cape Verde"), new CountryInfo("CV", "CPV", "Cabo Verde") },
{ NormalizeName("Laos"), new CountryInfo("LA", "LAO", "Laos") },
{ NormalizeName("Kosovo"), new CountryInfo("XK", "XKX", "Kosovo") },
{ NormalizeName("Palestine"), new CountryInfo("PS", "PSE", "Palestine") },
{ NormalizeName("Vatican"), new CountryInfo("VA", "VAT", "Vatican City") },
{ "__ALPHA2__XK", new CountryInfo("XK", "XKX", "Kosovo") },
{ "__ALPHA3__XKX", new CountryInfo("XK", "XKX", "Kosovo") }
};
return new ReadOnlyDictionary<string, CountryInfo>(map);
}
private static string NormalizeName(string value)
{
return new string(value.Trim().ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray());
}
private readonly struct CountryInfo
{
public CountryInfo(string alpha2, string alpha3, string englishName)
{
Alpha2 = alpha2;
Alpha3 = alpha3;
EnglishName = englishName;
}
public string Alpha2 { get; }
public string Alpha3 { get; }
public string EnglishName { get; }
}
private sealed class CountryAggregate
{
public CountryAggregate(CountryInfo info)
{
Info = info;
DisplayName = info.EnglishName;
}
public CountryInfo Info { get; }
public string DisplayName { get; set; }
public int Count { get; set; }
}
}
}
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Pulsar.Server.Persistence;
namespace Pulsar.Server.Statistics
{
public static class ClientStatisticsService
{
private const int DefaultHistoryDays = 14;
private const int MaxPieSegments = 7;
private const int MaxTagRows = 10;
public static ClientStatisticsSnapshot CreateSnapshot(IEnumerable<OfflineClientRecord>? records)
{
try
{
var list = records?.ToList() ?? new List<OfflineClientRecord>();
var nowUtc = DateTime.UtcNow;
var total = list.Count;
var online = list.Count(r => r.IsOnline);
var offline = total - online;
var dayRange = BuildDayRange(nowUtc.Date, DefaultHistoryDays);
var firstSeenGroups = list
.Where(r => r.FirstSeenUtc.HasValue)
.GroupBy(r => r.FirstSeenUtc!.Value.Date)
.ToDictionary(g => g.Key, g => g.Count());
var daily = dayRange
.Select(date => new DailyCount(date, firstSeenGroups.TryGetValue(date, out var value) ? value : 0))
.ToList();
var sevenDayThreshold = nowUtc.Date.AddDays(-6);
var newSevenDayCount = daily.Where(d => d.Date >= sevenDayThreshold).Sum(d => d.Count);
var countryStats = BuildCategoryList(list, r => Normalize(r.Country), total, MaxPieSegments);
var osStats = BuildCategoryList(list, r => Normalize(r.OperatingSystem), total, MaxPieSegments);
var tagStats = BuildCategoryList(list, r => string.IsNullOrWhiteSpace(r.Tag) ? "(none)" : r.Tag, total, MaxTagRows);
return new ClientStatisticsSnapshot(
total,
online,
offline,
newSevenDayCount,
daily,
countryStats,
osStats,
tagStats,
nowUtc);
}
catch (Exception ex)
{
return ClientStatisticsSnapshot.CreateError(ex.Message);
}
}
private static IReadOnlyList<CategoryCount> BuildCategoryList(IEnumerable<OfflineClientRecord> source, Func<OfflineClientRecord, string> selector, int total, int maxItems)
{
var grouped = source
.GroupBy(selector)
.Select(g => new { Label = string.IsNullOrWhiteSpace(g.Key) ? "Unknown" : g.Key.Trim(), Count = g.Count() })
.OrderByDescending(g => g.Count)
.ThenBy(g => g.Label)
.ToList();
if (grouped.Count == 0)
{
return Array.Empty<CategoryCount>();
}
if (maxItems <= 0 || grouped.Count <= maxItems)
{
return grouped
.Select(g => new CategoryCount(g.Label, g.Count, total > 0 ? (double)g.Count / total : 0))
.ToList();
}
var top = grouped.Take(maxItems - 1).ToList();
var otherCount = grouped.Skip(maxItems - 1).Sum(g => g.Count);
var result = top
.Select(g => new CategoryCount(g.Label, g.Count, total > 0 ? (double)g.Count / total : 0))
.ToList();
if (otherCount > 0)
{
result.Add(new CategoryCount("Other", otherCount, total > 0 ? (double)otherCount / total : 0));
}
return result;
}
private static IReadOnlyList<DateTime> BuildDayRange(DateTime endDateInclusive, int days)
{
if (days <= 0)
{
return new[] { endDateInclusive };
}
var start = endDateInclusive.AddDays(-(days - 1));
var result = new List<DateTime>(days);
for (var d = start; d <= endDateInclusive; d = d.AddDays(1))
{
result.Add(d);
}
return result;
}
private static string Normalize(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "Unknown";
}
return value.Trim();
}
}
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Pulsar.Server.Statistics
{
public sealed class ClientStatisticsSnapshot
{
public ClientStatisticsSnapshot(
int totalClients,
int onlineClients,
int offlineClients,
int newClientsLast7Days,
IReadOnlyList<DailyCount> newClientsByDay,
IReadOnlyList<CategoryCount> clientsByCountry,
IReadOnlyList<CategoryCount> clientsByOperatingSystem,
IReadOnlyList<CategoryCount> topTags,
DateTime generatedAtUtc,
string? errorMessage = null)
{
TotalClients = totalClients;
OnlineClients = onlineClients;
OfflineClients = offlineClients;
NewClientsLast7Days = newClientsLast7Days;
NewClientsByDay = newClientsByDay ?? Array.Empty<DailyCount>();
ClientsByCountry = clientsByCountry ?? Array.Empty<CategoryCount>();
ClientsByOperatingSystem = clientsByOperatingSystem ?? Array.Empty<CategoryCount>();
TopTags = topTags ?? Array.Empty<CategoryCount>();
GeneratedAtUtc = generatedAtUtc;
ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? null : errorMessage;
}
public int TotalClients { get; }
public int OnlineClients { get; }
public int OfflineClients { get; }
public int NewClientsLast7Days { get; }
public IReadOnlyList<DailyCount> NewClientsByDay { get; }
public IReadOnlyList<CategoryCount> ClientsByCountry { get; }
public IReadOnlyList<CategoryCount> ClientsByOperatingSystem { get; }
public IReadOnlyList<CategoryCount> TopTags { get; }
public DateTime GeneratedAtUtc { get; }
public string? ErrorMessage { get; }
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
public static ClientStatisticsSnapshot CreateError(string message)
{
return new ClientStatisticsSnapshot(
0,
0,
0,
0,
Array.Empty<DailyCount>(),
Array.Empty<CategoryCount>(),
Array.Empty<CategoryCount>(),
Array.Empty<CategoryCount>(),
DateTime.UtcNow,
message);
}
}
public sealed class DailyCount
{
public DailyCount(DateTime date, int count)
{
Date = date;
Count = count;
}
public DateTime Date { get; }
public int Count { get; }
}
public sealed class CategoryCount
{
public CategoryCount(string label, int count, double share)
{
Label = string.IsNullOrWhiteSpace(label) ? "Unknown" : label;
Count = count;
Share = share;
}
public string Label { get; }
public int Count { get; }
public double Share { get; }
public override string ToString()
{
return $"{Label} ({Count})";
}
}
}