PageRenderTime 52ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/InternetStandards.oEmbed/oEmbedClient.cs

https://bitbucket.org/jkporter/internetstandards.oembed
C# | 267 lines | 238 code | 29 blank | 0 comment | 21 complexity | 92e75102210149d5e0c2e6061142053a MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net.Http;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using System.Xml;
  10. using System.Xml.Linq;
  11. using System.Xml.Serialization;
  12. using System.Xml.XPath;
  13. using AngleSharp;
  14. using AngleSharp.Dom;
  15. using AngleSharp.Dom.Html;
  16. using AngleSharp.Extensions;
  17. using AngleSharp.Network;
  18. using AngleSharp.Parser.Html;
  19. using InternetStandards.oEmbed.Network;
  20. using InternetStandards.WHATWG.Url;
  21. using Newtonsoft.Json;
  22. using Newtonsoft.Json.Linq;
  23. using HttpMethod = System.Net.Http.HttpMethod;
  24. namespace InternetStandards.oEmbed
  25. {
  26. public class oEmbedClient
  27. {
  28. private readonly HttpClient _httpClient;
  29. public oEmbedClient(HttpClient httpClient)
  30. {
  31. _httpClient = httpClient;
  32. }
  33. public oEmbedClient() : this(new HttpClient())
  34. { }
  35. public Task<T> MakeRequest<T>(string endpointUrl, double? maxWidth = null, double? maxHeight = null,
  36. Dictionary<string, string> additionalArguments = null, CancellationToken cancellationToken = default)
  37. where T : oEmbedResponse
  38. {
  39. return MakeRequest<T>(endpointUrl, null, maxWidth, maxHeight, additionalArguments: additionalArguments,
  40. cancellationToken: cancellationToken);
  41. }
  42. public async Task<T> MakeRequest<T>(string endpointUrl, string url = null,
  43. double? maxWidth = null,
  44. double? maxHeight = null, string format = null, Dictionary<string, string> additionalArguments = null,
  45. CancellationToken cancellationToken = default) where T : oEmbedResponse
  46. {
  47. List<(string name, string value)> arguments;
  48. var queryDelimterIndex = endpointUrl.IndexOf("?", StringComparison.InvariantCulture);
  49. if (queryDelimterIndex != -1)
  50. {
  51. var query = endpointUrl.Substring(queryDelimterIndex).Remove(0, 1);
  52. endpointUrl = endpointUrl.Substring(0, queryDelimterIndex + 1);
  53. arguments = new List<(string name, string value)>(application_x_www_form_urlencoded.Parse(query));
  54. }
  55. else
  56. {
  57. arguments = new List<(string name, string value)>();
  58. }
  59. void AddArgument(string name, string value)
  60. {
  61. if (value == null)
  62. return;
  63. var index = arguments.FindIndex(tuple => tuple.name == name);
  64. if (index == -1)
  65. {
  66. arguments.Add((name, value));
  67. }
  68. else
  69. {
  70. arguments[index] = (name, value);
  71. }
  72. }
  73. AddArgument("url", url);
  74. AddArgument("maxwidth", maxWidth?.ToString(CultureInfo.InvariantCulture));
  75. AddArgument("maxheight", maxHeight?.ToString(CultureInfo.InvariantCulture));
  76. AddArgument("format", format);
  77. if (additionalArguments != null)
  78. arguments.AddRange(additionalArguments.Select(argument => (argument.Key, argument.Value)));
  79. if (arguments.Count > 0)
  80. {
  81. if (!endpointUrl.EndsWith("?")) endpointUrl += "?";
  82. endpointUrl += application_x_www_form_urlencoded.Serializer(arguments);
  83. }
  84. var httpRequest = new HttpRequestMessage(HttpMethod.Get, endpointUrl);
  85. httpRequest.Headers.Accept.ParseAdd("application/json;q=0.003");
  86. httpRequest.Headers.Accept.ParseAdd("text/xml;q=0.002");
  87. httpRequest.Headers.Accept.ParseAdd("application/xml;q=0.001");
  88. using (var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false))
  89. {
  90. using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
  91. {
  92. Type GetoEmbedResponseType(string type)
  93. {
  94. switch (type)
  95. {
  96. case "photo":
  97. return typeof(PhotoTypeoEmbedResponse);
  98. case "video":
  99. return typeof(VideoTypeoEmbedResponse);
  100. case "link":
  101. return typeof(LinkTypeoEmbedResponse);
  102. case "rich":
  103. return typeof(RichTypeoEmbedResponse);
  104. default:
  105. throw new Exception();
  106. }
  107. }
  108. switch (response.Content.Headers.ContentType.MediaType.ToLowerInvariant())
  109. {
  110. case "application/json":
  111. using (var streamReader = new StreamReader(responseStream))
  112. {
  113. using (var reader = new JsonTextReader(streamReader))
  114. {
  115. var serializer = new JsonSerializer();
  116. var oEmbedResponse = serializer.Deserialize<JObject>(reader);
  117. return (T)oEmbedResponse.ToObject(
  118. GetoEmbedResponseType(oEmbedResponse["type"].Value<string>()));
  119. }
  120. }
  121. case "text/xml":
  122. case "application/xml":
  123. var document = XDocument.Load(responseStream);
  124. using (var xmlReader = document.CreateReader())
  125. {
  126. var xmlSerializer =
  127. new XmlSerializer(
  128. GetoEmbedResponseType(document.XPathSelectElement("oembed/type").Value));
  129. return (T)xmlSerializer.Deserialize(xmlReader);
  130. }
  131. }
  132. }
  133. }
  134. throw new Exception();
  135. }
  136. public Task<oEmbedResponse> MakeRequest(string endpointUrl, string url = null,
  137. double? maxWidth = null,
  138. double? maxHeight = null, string format = null, Dictionary<string, string> additionalArguments = null,
  139. CancellationToken cancellationToken = default)
  140. {
  141. return MakeRequest<oEmbedResponse>(endpointUrl, url, maxWidth, maxHeight, format, additionalArguments,
  142. cancellationToken);
  143. }
  144. public static string[] oEmbedLinkTypes = { "application/json+oembed", "text/xml+oembed" };
  145. private IConfiguration AngleSharpConfig()
  146. {
  147. var defaultConfig = AngleSharp.Configuration.Default;
  148. var requesters = defaultConfig.WithDefaultLoader().Services.OfType<IRequester>().ToList();
  149. var requesterIndex = 0;
  150. for (; requesterIndex < requesters.Count; requesterIndex++)
  151. {
  152. var requester = requesters[requesterIndex];
  153. if (requester.SupportsProtocol("http") || requester.SupportsProtocol("https")) break;
  154. }
  155. requesters.Insert(requesterIndex, new HttpClientRequester(_httpClient));
  156. return defaultConfig.WithDefaultLoader(null, requesters);
  157. }
  158. public async Task<IEnumerable<(string Url, string Type)>> Discover(string uri,
  159. CancellationToken cancellationToken = default)
  160. {
  161. var context = BrowsingContext.New(AngleSharpConfig());
  162. var request = DocumentRequest.Get(new Url(uri));
  163. if (context?.Active != null)
  164. {
  165. request.Referer = context.Active.DocumentUri;
  166. }
  167. var loader = context.Loader;
  168. var download = loader.DownloadAsync(request);
  169. cancellationToken.Register(download.Cancel);
  170. using (var response = await download.Task.ConfigureAwait(false))
  171. {
  172. var syntax = response.GetContentType().Equals(new MimeType("application/xhtml+xml"))
  173. ? LanguageSyntax.Xhtml
  174. : LanguageSyntax.Html;
  175. return await Discover(response.Content, context, response.Address.ToString(), syntax, cancellationToken)
  176. .ConfigureAwait(false);
  177. }
  178. }
  179. public Task<IEnumerable<(string Url, string Type)>> Discover(TextReader source, string baseUri = null,
  180. LanguageSyntax syntax = LanguageSyntax.Html, CancellationToken cancellationToken = default)
  181. {
  182. return Discover(baseUri, syntax,
  183. async (p, b, c) => await p.ParseAsync(await source.ReadToEndAsync().ConfigureAwait(false), c)
  184. .ConfigureAwait(false), null, (s, b) => XmlReader.Create(source, s, b), cancellationToken);
  185. }
  186. public Task<IEnumerable<(string Url, string Type)>> Discover(Stream source, string baseUri = null,
  187. LanguageSyntax syntax = LanguageSyntax.Html, CancellationToken cancellationToken = default)
  188. {
  189. return Discover(source, null, baseUri, syntax, cancellationToken);
  190. }
  191. private Task<IEnumerable<(string Url, string Type)>> Discover(Stream source, IBrowsingContext browsingContext, string baseUri = null,
  192. LanguageSyntax syntax = LanguageSyntax.Html, CancellationToken cancellationToken = default)
  193. {
  194. return Discover(baseUri, syntax, async (p, b, c) => await p.ParseAsync(source, c).ConfigureAwait(false),
  195. browsingContext, (s, b) => XmlReader.Create(source, s, b), cancellationToken);
  196. }
  197. private async Task<IEnumerable<(string Url, string Type)>> Discover(string baseUri, LanguageSyntax syntax,
  198. Func<HtmlParser, string, CancellationToken, Task<IDocument>> htmlParseAsync, IBrowsingContext browsingContext,
  199. Func<XmlReaderSettings, string, XmlReader> createReader, CancellationToken cancellationToken = default)
  200. {
  201. switch (syntax)
  202. {
  203. case LanguageSyntax.Html:
  204. var parser = browsingContext == null
  205. ? new HtmlParser(AngleSharpConfig())
  206. : new HtmlParser(new HtmlParserOptions(), browsingContext);
  207. return Discover(await htmlParseAsync(parser, baseUri, cancellationToken).ConfigureAwait(false));
  208. case LanguageSyntax.Xhtml:
  209. using (var xmlReader = createReader(new XmlReaderSettings { DtdProcessing= DtdProcessing.Ignore}, baseUri))
  210. {
  211. return Discover(XDocument.Load(xmlReader, LoadOptions.SetBaseUri), xmlReader.NameTable);
  212. }
  213. default:
  214. throw new ArgumentOutOfRangeException(nameof(syntax), syntax, null);
  215. }
  216. }
  217. private static IEnumerable<(string Url, string Type)> Discover(IDocument document)
  218. {
  219. return document.Head.QuerySelectorAll("link[rel=alternate][type]").Cast<IHtmlLinkElement>()
  220. .Where(
  221. link =>
  222. oEmbedLinkTypes.Any(type =>
  223. string.Compare(type, link.Type,
  224. StringComparison.InvariantCultureIgnoreCase) == 0))
  225. .Select(link => (new Url(link.BaseUrl, link.Href).ToString(), link.Type));
  226. }
  227. private static IEnumerable<(string Url, string Type)> Discover(XDocument document, XmlNameTable nameTable)
  228. {
  229. var namespaceManager = new XmlNamespaceManager(nameTable);
  230. namespaceManager.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");
  231. return document.XPathSelectElements("/xhtml:html/xhtml:head/xhtml:link[@rel='alternate']", namespaceManager)
  232. .Where(link => oEmbedLinkTypes.Any(type =>
  233. string.Compare(type, link.Attribute("type").Value, StringComparison.InvariantCultureIgnoreCase) ==
  234. 0)).Select(link => (new Url(new Url(link.BaseUri), link.Attribute("href").Value).ToString(),
  235. link.Attribute("type").Value));
  236. }
  237. }
  238. }