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;
35 using System.Threading.Tasks;
41 /// 短縮 URL サービスによる URL の展開・短縮を行うクラス
45 private static Lazy<ShortUrl> _instance;
48 /// ShortUrl のインスタンスを取得します
50 public static ShortUrl Instance
52 get { return _instance.Value; }
56 /// 短縮 URL の展開を無効にするか否か
58 public bool DisableExpanding { get; set; }
61 /// 短縮 URL のキャッシュを定期的にクリアする回数
63 public int PurgeCount { get; set; }
65 public string BitlyId { get; set; }
66 public string BitlyKey { get; set; }
68 private HttpClient http;
69 private ConcurrentDictionary<Uri, Uri> urlCache = new ConcurrentDictionary<Uri, Uri>();
71 private static readonly Regex HtmlLinkPattern = new Regex(@"(<a href="")(.+?)("")");
73 private static readonly HashSet<string> ShortUrlHosts = new HashSet<string>()
87 "feeds.feedburner.com",
126 _instance = new Lazy<ShortUrl>(() => new ShortUrl(), true);
130 : this(CreateDefaultHttpClient())
132 HttpConnection.WebProxyChanged += (o, e) =>
134 var newClient = CreateDefaultHttpClient();
135 var oldClient = Interlocked.Exchange(ref this.http, newClient);
140 internal ShortUrl(HttpClient http)
142 this.DisableExpanding = false;
143 this.PurgeCount = 500;
151 public string ExpandUrl(string uri)
155 return this.ExpandUrlAsync(new Uri(uri), 10).Result.ToString();
157 catch (UriFormatException)
164 /// 短縮 URL を非同期に展開します
166 /// <param name="uri">展開するURL</param>
167 /// <returns>URLの展開を行うタスク</returns>
168 public Task<Uri> ExpandUrlAsync(Uri uri)
170 return this.ExpandUrlAsync(uri, 10);
174 /// 短縮 URL を非同期に展開します
176 /// <param name="uri">展開するURL</param>
177 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
178 /// <returns>URLの展開を行うタスク</returns>
179 public async Task<Uri> ExpandUrlAsync(Uri uri, int redirectLimit)
181 if (this.DisableExpanding)
184 if (redirectLimit <= 0)
187 if (!uri.IsAbsoluteUri)
192 if (!ShortUrlHosts.Contains(uri.Host))
196 if (this.urlCache.TryGetValue(uri, out expanded))
199 if (this.urlCache.Count > this.PurgeCount)
200 this.urlCache.Clear();
205 expanded = await this.GetRedirectTo(uri)
206 .ConfigureAwait(false);
208 catch (TaskCanceledException) { }
209 catch (HttpRequestException) { }
211 if (expanded == null || expanded == uri)
214 this.urlCache[uri] = expanded;
216 var recursiveExpanded = await this.ExpandUrlAsync(expanded, --redirectLimit)
217 .ConfigureAwait(false);
219 // URL1 -> URL2 -> URL3 のように再帰的に展開されたURLを URL1 -> URL3 としてキャッシュに格納する
220 if (recursiveExpanded != expanded)
221 this.urlCache[uri] = recursiveExpanded;
223 return recursiveExpanded;
225 catch (UriFormatException)
232 /// 短縮 URL を非同期に展開します
235 /// 不正なURLが渡された場合は例外を投げず uriStr をそのまま返します
237 /// <param name="uriStr">展開するURL</param>
238 /// <returns>URLの展開を行うタスク</returns>
239 public Task<string> ExpandUrlAsync(string uriStr)
241 return this.ExpandUrlAsync(uriStr, 10);
245 /// 短縮 URL を非同期に展開します
248 /// 不正なURLが渡された場合は例外を投げず uriStr をそのまま返します
250 /// <param name="uriStr">展開するURL</param>
251 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
252 /// <returns>URLの展開を行うタスク</returns>
253 public async Task<string> ExpandUrlAsync(string uriStr, int redirectLimit)
259 if (!uriStr.StartsWith("http", StringComparison.OrdinalIgnoreCase))
260 uri = new Uri("http://" + uriStr);
262 uri = new Uri(uriStr);
264 catch (UriFormatException)
269 var expandedUri = await this.ExpandUrlAsync(uri, redirectLimit)
270 .ConfigureAwait(false);
272 return expandedUri.OriginalString;
276 public string ExpandUrlHtml(string html)
278 return this.ExpandUrlHtmlAsync(html, 10).Result;
282 /// HTML内に含まれるリンクのURLを非同期に展開する
284 /// <param name="html">処理対象のHTML</param>
285 /// <returns>URLの展開を行うタスク</returns>
286 public Task<string> ExpandUrlHtmlAsync(string html)
288 return this.ExpandUrlHtmlAsync(html, 10);
292 /// HTML内に含まれるリンクのURLを非同期に展開する
294 /// <param name="html">処理対象のHTML</param>
295 /// <param name="redirectLimit">再帰的に展開を試みる上限</param>
296 /// <returns>URLの展開を行うタスク</returns>
297 public Task<string> ExpandUrlHtmlAsync(string html, int redirectLimit)
299 if (this.DisableExpanding)
300 return Task.FromResult(html);
302 return HtmlLinkPattern.ReplaceAsync(html, async m =>
303 m.Groups[1].Value + await this.ExpandUrlAsync(m.Groups[2].Value, redirectLimit).ConfigureAwait(false) + m.Groups[3].Value);
307 /// 指定された短縮URLサービスを使用してURLを短縮します
309 /// <param name="shortenerType">使用する短縮URLサービス</param>
310 /// <param name="srcUri">短縮するURL</param>
311 /// <returns>短縮されたURL</returns>
312 public Task<Uri> ShortenUrlAsync(MyCommon.UrlConverter shortenerType, Uri srcUri)
314 // 既に短縮されている状態のURLであれば短縮しない
315 if (ShortUrlHosts.Contains(srcUri.Host))
316 return Task.FromResult(srcUri);
318 switch (shortenerType)
320 case MyCommon.UrlConverter.TinyUrl:
321 return this.ShortenByTinyUrlAsync(srcUri);
322 case MyCommon.UrlConverter.Isgd:
323 return this.ShortenByIsgdAsync(srcUri);
324 case MyCommon.UrlConverter.Twurl:
325 return this.ShortenByTwurlAsync(srcUri);
326 case MyCommon.UrlConverter.Bitly:
327 return this.ShortenByBitlyAsync(srcUri, "bit.ly");
328 case MyCommon.UrlConverter.Jmp:
329 return this.ShortenByBitlyAsync(srcUri, "j.mp");
330 case MyCommon.UrlConverter.Uxnu:
331 return this.ShortenByUxnuAsync(srcUri);
333 throw new ArgumentException("Unknown shortener.", "shortenerType");
337 private async Task<Uri> ShortenByTinyUrlAsync(Uri srcUri)
339 // 明らかに長くなると推測できる場合は短縮しない
340 if ("http://tinyurl.com/xxxxxx".Length > srcUri.OriginalString.Length)
343 var content = new FormUrlEncodedContent(new[]
345 new KeyValuePair<string, string>("url", srcUri.OriginalString),
348 using (var response = await this.http.PostAsync("http://tinyurl.com/api-create.php", content).ConfigureAwait(false))
350 response.EnsureSuccessStatusCode();
352 var result = await response.Content.ReadAsStringAsync()
353 .ConfigureAwait(false);
355 if (!Regex.IsMatch(result, @"^https?://"))
356 throw new WebApiException("Failed to create URL.", result);
358 return new Uri(result.TrimEnd());
362 private async Task<Uri> ShortenByIsgdAsync(Uri srcUri)
364 // 明らかに長くなると推測できる場合は短縮しない
365 if ("http://is.gd/xxxx".Length > srcUri.OriginalString.Length)
368 var content = new FormUrlEncodedContent(new[]
370 new KeyValuePair<string, string>("format", "simple"),
371 new KeyValuePair<string, string>("url", srcUri.OriginalString),
374 using (var response = await this.http.PostAsync("http://is.gd/create.php", content).ConfigureAwait(false))
376 response.EnsureSuccessStatusCode();
378 var result = await response.Content.ReadAsStringAsync()
379 .ConfigureAwait(false);
381 if (!Regex.IsMatch(result, @"^https?://"))
382 throw new WebApiException("Failed to create URL.", result);
384 return new Uri(result.TrimEnd());
388 private async Task<Uri> ShortenByTwurlAsync(Uri srcUri)
390 // 明らかに長くなると推測できる場合は短縮しない
391 if ("http://twurl.nl/xxxxxx".Length > srcUri.OriginalString.Length)
394 var content = new FormUrlEncodedContent(new[]
396 new KeyValuePair<string, string>("link[url]", srcUri.OriginalString),
399 using (var response = await this.http.PostAsync("http://tweetburner.com/links", content).ConfigureAwait(false))
401 response.EnsureSuccessStatusCode();
403 var result = await response.Content.ReadAsStringAsync()
404 .ConfigureAwait(false);
406 if (!Regex.IsMatch(result, @"^https?://"))
407 throw new WebApiException("Failed to create URL.", result);
409 return new Uri(result.TrimEnd());
413 private async Task<Uri> ShortenByBitlyAsync(Uri srcUri, string domain = "bit.ly")
415 // 明らかに長くなると推測できる場合は短縮しない
416 if ("http://bit.ly/xxxx".Length > srcUri.OriginalString.Length)
419 // bit.ly 短縮機能実装のプライバシー問題の暫定対応
420 // ログインIDとAPIキーが指定されていない場合は短縮せずにPOSTする
421 // 参照: http://sourceforge.jp/projects/opentween/lists/archive/dev/2012-January/000020.html
422 if (string.IsNullOrEmpty(this.BitlyId) || string.IsNullOrEmpty(this.BitlyKey))
425 var query = new Dictionary<string, string>
427 {"login", this.BitlyId},
428 {"apiKey", this.BitlyKey},
431 {"longUrl", srcUri.OriginalString},
434 var uri = new Uri("https://api-ssl.bitly.com/v3/shorten?" + MyCommon.BuildQueryString(query));
435 using (var response = await this.http.GetAsync(uri).ConfigureAwait(false))
437 response.EnsureSuccessStatusCode();
439 var result = await response.Content.ReadAsStringAsync()
440 .ConfigureAwait(false);
442 if (!Regex.IsMatch(result, @"^https?://"))
443 throw new WebApiException("Failed to create URL.", result);
445 return new Uri(result.TrimEnd());
449 private async Task<Uri> ShortenByUxnuAsync(Uri srcUri)
451 // 明らかに長くなると推測できる場合は短縮しない
452 if ("http://ux.nx/xxxxxx".Length > srcUri.OriginalString.Length)
455 var query = new Dictionary<string, string>
458 {"url", srcUri.OriginalString},
461 var uri = new Uri("http://ux.nu/api/short?" + MyCommon.BuildQueryString(query));
462 using (var response = await this.http.GetAsync(uri).ConfigureAwait(false))
464 response.EnsureSuccessStatusCode();
466 var result = await response.Content.ReadAsStringAsync()
467 .ConfigureAwait(false);
469 if (!Regex.IsMatch(result, @"^https?://"))
470 throw new WebApiException("Failed to create URL.", result);
472 return new Uri(result.TrimEnd());
476 private async Task<Uri> GetRedirectTo(Uri url)
478 var request = new HttpRequestMessage(HttpMethod.Head, url);
480 using (var response = await this.http.SendAsync(request).ConfigureAwait(false))
482 if (!response.IsSuccessStatusCode)
484 // ステータスコードが 3xx であれば例外を発生させない
485 if ((int)response.StatusCode / 100 != 3)
486 response.EnsureSuccessStatusCode();
489 return response.Headers.Location;
493 private static HttpClient CreateDefaultHttpClient()
495 var handler = new HttpClientHandler
497 AllowAutoRedirect = false,
500 var http = HttpConnection.CreateHttpClient(handler);
501 http.Timeout = TimeSpan.FromSeconds(5);