2026-10-03 10:56:13 +02:00
|
|
|
using PrivaPub.Models.Statistics;
|
|
|
|
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
|
|
|
|
namespace PrivaPub.Infrastructure.Statistics
|
|
|
|
|
{
|
|
|
|
|
public static partial class Interactions
|
|
|
|
|
{
|
|
|
|
|
public const string Receive = "recv";
|
|
|
|
|
public const string In = "in";
|
|
|
|
|
public const string Out = "out";
|
|
|
|
|
public const string Http = "http";
|
|
|
|
|
public const string Preview = "preview";
|
|
|
|
|
public const string Crawl = "crawl";
|
|
|
|
|
|
|
|
|
|
public const string Queued = "queued";
|
|
|
|
|
public const string Refused = "refused";
|
|
|
|
|
public const string Accepted = "accepted";
|
|
|
|
|
public const string Dropped = "dropped";
|
|
|
|
|
public const string Rejected = "rejected";
|
|
|
|
|
public const string Ok = "ok";
|
|
|
|
|
public const string Failed = "failed";
|
|
|
|
|
public const string Deferred = "deferred";
|
|
|
|
|
public const string Retry = "retry";
|
|
|
|
|
public const string Dead = "dead";
|
|
|
|
|
|
|
|
|
|
public const string Public = "public";
|
|
|
|
|
public const string Unlisted = "unlisted";
|
|
|
|
|
public const string Private = "private";
|
|
|
|
|
public const string None = "none";
|
|
|
|
|
|
|
|
|
|
public const string Unknown = "-";
|
|
|
|
|
public const string Other = "other";
|
|
|
|
|
|
|
|
|
|
static readonly HashSet<string> Channels = new(StringComparer.Ordinal) { Receive, In, Out, Http, Preview, Crawl };
|
|
|
|
|
static readonly HashSet<string> Audiences = new(StringComparer.Ordinal) { Public, Unlisted, Private, None };
|
|
|
|
|
static readonly HashSet<string> LocalKinds = new(StringComparer.Ordinal) { "person", "group", "application" };
|
|
|
|
|
static readonly HashSet<string> Inboxes = new(StringComparer.Ordinal) { "shared", "personal" };
|
|
|
|
|
static readonly int[] LatencyBuckets = { 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000 };
|
|
|
|
|
static readonly int[] WaitBuckets = { 1, 10, 60, 600, 3600, 21600, 86400 };
|
|
|
|
|
|
|
|
|
|
public static InteractionEvent Sanitize(InteractionEvent e)
|
|
|
|
|
{
|
|
|
|
|
e.Host = Host(e.Host);
|
|
|
|
|
e.Channel = Channels.Contains(e.Channel) ? e.Channel : Other;
|
|
|
|
|
e.Activity = TypeName(e.Activity);
|
|
|
|
|
e.Object = TypeName(e.Object);
|
|
|
|
|
e.Purpose = Word(e.Purpose);
|
|
|
|
|
e.Trigger = Word(e.Trigger);
|
|
|
|
|
e.Outcome = Word(e.Outcome);
|
|
|
|
|
e.Reason = Word(e.Reason);
|
|
|
|
|
e.Audience = e.Audience != default && Audiences.Contains(e.Audience) ? e.Audience : default;
|
|
|
|
|
e.LocalKind = e.Audience is Public or Unlisted && e.LocalKind != default && LocalKinds.Contains(e.LocalKind) ? e.LocalKind : default;
|
|
|
|
|
e.Inbox = e.Inbox != default && Inboxes.Contains(e.Inbox) ? e.Inbox : default;
|
|
|
|
|
e.Signature = e.Signature == default ? default : SignatureName().IsMatch(e.Signature) ? e.Signature : Other;
|
2026-10-03 11:04:42 +02:00
|
|
|
e.Features = e.Features?.Select(Feature).Where(f => f != default).Distinct().Take(32).ToList();
|
2026-10-03 10:56:13 +02:00
|
|
|
if (e.Features?.Count == 0)
|
|
|
|
|
e.Features = default;
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string Host(string host)
|
|
|
|
|
{
|
|
|
|
|
host = host?.Trim().TrimEnd('.').ToLowerInvariant();
|
|
|
|
|
return string.IsNullOrEmpty(host) || host.Length > 253 || !HostName().IsMatch(host) ? Unknown : host;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string HostOf(string uri) =>
|
|
|
|
|
Uri.TryCreate(uri, UriKind.Absolute, out var parsed) ? Host(parsed.Host) : Unknown;
|
|
|
|
|
|
|
|
|
|
public static string Latency(int milliseconds) => Bucket("le", milliseconds, LatencyBuckets, "ms");
|
|
|
|
|
|
2026-10-03 11:30:14 +02:00
|
|
|
public static string Wait(int milliseconds) => Bucket("le", (int)Math.Ceiling(milliseconds / 1000.0), WaitBuckets, "s");
|
2026-10-03 10:56:13 +02:00
|
|
|
|
|
|
|
|
static string Bucket(string prefix, int value, int[] limits, string unit)
|
|
|
|
|
{
|
|
|
|
|
foreach (var limit in limits)
|
|
|
|
|
if (value <= limit)
|
|
|
|
|
return $"{prefix}{limit}{unit}";
|
|
|
|
|
return "inf";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static string TypeName(string value) =>
|
|
|
|
|
value == default ? default : TypeWord().IsMatch(value) ? value : Other;
|
|
|
|
|
|
|
|
|
|
static string Word(string value) =>
|
|
|
|
|
value == default ? default : KebabWord().IsMatch(value) ? value : Other;
|
|
|
|
|
|
2026-10-03 11:04:42 +02:00
|
|
|
static string Feature(string value) =>
|
|
|
|
|
value == default ? default : FeatureName().IsMatch(value) ? value : Other;
|
|
|
|
|
|
2026-10-03 10:56:13 +02:00
|
|
|
[GeneratedRegex("^[A-Za-z][A-Za-z0-9_-]{0,31}$")]
|
|
|
|
|
private static partial Regex TypeWord();
|
|
|
|
|
|
|
|
|
|
[GeneratedRegex("^[a-z0-9][a-z0-9-]{0,39}$")]
|
|
|
|
|
private static partial Regex KebabWord();
|
|
|
|
|
|
|
|
|
|
[GeneratedRegex("^[a-z0-9][a-z0-9:-]{0,39}$")]
|
|
|
|
|
private static partial Regex SignatureName();
|
|
|
|
|
|
2026-10-03 11:04:42 +02:00
|
|
|
[GeneratedRegex("^[a-z0-9][a-z0-9:/+.-]{0,63}$")]
|
|
|
|
|
private static partial Regex FeatureName();
|
|
|
|
|
|
2026-10-03 10:56:13 +02:00
|
|
|
[GeneratedRegex("^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$")]
|
|
|
|
|
private static partial Regex HostName();
|
|
|
|
|
}
|
|
|
|
|
}
|