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>()
86 "feeds.feedburner.com",
125 _instance = new Lazy<ShortUrl>(() =>
127 var handler = new HttpClientHandler
129 AllowAutoRedirect = false,
132 var http = MyCommon.CreateHttpClient(handler);
133 http.Timeout = new TimeSpan(0, 0, seconds: 5);
135 return new ShortUrl(http);
139 internal ShortUrl(HttpClient http)
141 this.DisableExpanding = false;
142 this.PurgeCount = 500;
150 public string ExpandUrl(string uri)
154 return this.ExpandUrlAsync(new Uri(uri), 10).Result.ToString();
156 catch (UriFormatException)
163 /// 短縮 URL を非同期に展開します
165 /// <param name="uri">展開するURL</param>
166 /// <returns>URLの展開を行うタスク</returns>
167 public Task<Uri> ExpandUrlAsync(Uri uri)
169 return this.ExpandUrlAsync(uri, 10);
173 /// 短縮 URL を非同期に展開します
175 /// <param name="uri">展開するURL</param>
176 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
177 /// <returns>URLの展開を行うタスク</returns>
178 public async Task<Uri> ExpandUrlAsync(Uri uri, int redirectLimit)
180 if (this.DisableExpanding)
183 if (redirectLimit <= 0)
186 if (!uri.IsAbsoluteUri)
191 if (!ShortUrlHosts.Contains(uri.Host))
195 if (this.urlCache.TryGetValue(uri, out expanded))
198 if (this.urlCache.Count > this.PurgeCount)
199 this.urlCache.Clear();
204 expanded = await this.GetRedirectTo(uri)
205 .ConfigureAwait(false);
207 catch (TaskCanceledException) { }
208 catch (HttpRequestException) { }
210 if (expanded == null || expanded == uri)
213 this.urlCache[uri] = expanded;
215 var recursiveExpanded = await this.ExpandUrlAsync(expanded, --redirectLimit)
216 .ConfigureAwait(false);
218 // URL1 -> URL2 -> URL3 のように再帰的に展開されたURLを URL1 -> URL3 としてキャッシュに格納する
219 if (recursiveExpanded != expanded)
220 this.urlCache[uri] = recursiveExpanded;
222 return recursiveExpanded;
224 catch (UriFormatException)
231 /// 短縮 URL を非同期に展開します
234 /// 不正なURLが渡された場合は例外を投げず uriStr をそのまま返します
236 /// <param name="uriStr">展開するURL</param>
237 /// <returns>URLの展開を行うタスク</returns>
238 public Task<string> ExpandUrlAsync(string uriStr)
240 return this.ExpandUrlAsync(uriStr, 10);
244 /// 短縮 URL を非同期に展開します
247 /// 不正なURLが渡された場合は例外を投げず uriStr をそのまま返します
249 /// <param name="uriStr">展開するURL</param>
250 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
251 /// <returns>URLの展開を行うタスク</returns>
252 public async Task<string> ExpandUrlAsync(string uriStr, int redirectLimit)
258 if (!uriStr.StartsWith("http", StringComparison.OrdinalIgnoreCase))
259 uri = new Uri("http://" + uriStr);
261 uri = new Uri(uriStr);
263 catch (UriFormatException)
268 var expandedUri = await this.ExpandUrlAsync(uri, redirectLimit)
269 .ConfigureAwait(false);
271 return expandedUri.OriginalString;
275 public string ExpandUrlHtml(string html)
277 return this.ExpandUrlHtmlAsync(html, 10).Result;
281 /// HTML内に含まれるリンクのURLを非同期に展開する
283 /// <param name="html">処理対象のHTML</param>
284 /// <returns>URLの展開を行うタスク</returns>
285 public Task<string> ExpandUrlHtmlAsync(string html)
287 return this.ExpandUrlHtmlAsync(html, 10);
291 /// HTML内に含まれるリンクのURLを非同期に展開する
293 /// <param name="html">処理対象のHTML</param>
294 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
295 /// <returns>URLの展開を行うタスク</returns>
296 public Task<string> ExpandUrlHtmlAsync(string html, int redirectLimit)
298 if (this.DisableExpanding)
299 return Task.FromResult(html);
301 return HtmlLinkPattern.ReplaceAsync(html, async m =>
302 m.Groups[1].Value + await this.ExpandUrlAsync(m.Groups[2].Value, redirectLimit).ConfigureAwait(false) + m.Groups[3].Value);
306 /// 指定された短縮URLサービスを使用してURLを短縮します
308 /// <param name="shortenerType">使用する短縮URLサービス</param>
309 /// <param name="srcUri">短縮するURL</param>
310 /// <returns>短縮されたURL</returns>
311 public Task<Uri> ShortenUrlAsync(MyCommon.UrlConverter shortenerType, Uri srcUri)
313 // 既に短縮されている状態のURLであれば短縮しない
314 if (ShortUrlHosts.Contains(srcUri.Host))
315 return Task.FromResult(srcUri);
317 switch (shortenerType)
319 case MyCommon.UrlConverter.TinyUrl:
320 return this.ShortenByTinyUrlAsync(srcUri);
321 case MyCommon.UrlConverter.Isgd:
322 return this.ShortenByIsgdAsync(srcUri);
323 case MyCommon.UrlConverter.Twurl:
324 return this.ShortenByTwurlAsync(srcUri);
325 case MyCommon.UrlConverter.Bitly:
326 return this.ShortenByBitlyAsync(srcUri, "bit.ly");
327 case MyCommon.UrlConverter.Jmp:
328 return this.ShortenByBitlyAsync(srcUri, "j.mp");
329 case MyCommon.UrlConverter.Uxnu:
330 return this.ShortenByUxnuAsync(srcUri);
332 throw new ArgumentException("Unknown shortener.", "shortenerType");
336 private async Task<Uri> ShortenByTinyUrlAsync(Uri srcUri)
338 // 明らかに長くなると推測できる場合は短縮しない
339 if ("http://tinyurl.com/xxxxxx".Length > srcUri.OriginalString.Length)
342 var content = new FormUrlEncodedContent(new[]
344 new KeyValuePair<string, string>("url", srcUri.OriginalString),
347 using (var response = await this.http.PostAsync("http://tinyurl.com/api-create.php", content).ConfigureAwait(false))
349 response.EnsureSuccessStatusCode();
351 var result = await response.Content.ReadAsStringAsync()
352 .ConfigureAwait(false);
354 if (!Regex.IsMatch(result, @"^https?://"))
355 throw new WebApiException("Failed to create URL.", result);
357 return new Uri(result.TrimEnd());
361 private async Task<Uri> ShortenByIsgdAsync(Uri srcUri)
363 // 明らかに長くなると推測できる場合は短縮しない
364 if ("http://is.gd/xxxx".Length > srcUri.OriginalString.Length)
367 var content = new FormUrlEncodedContent(new[]
369 new KeyValuePair<string, string>("format", "simple"),
370 new KeyValuePair<string, string>("url", srcUri.OriginalString),
373 using (var response = await this.http.PostAsync("http://is.gd/create.php", content).ConfigureAwait(false))
375 response.EnsureSuccessStatusCode();
377 var result = await response.Content.ReadAsStringAsync()
378 .ConfigureAwait(false);
380 if (!Regex.IsMatch(result, @"^https?://"))
381 throw new WebApiException("Failed to create URL.", result);
383 return new Uri(result.TrimEnd());
387 private async Task<Uri> ShortenByTwurlAsync(Uri srcUri)
389 // 明らかに長くなると推測できる場合は短縮しない
390 if ("http://twurl.nl/xxxxxx".Length > srcUri.OriginalString.Length)
393 var content = new FormUrlEncodedContent(new[]
395 new KeyValuePair<string, string>("link[url]", srcUri.OriginalString),
398 using (var response = await this.http.PostAsync("http://tweetburner.com/links", content).ConfigureAwait(false))
400 response.EnsureSuccessStatusCode();
402 var result = await response.Content.ReadAsStringAsync()
403 .ConfigureAwait(false);
405 if (!Regex.IsMatch(result, @"^https?://"))
406 throw new WebApiException("Failed to create URL.", result);
408 return new Uri(result.TrimEnd());
412 private async Task<Uri> ShortenByBitlyAsync(Uri srcUri, string domain = "bit.ly")
414 // 明らかに長くなると推測できる場合は短縮しない
415 if ("http://bit.ly/xxxx".Length > srcUri.OriginalString.Length)
418 // bit.ly 短縮機能実装のプライバシー問題の暫定対応
419 // ログインIDとAPIキーが指定されていない場合は短縮せずにPOSTする
420 // 参照: http://sourceforge.jp/projects/opentween/lists/archive/dev/2012-January/000020.html
421 if (string.IsNullOrEmpty(this.BitlyId) || string.IsNullOrEmpty(this.BitlyKey))
424 var query = new Dictionary<string, string>
426 {"login", this.BitlyId},
427 {"apiKey", this.BitlyKey},
430 {"longUrl", srcUri.OriginalString},
433 var uri = new Uri("https://api-ssl.bitly.com/v3/shorten?" + MyCommon.BuildQueryString(query));
434 using (var response = await this.http.GetAsync(uri).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> ShortenByUxnuAsync(Uri srcUri)
450 // 明らかに長くなると推測できる場合は短縮しない
451 if ("http://ux.nx/xxxxxx".Length > srcUri.OriginalString.Length)
454 var query = new Dictionary<string, string>
457 {"url", srcUri.OriginalString},
460 var uri = new Uri("http://ux.nu/api/short?" + MyCommon.BuildQueryString(query));
461 using (var response = await this.http.GetAsync(uri).ConfigureAwait(false))
463 response.EnsureSuccessStatusCode();
465 var result = await response.Content.ReadAsStringAsync()
466 .ConfigureAwait(false);
468 if (!Regex.IsMatch(result, @"^https?://"))
469 throw new WebApiException("Failed to create URL.", result);
471 return new Uri(result.TrimEnd());
475 private async Task<Uri> GetRedirectTo(Uri url)
477 var request = new HttpRequestMessage(HttpMethod.Head, url);
479 using (var response = await this.http.SendAsync(request).ConfigureAwait(false))
481 if (!response.IsSuccessStatusCode)
483 // ステータスコードが 3xx であれば例外を発生させない
484 if ((int)response.StatusCode / 100 != 3)
485 response.EnsureSuccessStatusCode();
488 return response.Headers.Location;