1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
10 // This file is part of OpenTween.
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
22 // You should have received a copy of the GNU General public License along
23 // with this program. if (not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
28 using System.Collections.Concurrent;
29 using System.Collections.Generic;
31 using System.Net.Http;
33 using System.Text.RegularExpressions;
34 using System.Threading.Tasks;
40 /// 短縮 URL サービスによる URL の展開・短縮を行うクラス
44 private static Lazy<ShortUrl> _instance;
47 /// ShortUrl のインストタンスを取得します
49 public static ShortUrl Instance
51 get { return _instance.Value; }
55 /// 短縮 URL の展開を無効にするか否か
57 public bool DisableExpanding { get; set; }
60 /// 短縮 URL のキャッシュを定期的にクリアする回数
62 public int PurgeCount { get; set; }
64 public string BitlyId { get; set; }
65 public string BitlyKey { get; set; }
67 private HttpClient http;
68 private ConcurrentDictionary<Uri, Uri> urlCache = new ConcurrentDictionary<Uri, Uri>();
70 private static readonly Regex HtmlLinkPattern = new Regex(@"(<a href="")(.+?)("")");
72 private static readonly HashSet<string> ShortUrlHosts = new HashSet<string>()
85 "feeds.feedburner.com",
124 _instance = new Lazy<ShortUrl>(() =>
126 var handler = new HttpClientHandler
128 AllowAutoRedirect = false,
131 var http = MyCommon.CreateHttpClient(handler);
132 http.Timeout = new TimeSpan(0, 0, seconds: 5);
134 return new ShortUrl(http);
138 internal ShortUrl(HttpClient http)
140 this.DisableExpanding = false;
141 this.PurgeCount = 500;
149 public string ExpandUrl(string uri)
153 return this.ExpandUrlAsync(new Uri(uri), 10).Result.ToString();
155 catch (UriFormatException)
162 /// 短縮 URL を非同期に展開します
164 /// <param name="uri">展開するURL</param>
165 /// <returns>URLの展開を行うタスク</returns>
166 public Task<Uri> ExpandUrlAsync(Uri uri)
168 return this.ExpandUrlAsync(uri, 10);
172 /// 短縮 URL を非同期に展開します
174 /// <param name="uri">展開するURL</param>
175 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
176 /// <returns>URLの展開を行うタスク</returns>
177 public async Task<Uri> ExpandUrlAsync(Uri uri, int redirectLimit)
179 if (this.DisableExpanding)
182 if (redirectLimit <= 0)
187 if (!ShortUrlHosts.Contains(uri.Host))
191 if (this.urlCache.TryGetValue(uri, out expanded))
194 if (this.urlCache.Count > this.PurgeCount)
195 this.urlCache.Clear();
200 expanded = await this.GetRedirectTo(uri)
201 .ConfigureAwait(false);
203 catch (TaskCanceledException) { }
204 catch (HttpRequestException) { }
206 if (expanded == null || expanded == uri)
209 this.urlCache[uri] = expanded;
211 var recursiveExpanded = await this.ExpandUrlAsync(expanded, --redirectLimit)
212 .ConfigureAwait(false);
214 // URL1 -> URL2 -> URL3 のように再帰的に展開されたURLを URL1 -> URL3 としてキャッシュに格納する
215 if (recursiveExpanded != expanded)
216 this.urlCache[uri] = recursiveExpanded;
218 return recursiveExpanded;
220 catch (UriFormatException)
227 /// 短縮 URL を非同期に展開します
229 /// <param name="uriStr">展開するURL</param>
230 /// <returns>URLの展開を行うタスク</returns>
231 public async Task<string> ExpandUrlStrAsync(string uriStr)
237 if (!uriStr.StartsWith("http", StringComparison.OrdinalIgnoreCase))
238 uri = new Uri("http://" + uriStr);
240 uri = new Uri(uriStr);
242 catch (UriFormatException)
247 var expandedUri = await this.ExpandUrlAsync(uri, 10)
248 .ConfigureAwait(false);
250 return expandedUri.OriginalString;
254 public string ExpandUrlHtml(string html)
256 return this.ExpandUrlHtmlAsync(html, 10).Result;
260 /// HTML内に含まれるリンクのURLを非同期に展開する
262 /// <param name="html">処理対象のHTML</param>
263 /// <returns>URLの展開を行うタスク</returns>
264 public Task<string> ExpandUrlHtmlAsync(string html)
266 return this.ExpandUrlHtmlAsync(html, 10);
270 /// HTML内に含まれるリンクのURLを非同期に展開する
272 /// <param name="html">処理対象のHTML</param>
273 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
274 /// <returns>URLの展開を行うタスク</returns>
275 public Task<string> ExpandUrlHtmlAsync(string html, int redirectLimit)
277 if (this.DisableExpanding)
278 return Task.FromResult(html);
280 return HtmlLinkPattern.ReplaceAsync(html, async m =>
281 m.Groups[1].Value + await this.ExpandUrlAsync(new Uri(m.Groups[2].Value), redirectLimit).ConfigureAwait(false) + m.Groups[3].Value);
285 /// 指定された短縮URLサービスを使用してURLを短縮します
287 /// <param name="shortenerType">使用する短縮URLサービス</param>
288 /// <param name="srcUri">短縮するURL</param>
289 /// <returns>短縮されたURL</returns>
290 public Task<Uri> ShortenUrlAsync(MyCommon.UrlConverter shortenerType, Uri srcUri)
292 // 既に短縮されている状態のURLであれば短縮しない
293 if (ShortUrlHosts.Contains(srcUri.Host))
294 return Task.FromResult(srcUri);
296 switch (shortenerType)
298 case MyCommon.UrlConverter.TinyUrl:
299 return this.ShortenByTinyUrlAsync(srcUri);
300 case MyCommon.UrlConverter.Isgd:
301 return this.ShortenByIsgdAsync(srcUri);
302 case MyCommon.UrlConverter.Twurl:
303 return this.ShortenByTwurlAsync(srcUri);
304 case MyCommon.UrlConverter.Bitly:
305 return this.ShortenByBitlyAsync(srcUri, "bit.ly");
306 case MyCommon.UrlConverter.Jmp:
307 return this.ShortenByBitlyAsync(srcUri, "j.mp");
308 case MyCommon.UrlConverter.Uxnu:
309 return this.ShortenByUxnuAsync(srcUri);
311 throw new ArgumentException("Unknown shortener.", "shortenerType");
315 private async Task<Uri> ShortenByTinyUrlAsync(Uri srcUri)
317 // 明らかに長くなると推測できる場合は短縮しない
318 if ("http://tinyurl.com/xxxxxx".Length > srcUri.OriginalString.Length)
321 var content = new FormUrlEncodedContent(new[]
323 new KeyValuePair<string, string>("url", srcUri.OriginalString),
326 using (var response = await this.http.PostAsync("http://tinyurl.com/api-create.php", content).ConfigureAwait(false))
328 response.EnsureSuccessStatusCode();
330 var result = await response.Content.ReadAsStringAsync()
331 .ConfigureAwait(false);
333 if (!Regex.IsMatch(result, @"^https?://"))
334 throw new WebApiException("Failed to create URL.", result);
336 return new Uri(result.TrimEnd());
340 private async Task<Uri> ShortenByIsgdAsync(Uri srcUri)
342 // 明らかに長くなると推測できる場合は短縮しない
343 if ("http://is.gd/xxxx".Length > srcUri.OriginalString.Length)
346 var content = new FormUrlEncodedContent(new[]
348 new KeyValuePair<string, string>("format", "simple"),
349 new KeyValuePair<string, string>("url", srcUri.OriginalString),
352 using (var response = await this.http.PostAsync("http://is.gd/create.php", content).ConfigureAwait(false))
354 response.EnsureSuccessStatusCode();
356 var result = await response.Content.ReadAsStringAsync()
357 .ConfigureAwait(false);
359 if (!Regex.IsMatch(result, @"^https?://"))
360 throw new WebApiException("Failed to create URL.", result);
362 return new Uri(result.TrimEnd());
366 private async Task<Uri> ShortenByTwurlAsync(Uri srcUri)
368 // 明らかに長くなると推測できる場合は短縮しない
369 if ("http://twurl.nl/xxxxxx".Length > srcUri.OriginalString.Length)
372 var content = new FormUrlEncodedContent(new[]
374 new KeyValuePair<string, string>("link[url]", srcUri.OriginalString),
377 using (var response = await this.http.PostAsync("http://tweetburner.com/links", content).ConfigureAwait(false))
379 response.EnsureSuccessStatusCode();
381 var result = await response.Content.ReadAsStringAsync()
382 .ConfigureAwait(false);
384 if (!Regex.IsMatch(result, @"^https?://"))
385 throw new WebApiException("Failed to create URL.", result);
387 return new Uri(result.TrimEnd());
391 private async Task<Uri> ShortenByBitlyAsync(Uri srcUri, string domain = "bit.ly")
393 // 明らかに長くなると推測できる場合は短縮しない
394 if ("http://bit.ly/xxxx".Length > srcUri.OriginalString.Length)
397 // bit.ly 短縮機能実装のプライバシー問題の暫定対応
398 // ログインIDとAPIキーが指定されていない場合は短縮せずにPOSTする
399 // 参照: http://sourceforge.jp/projects/opentween/lists/archive/dev/2012-January/000020.html
400 if (string.IsNullOrEmpty(this.BitlyId) || string.IsNullOrEmpty(this.BitlyKey))
403 var query = HttpUtility.ParseQueryString(string.Empty);
404 query["login"] = this.BitlyId;
405 query["apiKey"] = this.BitlyKey;
406 query["format"] = "txt";
407 query["domain"] = domain;
408 query["longUrl"] = srcUri.OriginalString;
410 using (var response = await this.http.GetAsync("https://api-ssl.bitly.com/v3/shorten?" + query).ConfigureAwait(false))
412 response.EnsureSuccessStatusCode();
414 var result = await response.Content.ReadAsStringAsync()
415 .ConfigureAwait(false);
417 if (!Regex.IsMatch(result, @"^https?://"))
418 throw new WebApiException("Failed to create URL.", result);
420 return new Uri(result.TrimEnd());
424 private async Task<Uri> ShortenByUxnuAsync(Uri srcUri)
426 // 明らかに長くなると推測できる場合は短縮しない
427 if ("http://ux.nx/xxxxxx".Length > srcUri.OriginalString.Length)
430 var query = HttpUtility.ParseQueryString(string.Empty);
431 query["format"] = "plain";
432 query["url"] = srcUri.OriginalString;
434 using (var response = await this.http.GetAsync("http://ux.nu/api/short?" + query).ConfigureAwait(false))
436 response.EnsureSuccessStatusCode();
438 var result = await response.Content.ReadAsStringAsync()
439 .ConfigureAwait(false);
441 if (!Regex.IsMatch(result, @"^https?://"))
442 throw new WebApiException("Failed to create URL.", result);
444 return new Uri(result.TrimEnd());
448 private async Task<Uri> GetRedirectTo(Uri url)
450 var request = new HttpRequestMessage(HttpMethod.Head, url);
452 using (var response = await this.http.SendAsync(request).ConfigureAwait(false))
454 if (!response.IsSuccessStatusCode)
456 // ステータスコードが 3xx であれば例外を発生させない
457 if ((int)response.StatusCode / 100 != 3)
458 response.EnsureSuccessStatusCode();
461 return response.Headers.Location;