Add project to update Patrons.yml from a csv file containing Patreon webhooks, add missing Patrons (#20942)
This commit is contained in:
15
Content.PatreonParser/Attributes.cs
Normal file
15
Content.PatreonParser/Attributes.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
14
Content.PatreonParser/Content.PatreonParser.csproj
Normal file
14
Content.PatreonParser/Content.PatreonParser.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
9
Content.PatreonParser/CurrentlyEntitledTiers.cs
Normal file
9
Content.PatreonParser/CurrentlyEntitledTiers.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Content.PatreonParser;
|
||||||
|
|
||||||
|
public sealed class CurrentlyEntitledTiers
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public List<TierData> Data = default!;
|
||||||
|
}
|
||||||
18
Content.PatreonParser/Data.cs
Normal file
18
Content.PatreonParser/Data.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
15
Content.PatreonParser/Included.cs
Normal file
15
Content.PatreonParser/Included.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
3
Content.PatreonParser/Patron.cs
Normal file
3
Content.PatreonParser/Patron.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Content.PatreonParser;
|
||||||
|
|
||||||
|
public readonly record struct Patron(string FullName, string TierName, DateTime Start);
|
||||||
125
Content.PatreonParser/Program.cs
Normal file
125
Content.PatreonParser/Program.cs
Normal file
@@ -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<Guid, Patron>();
|
||||||
|
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<Row>())
|
||||||
|
{
|
||||||
|
if (record.Trigger == "members:create")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var content = JsonSerializer.Deserialize<Root>(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.");
|
||||||
9
Content.PatreonParser/Relationships.cs
Normal file
9
Content.PatreonParser/Relationships.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Content.PatreonParser;
|
||||||
|
|
||||||
|
public sealed class Relationships
|
||||||
|
{
|
||||||
|
[JsonPropertyName("currently_entitled_tiers")]
|
||||||
|
public CurrentlyEntitledTiers CurrentlyEntitledTiers = default!;
|
||||||
|
}
|
||||||
12
Content.PatreonParser/Root.cs
Normal file
12
Content.PatreonParser/Root.cs
Normal file
@@ -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> Included = default!;
|
||||||
|
}
|
||||||
19
Content.PatreonParser/Row.cs
Normal file
19
Content.PatreonParser/Row.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
12
Content.PatreonParser/TierData.cs
Normal file
12
Content.PatreonParser/TierData.cs
Normal file
@@ -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!;
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@
|
|||||||
Tier: Revolutionary
|
Tier: Revolutionary
|
||||||
- Name: "Mikhail"
|
- Name: "Mikhail"
|
||||||
Tier: Revolutionary
|
Tier: Revolutionary
|
||||||
|
- Name: "Ramiro Agis"
|
||||||
|
Tier: Revolutionary
|
||||||
- Name: "osborn"
|
- Name: "osborn"
|
||||||
Tier: Syndicate Agent
|
Tier: Syndicate Agent
|
||||||
- Name: "Uinseann"
|
- Name: "Uinseann"
|
||||||
@@ -108,6 +110,8 @@
|
|||||||
Tier: Syndicate Agent
|
Tier: Syndicate Agent
|
||||||
- Name: "Odin The Wanderer"
|
- Name: "Odin The Wanderer"
|
||||||
Tier: Revolutionary
|
Tier: Revolutionary
|
||||||
|
- Name: "tokie"
|
||||||
|
Tier: Nuclear Operative
|
||||||
- Name: "Wallace Megas"
|
- Name: "Wallace Megas"
|
||||||
Tier: Revolutionary
|
Tier: Revolutionary
|
||||||
- Name: "Vandell"
|
- Name: "Vandell"
|
||||||
@@ -130,6 +134,8 @@
|
|||||||
Tier: Revolutionary
|
Tier: Revolutionary
|
||||||
- Name: "eric156"
|
- Name: "eric156"
|
||||||
Tier: Revolutionary
|
Tier: Revolutionary
|
||||||
|
- Name: "SHANE ALAN ZINDA"
|
||||||
|
Tier: Nuclear Operative
|
||||||
- Name: "Glenn Olsen"
|
- Name: "Glenn Olsen"
|
||||||
Tier: Syndicate Agent
|
Tier: Syndicate Agent
|
||||||
- Name: "Constellations"
|
- Name: "Constellations"
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Shared.CompNetworkGe
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Serialization.Generator", "RobustToolbox\Robust.Serialization.Generator\Robust.Serialization.Generator.csproj", "{6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Serialization.Generator", "RobustToolbox\Robust.Serialization.Generator\Robust.Serialization.Generator.csproj", "{6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Content.PatreonParser", "Content.PatreonParser\Content.PatreonParser.csproj", "{D97D8258-D915-4D1D-B1E3-1A8D00CF9EB5}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Debug|Any CPU
|
||||||
{6FBF108E-5CB5-47DE-8D7E-B496ABA9E3E2}.Tools|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user