diff options
Diffstat (limited to 'src/SMAPI.Web/Framework/Clients/Pastebin')
4 files changed, 181 insertions, 0 deletions
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs new file mode 100644 index 00000000..630dfb76 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin +{ + /// <summary>An API client for Pastebin.</summary> + internal interface IPastebinClient : IDisposable + { + /// <summary>Fetch a saved paste.</summary> + /// <param name="id">The paste ID.</param> + Task<PasteInfo> GetAsync(string id); + + /// <summary>Save a paste to Pastebin.</summary> + /// <param name="content">The paste content.</param> + Task<SavePasteResult> PostAsync(string content); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs new file mode 100644 index 00000000..955156eb --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin +{ + /// <summary>The response for a get-paste request.</summary> + internal class PasteInfo + { + /// <summary>Whether the log was successfully fetched.</summary> + public bool Success { get; set; } + + /// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary> + public string Content { get; set; } + + /// <summary>The error message (if saving failed).</summary> + public string Error { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs new file mode 100644 index 00000000..ef83a91e --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin +{ + /// <summary>An API client for Pastebin.</summary> + internal class PastebinClient : IPastebinClient + { + /********* + ** Properties + *********/ + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; + + /// <summary>The user key used to authenticate with the Pastebin API.</summary> + private readonly string UserKey; + + /// <summary>The developer key used to authenticate with the Pastebin API.</summary> + private readonly string DevKey; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="baseUrl">The base URL for the Pastebin API.</param> + /// <param name="userAgent">The user agent for the API client.</param> + /// <param name="userKey">The user key used to authenticate with the Pastebin API.</param> + /// <param name="devKey">The developer key used to authenticate with the Pastebin API.</param> + public PastebinClient(string baseUrl, string userAgent, string userKey, string devKey) + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.UserKey = userKey; + this.DevKey = devKey; + } + + /// <summary>Fetch a saved paste.</summary> + /// <param name="id">The paste ID.</param> + public async Task<PasteInfo> GetAsync(string id) + { + try + { + // get from API + string content = await this.Client + .GetAsync($"raw/{id}") + .AsString(); + + // handle Pastebin errors + if (string.IsNullOrWhiteSpace(content)) + return new PasteInfo { Error = "Received an empty response from Pastebin." }; + if (content.StartsWith("<!DOCTYPE")) + return new PasteInfo { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." }; + return new PasteInfo { Success = true, Content = content }; + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new PasteInfo { Error = "There's no log with that ID." }; + } + catch (Exception ex) + { + return new PasteInfo { Error = ex.ToString() }; + } + } + + /// <summary>Save a paste to Pastebin.</summary> + /// <param name="content">The paste content.</param> + public async Task<SavePasteResult> PostAsync(string content) + { + try + { + // validate + if (string.IsNullOrWhiteSpace(content)) + return new SavePasteResult { Error = "The log content can't be empty." }; + + // post to API + string response = await this.Client + .PostAsync("api/api_post.php") + .WithBodyContent(this.GetFormUrlEncodedContent(new Dictionary<string, string> + { + ["api_option"] = "paste", + ["api_user_key"] = this.UserKey, + ["api_dev_key"] = this.DevKey, + ["api_paste_private"] = "1", // unlisted + ["api_paste_name"] = $"SMAPI log {DateTime.UtcNow:s}", + ["api_paste_expire_date"] = "N", // never expire + ["api_paste_code"] = content + })) + .AsString(); + + // handle Pastebin errors + if (string.IsNullOrWhiteSpace(response)) + return new SavePasteResult { Error = "Received an empty response from Pastebin." }; + if (response.StartsWith("Bad API request")) + return new SavePasteResult { Error = response }; + if (!response.Contains("/")) + return new SavePasteResult { Error = $"Received an unknown response: {response}" }; + + // return paste ID + string pastebinID = response.Split("/").Last(); + return new SavePasteResult { Success = true, ID = pastebinID }; + } + catch (Exception ex) + { + return new SavePasteResult { Success = false, Error = ex.ToString() }; + } + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Build an HTTP content body with form-url-encoded content.</summary> + /// <param name="data">The content to encode.</param> + /// <remarks>This bypasses an issue where <see cref="FormUrlEncodedContent"/> restricts the body length to the maximum size of a URL, which isn't applicable here.</remarks> + private HttpContent GetFormUrlEncodedContent(IDictionary<string, string> data) + { + string body = string.Join("&", from arg in data select $"{HttpUtility.UrlEncode(arg.Key)}={HttpUtility.UrlEncode(arg.Value)}"); + return new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/SavePasteResult.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/SavePasteResult.cs new file mode 100644 index 00000000..89dab697 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/SavePasteResult.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin +{ + /// <summary>The response for a save-log request.</summary> + internal class SavePasteResult + { + /// <summary>Whether the log was successfully saved.</summary> + public bool Success { get; set; } + + /// <summary>The saved paste ID (if <see cref="Success"/> is <c>true</c>).</summary> + public string ID { get; set; } + + /// <summary>The error message (if saving failed).</summary> + public string Error { get; set; } + } +} |