From 388e424a17f8c964fb5696377c6bfc58e99998bc Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Thu, 12 Oct 2023 00:33:30 -0700 Subject: [PATCH] Add project to update Patrons.yml from a csv file containing Patreon webhooks, add missing Patrons (#20942) --- Content.PatreonParser/Attributes.cs | 15 +++ .../Content.PatreonParser.csproj | 14 ++ .../CurrentlyEntitledTiers.cs | 9 ++ Content.PatreonParser/Data.cs | 18 +++ Content.PatreonParser/Included.cs | 15 +++ Content.PatreonParser/Patron.cs | 3 + Content.PatreonParser/Program.cs | 125 ++++++++++++++++++ Content.PatreonParser/Relationships.cs | 9 ++ Content.PatreonParser/Root.cs | 12 ++ Content.PatreonParser/Row.cs | 19 +++ Content.PatreonParser/TierData.cs | 12 ++ Resources/Credits/Patrons.yml | 6 + SpaceStation14.sln | 10 ++ 13 files changed, 267 insertions(+) create mode 100644 Content.PatreonParser/Attributes.cs create mode 100644 Content.PatreonParser/Content.PatreonParser.csproj create mode 100644 Content.PatreonParser/CurrentlyEntitledTiers.cs create mode 100644 Content.PatreonParser/Data.cs create mode 100644 Content.PatreonParser/Included.cs create mode 100644 Content.PatreonParser/Patron.cs create mode 100644 Content.PatreonParser/Program.cs create mode 100644 Content.PatreonParser/Relationships.cs create mode 100644 Content.PatreonParser/Root.cs create mode 100644 Content.PatreonParser/Row.cs create mode 100644 Content.PatreonParser/TierData.cs diff --git a/Content.PatreonParser/Attributes.cs b/Content.PatreonParser/Attributes.cs new file mode 100644 index 00000000000..d3ebeb5f7c8 --- /dev/null +++ b/Content.PatreonParser/Attributes.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class Attributes +{ + [JsonPropertyName("full_name")] + public string FullName = default!; + + [JsonPropertyName("pledge_relationship_start")] + public DateTime? PledgeRelationshipStart; + + [JsonPropertyName("title")] + public string Title = default!; +} diff --git a/Content.PatreonParser/Content.PatreonParser.csproj b/Content.PatreonParser/Content.PatreonParser.csproj new file mode 100644 index 00000000000..53b06b265b6 --- /dev/null +++ b/Content.PatreonParser/Content.PatreonParser.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/Content.PatreonParser/CurrentlyEntitledTiers.cs b/Content.PatreonParser/CurrentlyEntitledTiers.cs new file mode 100644 index 00000000000..fd1747efda9 --- /dev/null +++ b/Content.PatreonParser/CurrentlyEntitledTiers.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class CurrentlyEntitledTiers +{ + [JsonPropertyName("data")] + public List Data = default!; +} diff --git a/Content.PatreonParser/Data.cs b/Content.PatreonParser/Data.cs new file mode 100644 index 00000000000..cdc7d79bff5 --- /dev/null +++ b/Content.PatreonParser/Data.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class Data +{ + [JsonPropertyName("id")] + public string Id = default!; + + [JsonPropertyName("type")] + public string Type = default!; + + [JsonPropertyName("attributes")] + public Attributes Attributes = default!; + + [JsonPropertyName("relationships")] + public Relationships Relationships = default!; +} diff --git a/Content.PatreonParser/Included.cs b/Content.PatreonParser/Included.cs new file mode 100644 index 00000000000..ec3363579b9 --- /dev/null +++ b/Content.PatreonParser/Included.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class Included +{ + [JsonPropertyName("id")] + public int Id; + + [JsonPropertyName("type")] + public string Type = default!; + + [JsonPropertyName("attributes")] + public Attributes Attributes = default!; +} diff --git a/Content.PatreonParser/Patron.cs b/Content.PatreonParser/Patron.cs new file mode 100644 index 00000000000..d9943a6a265 --- /dev/null +++ b/Content.PatreonParser/Patron.cs @@ -0,0 +1,3 @@ +namespace Content.PatreonParser; + +public readonly record struct Patron(string FullName, string TierName, DateTime Start); diff --git a/Content.PatreonParser/Program.cs b/Content.PatreonParser/Program.cs new file mode 100644 index 00000000000..60a320006fe --- /dev/null +++ b/Content.PatreonParser/Program.cs @@ -0,0 +1,125 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Content.PatreonParser; +using CsvHelper; +using CsvHelper.Configuration; +using static System.Environment; + +var repository = new DirectoryInfo(Directory.GetCurrentDirectory()).Parent!.Parent!.Parent!.Parent!; +var patronsPath = Path.Combine(repository.FullName, "Resources/Credits/Patrons.yml"); +if (!File.Exists(patronsPath)) +{ + Console.WriteLine($"File {patronsPath} not found."); + return; +} + +Console.WriteLine($"Updating {patronsPath}"); +Console.WriteLine("Is this correct? [Y/N]"); +var response = Console.ReadLine()?.ToUpper(); +if (response != "Y") +{ + Console.WriteLine("Exiting"); + return; +} + +var delimiter = ","; +var hasHeaderRecord = false; +var mode = CsvMode.RFC4180; +var escape = '\''; +Console.WriteLine($""" +Delimiter: {delimiter} +HasHeaderRecord: {hasHeaderRecord} +Mode: {mode} +Escape Character: {escape} +"""); + +Console.WriteLine("Enter the full path to the .csv file containing the Patreon webhook data:"); +var filePath = Console.ReadLine(); +if (filePath == null) +{ + Console.Write("No path given."); + return; +} + +var file = File.OpenRead(filePath); +var csvConfig = new CsvConfiguration(CultureInfo.InvariantCulture) +{ + Delimiter = delimiter, + HasHeaderRecord = hasHeaderRecord, + Mode = mode, + Escape = escape, +}; + +using var reader = new CsvReader(new StreamReader(file), csvConfig); + +// This does not handle tier name changes, but we haven't had any yet +var patrons = new Dictionary(); +var jsonOptions = new JsonSerializerOptions +{ + IncludeFields = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString +}; + +// This assumes that the rows are already sorted by id +foreach (var record in reader.GetRecords()) +{ + if (record.Trigger == "members:create") + continue; + + var content = JsonSerializer.Deserialize(record.ContentJson, jsonOptions)!; + + var id = Guid.Parse(content.Data.Id); + patrons.Remove(id); + + var tiers = content.Data.Relationships.CurrentlyEntitledTiers.Data; + if (tiers.Count == 0) + continue; + else if (tiers.Count > 1) + throw new ArgumentException("Found more than one tier"); + + var tier = tiers[0]; + var tierName = content.Included.SingleOrDefault(i => i.Id == tier.Id && i.Type == tier.Type)?.Attributes.Title; + if (tierName == null) + continue; + + if (record.Trigger == "members:delete") + continue; + + var fullName = content.Data.Attributes.FullName.Trim(); + var pledgeStart = content.Data.Attributes.PledgeRelationshipStart; + + switch (record.Trigger) + { + case "members:create": + break; + case "members:delete": + break; + case "members:update": + patrons.Add(id, new Patron(fullName, tierName, pledgeStart!.Value)); + break; + case "members:pledge:create": + if (pledgeStart == null) + continue; + + patrons.Add(id, new Patron(fullName, tierName, pledgeStart.Value)); + break; + case "members:pledge:delete": + // Deleted pledge but still not expired, expired is handled earlier + patrons.Add(id, new Patron(fullName, tierName, pledgeStart!.Value)); + break; + case "members:pledge:update": + patrons.Add(id, new Patron(fullName, tierName, pledgeStart!.Value)); + break; + } +} + +var patronList = patrons.Values.ToList(); +patronList.Sort((a, b) => a.Start.CompareTo(b.Start)); +var yaml = patronList.Select(p => $""" +- Name: "{p.FullName.Replace("\"", "\\\"")}" + Tier: {p.TierName} +"""); +var output = string.Join(NewLine, yaml) + NewLine; +File.WriteAllText(patronsPath, output); +Console.WriteLine($"Updated {patronsPath} with {patronList.Count} patrons."); diff --git a/Content.PatreonParser/Relationships.cs b/Content.PatreonParser/Relationships.cs new file mode 100644 index 00000000000..f919f812f62 --- /dev/null +++ b/Content.PatreonParser/Relationships.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class Relationships +{ + [JsonPropertyName("currently_entitled_tiers")] + public CurrentlyEntitledTiers CurrentlyEntitledTiers = default!; +} diff --git a/Content.PatreonParser/Root.cs b/Content.PatreonParser/Root.cs new file mode 100644 index 00000000000..2a772a12143 --- /dev/null +++ b/Content.PatreonParser/Root.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class Root +{ + [JsonPropertyName("data")] + public Data Data = default!; + + [JsonPropertyName("included")] + public List Included = default!; +} diff --git a/Content.PatreonParser/Row.cs b/Content.PatreonParser/Row.cs new file mode 100644 index 00000000000..f4d25046165 --- /dev/null +++ b/Content.PatreonParser/Row.cs @@ -0,0 +1,19 @@ +using CsvHelper.Configuration.Attributes; + +namespace Content.PatreonParser; + +// These need to be properties or CSVHelper will not find them +public sealed class Row +{ + [Name("Id"), Index(0)] + public int Id { get; set; } + + [Name("Trigger"), Index(1)] + public string Trigger { get; set; } = default!; + + [Name("Time"), Index(2)] + public DateTime Time { get; set; } + + [Name("Content"), Index(3)] + public string ContentJson { get; set; } = default!; +} diff --git a/Content.PatreonParser/TierData.cs b/Content.PatreonParser/TierData.cs new file mode 100644 index 00000000000..a840b21d359 --- /dev/null +++ b/Content.PatreonParser/TierData.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Content.PatreonParser; + +public sealed class TierData +{ + [JsonPropertyName("id")] + public int Id; + + [JsonPropertyName("type")] + public string Type = default!; +} diff --git a/Resources/Credits/Patrons.yml b/Resources/Credits/Patrons.yml index acb9eb229a5..427a8f62f09 100644 --- a/Resources/Credits/Patrons.yml +++ b/Resources/Credits/Patrons.yml @@ -58,6 +58,8 @@ Tier: Revolutionary - Name: "Mikhail" Tier: Revolutionary +- Name: "Ramiro Agis" + Tier: Revolutionary - Name: "osborn" Tier: Syndicate Agent - Name: "Uinseann" @@ -108,6 +110,8 @@ Tier: Syndicate Agent - Name: "Odin The Wanderer" Tier: Revolutionary +- Name: "tokie" + Tier: Nuclear Operative - Name: "Wallace Megas" Tier: Revolutionary - Name: "Vandell" @@ -130,6 +134,8 @@ Tier: Revolutionary - Name: "eric156" Tier: Revolutionary +- Name: "SHANE ALAN ZINDA" + Tier: Nuclear Operative - Name: "Glenn Olsen" Tier: Syndicate Agent - Name: "Constellations" diff --git a/SpaceStation14.sln b/SpaceStation14.sln index 2dc4c95508d..a94daa316dd 100644 --- a/SpaceStation14.sln +++ b/SpaceStation14.sln @@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Shared.CompNetworkGe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Serialization.Generator", "RobustToolbox\Robust.Serialization.Generator\Robust.Serialization.Generator.csproj", "{6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Content.PatreonParser", "Content.PatreonParser\Content.PatreonParser.csproj", "{D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -430,6 +432,14 @@ Global {6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}.DebugOpt|Any CPU.Build.0 = Debug|Any CPU {6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}.Tools|Any CPU.ActiveCfg = Debug|Any CPU {6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}.Tools|Any CPU.Build.0 = Debug|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.Release|Any CPU.Build.0 = Release|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.DebugOpt|Any CPU.ActiveCfg = Debug|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.DebugOpt|Any CPU.Build.0 = Debug|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.Tools|Any CPU.ActiveCfg = Debug|Any CPU + {D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}.Tools|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE