Repository navigation
Expand file tree
/
Copy pathBridge.cs
More file actions
154 lines (137 loc) · 6.41 KB
/
Copy pathBridge.cs
File metadata and controls
154 lines (137 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace SkinCraft;
public sealed partial class Bridge(Form owner, SkinStore store)
{
static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
static readonly HttpClient Http = CreateHttp();
static HttpClient CreateHttp()
{
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
http.DefaultRequestHeaders.UserAgent.ParseAdd("SkinCraft/1.0");
return http;
}
[GeneratedRegex("^[A-Za-z0-9_]{1,16}$")]
private static partial Regex UsernameRegex();
public async Task<string> HandleAsync(string message)
{
int id = 0;
try
{
using var doc = JsonDocument.Parse(message);
var root = doc.RootElement;
id = root.GetProperty("id").GetInt32();
var method = root.GetProperty("method").GetString();
var p = root.TryGetProperty("params", out var pp) ? pp : default;
object? result = method switch
{
"list" => store.List(),
"save" => store.Save(Str(p, "id"), Str(p, "name"), Str(p, "model"), Str(p, "png")),
"rename" => store.Rename(Str(p, "id"), Str(p, "name")),
"delete" => store.Delete(Str(p, "id")),
"duplicate" => store.Duplicate(Str(p, "id")),
"importFile" => ImportFile(),
"exportFile" => ExportFile(Str(p, "name"), Str(p, "png")),
"fetchPlayer" => await FetchPlayerAsync(Str(p, "name")),
"openFolder" => OpenFolder(),
_ => throw new InvalidOperationException("Unknown method: " + method),
};
return JsonSerializer.Serialize(new { id, ok = true, result }, Json);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { id, ok = false, error = ex.Message }, Json);
}
}
static string? Str(JsonElement p, string name) =>
p.ValueKind == JsonValueKind.Object && p.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
List<object> ImportFile()
{
using var dlg = new OpenFileDialog
{
Title = "Import Minecraft skins",
Filter = "PNG skin (*.png)|*.png",
Multiselect = true,
};
var list = new List<object>();
if (dlg.ShowDialog(owner) != DialogResult.OK) return list;
foreach (var file in dlg.FileNames)
{
var bytes = File.ReadAllBytes(file);
if (bytes.Length > 2_000_000) continue;
list.Add(new { name = Path.GetFileNameWithoutExtension(file), png = Convert.ToBase64String(bytes) });
}
return list;
}
bool ExportFile(string? name, string? png)
{
var bytes = SkinStore.DecodePng(png, true);
var safe = string.Concat((name ?? "skin").Split(Path.GetInvalidFileNameChars())).Trim();
using var dlg = new SaveFileDialog
{
Title = "Export skin",
Filter = "PNG skin (*.png)|*.png",
FileName = (safe.Length == 0 ? "skin" : safe) + ".png",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
};
if (dlg.ShowDialog(owner) != DialogResult.OK) return false;
File.WriteAllBytes(dlg.FileName, bytes);
return true;
}
bool OpenFolder()
{
Process.Start(new ProcessStartInfo("explorer.exe", $"\"{store.SkinsDir}\"") { UseShellExecute = true });
return true;
}
static async Task<object> FetchPlayerAsync(string? name)
{
name = name?.Trim() ?? "";
if (!UsernameRegex().IsMatch(name)) throw new ArgumentException("That is not a valid Minecraft username");
var (uuid, realName) = await LookupUuidAsync(name);
using var profileDoc = JsonDocument.Parse(await Http.GetStringAsync($"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}"));
string? texturesB64 = null;
foreach (var prop in profileDoc.RootElement.GetProperty("properties").EnumerateArray())
if (prop.GetProperty("name").GetString() == "textures")
texturesB64 = prop.GetProperty("value").GetString();
if (texturesB64 == null) throw new InvalidDataException("This player has no skin data");
using var texDoc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(texturesB64)));
if (!texDoc.RootElement.GetProperty("textures").TryGetProperty("SKIN", out var skin))
throw new InvalidDataException("This player uses a default skin");
var url = skin.GetProperty("url").GetString()!.Replace("http://", "https://");
if (!new Uri(url).Host.EndsWith("minecraft.net", StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("Unexpected skin host");
var slim = skin.TryGetProperty("metadata", out var md) && md.TryGetProperty("model", out var m) && m.GetString() == "slim";
var bytes = await Http.GetByteArrayAsync(url);
SkinStore.DecodePng(Convert.ToBase64String(bytes), false);
return new { name = realName, model = slim ? "slim" : "classic", png = Convert.ToBase64String(bytes) };
}
static async Task<(string uuid, string name)> LookupUuidAsync(string name)
{
string[] endpoints =
[
$"https://api.mojang.com/users/profiles/minecraft/{name}",
$"https://api.minecraftservices.com/minecraft/profile/lookup/name/{name}",
];
Exception? last = null;
foreach (var url in endpoints)
{
try
{
using var res = await Http.GetAsync(url);
if (res.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.NoContent)
throw new KeyNotFoundException($"No player named \"{name}\"");
res.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
return (doc.RootElement.GetProperty("id").GetString()!, doc.RootElement.GetProperty("name").GetString()!);
}
catch (KeyNotFoundException) { throw; }
catch (Exception ex) { last = ex; }
}
throw new HttpRequestException("Could not reach Minecraft servers. Check your internet connection.", last);
}
}