OSDN Git Service

TwitterApiConnectionにUserStreams用のHttpClientを追加
[opentween/open-tween.git] / OpenTween / Connection / TwitterApiConnection.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2016 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.IO;
25 using System.Linq;
26 using System.Net;
27 using System.Net.Cache;
28 using System.Net.Http;
29 using System.Runtime.Serialization;
30 using System.Text;
31 using System.Threading.Tasks;
32 using System.Web;
33 using OpenTween.Api;
34 using OpenTween.Api.DataModel;
35
36 namespace OpenTween.Connection
37 {
38     public class TwitterApiConnection : IApiConnection, IDisposable
39     {
40         public static Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/");
41
42         // SettingCommon.xml の TwitterUrl との互換性のために用意
43         public static string RestApiHost
44         {
45             get { return RestApiBase.Host; }
46             set { RestApiBase = new Uri($"https://{value}/1.1/"); }
47         }
48
49         public bool IsDisposed { get; private set; } = false;
50
51         public string AccessToken { get; }
52         public string AccessSecret { get; }
53
54         internal HttpClient http;
55         internal HttpClient httpStreaming;
56
57         public TwitterApiConnection(string accessToken, string accessSecret)
58         {
59             this.AccessToken = accessToken;
60             this.AccessSecret = accessSecret;
61
62             this.InitializeHttpClients();
63             Networking.WebProxyChanged += this.Networking_WebProxyChanged;
64         }
65
66         private void InitializeHttpClients()
67         {
68             this.http = InitializeHttpClient(this.AccessToken, this.AccessSecret, streaming: false);
69             this.httpStreaming = InitializeHttpClient(this.AccessToken, this.AccessSecret, streaming: true);
70         }
71
72         public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> param, string endpointName)
73         {
74             var requestUri = new Uri(RestApiBase, uri);
75
76             if (param != null)
77                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
78
79             var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
80
81             try
82             {
83                 using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
84                     .ConfigureAwait(false))
85                 {
86                     await this.CheckStatusCode(response)
87                         .ConfigureAwait(false);
88
89                     if (endpointName != null)
90                         MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
91
92                     using (var content = response.Content)
93                     {
94                         var responseText = await content.ReadAsStringAsync()
95                             .ConfigureAwait(false);
96
97                         try
98                         {
99                             return MyCommon.CreateDataFromJson<T>(responseText);
100                         }
101                         catch (SerializationException ex)
102                         {
103                             throw TwitterApiException.CreateFromException(ex, responseText);
104                         }
105                     }
106                 }
107             }
108             catch (HttpRequestException ex)
109             {
110                 throw TwitterApiException.CreateFromException(ex);
111             }
112             catch (OperationCanceledException ex)
113             {
114                 throw TwitterApiException.CreateFromException(ex);
115             }
116         }
117
118         public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string> param)
119         {
120             var requestUri = new Uri(RestApiBase, uri);
121
122             if (param != null)
123                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
124
125             try
126             {
127                 return await this.http.GetStreamAsync(requestUri)
128                     .ConfigureAwait(false);
129             }
130             catch (HttpRequestException ex)
131             {
132                 throw TwitterApiException.CreateFromException(ex);
133             }
134             catch (OperationCanceledException ex)
135             {
136                 throw TwitterApiException.CreateFromException(ex);
137             }
138         }
139
140         public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param)
141         {
142             var requestUri = new Uri(RestApiBase, uri);
143             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
144
145             using (var postContent = new FormUrlEncodedContent(param))
146             {
147                 request.Content = postContent;
148
149                 HttpResponseMessage response = null;
150                 try
151                 {
152                     response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
153                         .ConfigureAwait(false);
154
155                     await this.CheckStatusCode(response)
156                         .ConfigureAwait(false);
157
158                     var result = new LazyJson<T>(response);
159                     response = null;
160
161                     return result;
162                 }
163                 catch (HttpRequestException ex)
164                 {
165                     throw TwitterApiException.CreateFromException(ex);
166                 }
167                 catch (OperationCanceledException ex)
168                 {
169                     throw TwitterApiException.CreateFromException(ex);
170                 }
171                 finally
172                 {
173                     response?.Dispose();
174                 }
175             }
176         }
177
178         public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param, IDictionary<string, IMediaItem> media)
179         {
180             var requestUri = new Uri(RestApiBase, uri);
181             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
182
183             using (var postContent = new MultipartFormDataContent())
184             {
185                 if (param != null)
186                 {
187                     foreach (var kv in param)
188                         postContent.Add(new StringContent(kv.Value), kv.Key);
189                 }
190                 if (media != null)
191                 {
192                     foreach (var kv in media)
193                         postContent.Add(new StreamContent(kv.Value.OpenRead()), kv.Key, kv.Value.Name);
194                 }
195
196                 request.Content = postContent;
197
198                 HttpResponseMessage response = null;
199                 try
200                 {
201                     response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
202                         .ConfigureAwait(false);
203
204                     await this.CheckStatusCode(response)
205                         .ConfigureAwait(false);
206
207                     var result = new LazyJson<T>(response);
208                     response = null;
209
210                     return result;
211                 }
212                 catch (HttpRequestException ex)
213                 {
214                     throw TwitterApiException.CreateFromException(ex);
215                 }
216                 catch (OperationCanceledException ex)
217                 {
218                     throw TwitterApiException.CreateFromException(ex);
219                 }
220                 finally
221                 {
222                     response?.Dispose();
223                 }
224             }
225         }
226
227         public async Task PostJsonAsync(Uri uri, string json)
228         {
229             var requestUri = new Uri(RestApiBase, uri);
230             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
231
232             using (var postContent = new StringContent(json, Encoding.UTF8, "application/json"))
233             {
234                 request.Content = postContent;
235
236                 try
237                 {
238                     using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
239                         .ConfigureAwait(false))
240                     {
241                         await this.CheckStatusCode(response)
242                             .ConfigureAwait(false);
243                     }
244                 }
245                 catch (HttpRequestException ex)
246                 {
247                     throw TwitterApiException.CreateFromException(ex);
248                 }
249                 catch (OperationCanceledException ex)
250                 {
251                     throw TwitterApiException.CreateFromException(ex);
252                 }
253             }
254         }
255
256         protected async Task CheckStatusCode(HttpResponseMessage response)
257         {
258             var statusCode = response.StatusCode;
259             if (statusCode == HttpStatusCode.OK)
260             {
261                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
262                 return;
263             }
264
265             string responseText;
266             using (var content = response.Content)
267             {
268                 responseText = await content.ReadAsStringAsync()
269                     .ConfigureAwait(false);
270             }
271
272             if (string.IsNullOrWhiteSpace(responseText))
273             {
274                 if (statusCode == HttpStatusCode.Unauthorized)
275                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
276
277                 throw new TwitterApiException(statusCode, responseText);
278             }
279
280             try
281             {
282                 var error = TwitterError.ParseJson(responseText);
283
284                 if (error?.Errors == null || error.Errors.Length == 0)
285                     throw new TwitterApiException(statusCode, responseText);
286
287                 var errorCodes = error.Errors.Select(x => x.Code);
288                 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
289                 {
290                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
291                 }
292
293                 throw new TwitterApiException(error, responseText);
294             }
295             catch (SerializationException)
296             {
297                 throw new TwitterApiException(statusCode, responseText);
298             }
299         }
300
301         public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
302         {
303             var uri = new Uri(RestApiBase, authServiceProvider);
304
305             return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri,
306                 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
307                 this.AccessToken, this.AccessSecret, realm);
308         }
309
310         public void Dispose()
311         {
312             this.Dispose(true);
313             GC.SuppressFinalize(this);
314         }
315
316         protected virtual void Dispose(bool disposing)
317         {
318             if (this.IsDisposed)
319                 return;
320
321             this.IsDisposed = true;
322
323             if (disposing)
324             {
325                 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
326                 this.http.Dispose();
327                 this.httpStreaming.Dispose();
328             }
329         }
330
331         ~TwitterApiConnection()
332         {
333             this.Dispose(false);
334         }
335
336         private void Networking_WebProxyChanged(object sender, EventArgs e)
337         {
338             this.InitializeHttpClients();
339         }
340
341         public static async Task<Tuple<string, string>> GetRequestTokenAsync()
342         {
343             var param = new Dictionary<string, string>
344             {
345                 ["oauth_callback"] = "oob",
346             };
347             var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, oauthToken: null)
348                 .ConfigureAwait(false);
349
350             return Tuple.Create(response["oauth_token"], response["oauth_token_secret"]);
351         }
352
353         public static Uri GetAuthorizeUri(Tuple<string, string> requestToken, string screenName = null)
354         {
355             var param = new Dictionary<string, string>
356             {
357                 ["oauth_token"] = requestToken.Item1,
358             };
359
360             if (screenName != null)
361                 param["screen_name"] = screenName;
362
363             return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
364         }
365
366         public static async Task<IDictionary<string, string>> GetAccessTokenAsync(Tuple<string, string> requestToken, string verifier)
367         {
368             var param = new Dictionary<string, string>
369             {
370                 ["oauth_verifier"] = verifier,
371             };
372             var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, requestToken)
373                 .ConfigureAwait(false);
374
375             return response;
376         }
377
378         private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(Uri uri, IDictionary<string, string> param,
379             Tuple<string, string> oauthToken)
380         {
381             HttpClient authorizeClient;
382             if (oauthToken != null)
383                 authorizeClient = InitializeHttpClient(oauthToken.Item1, oauthToken.Item2);
384             else
385                 authorizeClient = InitializeHttpClient("", "");
386
387             var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
388
389             try
390             {
391                 using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))
392                 using (var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
393                     .ConfigureAwait(false))
394                 using (var content = response.Content)
395                 {
396                     var responseText = await content.ReadAsStringAsync()
397                         .ConfigureAwait(false);
398
399                     if (!response.IsSuccessStatusCode)
400                         throw new TwitterApiException(response.StatusCode, responseText);
401
402                     var responseParams = HttpUtility.ParseQueryString(responseText);
403
404                     return responseParams.Cast<string>()
405                         .ToDictionary(x => x, x => responseParams[x]);
406                 }
407             }
408             catch (HttpRequestException ex)
409             {
410                 throw TwitterApiException.CreateFromException(ex);
411             }
412             catch (OperationCanceledException ex)
413             {
414                 throw TwitterApiException.CreateFromException(ex);
415             }
416         }
417
418         private static HttpClient InitializeHttpClient(string accessToken, string accessSecret, bool streaming = false)
419         {
420             var innerHandler = Networking.CreateHttpClientHandler(streaming);
421             innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
422
423             var handler = new OAuthHandler(innerHandler,
424                 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
425                 accessToken, accessSecret);
426
427             return Networking.CreateHttpClient(handler, streaming);
428         }
429     }
430 }