Files
SocialPub/PrivaPub/Infrastructure/Statistics/Interactions.cs
T

101 lines
3.9 KiB
C#
Raw Normal View History

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;
e.Features = e.Features?.Select(Word).Where(f => f != default).Distinct().Take(32).ToList();
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");
public static string Wait(int milliseconds) => Bucket("le", milliseconds / 1000, WaitBuckets, "s");
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;
[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();
[GeneratedRegex("^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$")]
private static partial Regex HostName();
}
}