diff --git a/PKHeX.Core/Editing/Showdown/ShowdownTeam.cs b/PKHeX.Core/Editing/Showdown/ShowdownTeam.cs new file mode 100644 index 000000000..c27e1387a --- /dev/null +++ b/PKHeX.Core/Editing/Showdown/ShowdownTeam.cs @@ -0,0 +1,165 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace PKHeX.Core; + +/// +/// Logic for retrieving Showdown teams from URLs. +/// +public static class ShowdownTeam +{ + /// + /// Generates the API URL for retrieving a Showdown team based on the supplied team identifier. + /// + /// The numeric identifier of the team. + /// A string containing the full URL to access the team data via the API. + public static string GetTeamURL(int team) => $"https://play.pokemonshowdown.com/api/getteam?teamid={team}&raw=1"; + + /// + /// Attempts to retrieve the Showdown team data from a specified URL, and reformats it. + /// + /// The URL to retrieve the team data from. + /// When the method returns, contains the processed team data if retrieval and formatting succeed; otherwise, null. + /// The numeric identifier extracted from the URL or response. + /// true if the team data is successfully retrieved and reformatted; otherwise, false. + public static bool TryGetTeams(string url, [NotNullWhen(true)] out string? content, out int team) + { + team = 0; + content = null; + if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult) || (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps)) + return false; + + content = NetUtil.GetStringFromURL(uriResult); + if (content == null) + return false; + + return GetTeamsFromReply(ref content); + } + + /// + /// Extracts the team data from the API reply and reformats it by replacing escaped newline + /// characters with system-specific line breaks. + /// + /// + /// A reference to the API response string. On successful extraction, the value is replaced + /// with the reformatted team data; otherwise, it remains unchanged. + /// + /// + /// true if the team data is successfully extracted and reformatted; otherwise, false. + /// + public static bool GetTeamsFromReply(ref string content) + { + // reformat + const string startText = """ + "team":" + """; + var start = content.IndexOf(startText, StringComparison.Ordinal); + if (start == -1) + return false; + start += startText.Length; // skip to the start of the team + + var end = content.LastIndexOf("\\n", StringComparison.Ordinal); + if (end == -1 || end <= start) + return false; + + content = content[start..end].Replace("\\n", Environment.NewLine); + return true; + } + + /// + /// Determines if the provided text is a valid Showdown team URL. If valid, returns a normalized API URL. + /// + /// The text to evaluate. + /// When the method returns, contains the normalized API URL if the text represents a valid Showdown team URL; otherwise, null. + /// true if the text is a valid Showdown team URL; otherwise, false. + public static bool IsShowdownTeamURL(ReadOnlySpan text, [NotNullWhen(true)] out string? url) + { + text = text.Trim(); + if (text.StartsWith("https://psim.us/t/") || // short link + text.StartsWith("https://teams.pokemonshowdown.com/")) + return TryGetTeamFromWebURL(text, out url); + + if (text.StartsWith("https://play.pokemonshowdown.com/api/getteam?teamid=")) + return TryGetTeamFromAPITeamURL(text, out url); + + url = null; + return false; + } + + /// + /// Attempts to extract the team identifier from a Showdown web URL and converts it to a standard API URL. + /// + /// The Showdown web URL as a read-only span of characters. + /// When the method returns, contains the standardized API URL if extraction is successful; otherwise, null. + /// true if the team index is successfully extracted and converted; otherwise, false. + public static bool TryGetTeamFromWebURL(ReadOnlySpan text, [NotNullWhen(true)] out string? url) + { + url = null; + if (!TryGetTeamIndexWeb(text, out var team)) + return false; + url = GetTeamURL(team); + return true; + } + + /// + /// Attempts to extract the team identifier from a Showdown API URL and returns a standardized API URL. + /// + /// The Showdown API URL as a read-only span of characters. + /// When the method returns, contains the standardized API URL if extraction is successful; otherwise, null. + /// true if the team index is successfully extracted and the URL normalized; otherwise, false. + public static bool TryGetTeamFromAPITeamURL(ReadOnlySpan text, [NotNullWhen(true)] out string? url) + { + url = null; + if (!TryGetTeamIndexAPI(text, out var team)) + return false; + url = GetTeamURL(team); + return true; + } + + /// + /// Extracts the team identifier from a Showdown web URL. + /// + /// The Showdown web URL provided as a read-only span of characters. + /// When the method returns, contains the extracted team identifier if successful; otherwise, zero. + /// true if the team identifier is successfully extracted; otherwise, false. + public static bool TryGetTeamIndexWeb(ReadOnlySpan text, out int team) + { + team = 0; + if (text.EndsWith('/')) + text = text[..^1]; // remove trailing slash + if (text.EndsWith("/raw")) + text = text[..^4]; // remove trailing /raw + + int start = text.LastIndexOf('/'); // seek back to = + if (start == -1) + return false; + + var number = text[(start + 1)..]; + if (!int.TryParse(number, out team)) + return false; + return true; + } + + /// + /// Extracts the team identifier from a Showdown API URL. + /// + /// The Showdown API URL as a read-only span of characters. + /// When the method returns, contains the extracted team identifier if successful; otherwise, zero. + /// true if the team identifier is successfully extracted; otherwise, false. + public static bool TryGetTeamIndexAPI(ReadOnlySpan text, out int team) + { + team = 0; + if (!text.EndsWith("&raw=1")) + return false; + + text = text[..^6]; + int start = text.LastIndexOf('='); // seek back to = + if (start == -1) + return false; + + var number = text[(start + 1)..]; + if (!int.TryParse(number, out team)) + return false; + return true; + } +} diff --git a/PKHeX.Core/Util/NetUtil.cs b/PKHeX.Core/Util/NetUtil.cs index 894d32e2d..2b2324e03 100644 --- a/PKHeX.Core/Util/NetUtil.cs +++ b/PKHeX.Core/Util/NetUtil.cs @@ -30,6 +30,7 @@ public static class NetUtil { // The GitHub API will fail if no user agent is provided using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(3); const string agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; client.DefaultRequestHeaders.Add("User-Agent", agent); var response = client.GetAsync(url).Result; diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index 173fc70fd..55f5bc413 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -524,7 +524,11 @@ private void ClickShowdownImportPKM(object sender, EventArgs e) // Get Simulator Data var text = Clipboard.GetText(); - var set = new ShowdownSet(text); + ShowdownSet set; + if (ShowdownTeam.IsShowdownTeamURL(text, out var url) && ShowdownTeam.TryGetTeams(url, out var content, out _)) + set = ShowdownParsing.GetShowdownSets(content).FirstOrDefault() ?? new(""); // take only first set + else + set = new ShowdownSet(text); if (set.Species == 0) { WinFormsUtil.Alert(MsgSimulatorFailClipboard); return; }