OSDN Git Service

4bbe61fa96afc7d103933ef156d5244873a1639f
[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;
32 using System.Threading.Tasks;
33 using System.Web;
34 using OpenTween.Api;
35 using OpenTween.Api.DataModel;
36
37 namespace OpenTween.Connection
38 {
39     public class TwitterApiConnection : IApiConnection, IDisposable
40     {
41         public static Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/");
42
43         // SettingCommon.xml の TwitterUrl との互換性のために用意
44         public static string RestApiHost
45         {
46             get { return RestApiBase.Host; }
47             set { RestApiBase = new Uri($"https://{value}/1.1/"); }
48         }
49
50         public bool IsDisposed { get; private set; } = false;
51
52         public string AccessToken { get; }
53         public string AccessSecret { get; }
54
55         internal HttpClient http;
56         internal HttpClient httpStreaming;
57
58         public TwitterApiConnection(string accessToken, string accessSecret)
59         {
60             this.AccessToken = accessToken;
61             this.AccessSecret = accessSecret;
62
63             this.InitializeHttpClients();
64             Networking.WebProxyChanged += this.Networking_WebProxyChanged;
65         }
66
67         private void InitializeHttpClients()
68         {
69             this.http = InitializeHttpClient(this.AccessToken, this.AccessSecret);
70
71             this.httpStreaming = InitializeHttpClient(this.AccessToken, this.AccessSecret, disableGzip: true);
72             this.httpStreaming.Timeout = Timeout.InfiniteTimeSpan;
73         }
74
75         public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> param, string endpointName)
76         {
77             var requestUri = new Uri(RestApiBase, uri);
78
79             if (param != null)
80                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
81
82             var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
83
84             try
85             {
86                 using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
87                     .ConfigureAwait(false))
88                 {
89                     await this.CheckStatusCode(response)
90                         .ConfigureAwait(false);
91
92                     if (endpointName != null)
93                         MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
94
95                     using (var content = response.Content)
96                     {
97                         var responseText = await content.ReadAsStringAsync()
98                             .ConfigureAwait(false);
99
100                         try
101                         {
102                             return MyCommon.CreateDataFromJson<T>(responseText);
103                         }
104                         catch (SerializationException ex)
105                         {
106                             throw TwitterApiException.CreateFromException(ex, responseText);
107                         }
108                     }
109                 }
110             }
111             catch (HttpRequestException ex)
112             {
113                 throw TwitterApiException.CreateFromException(ex);
114             }
115             catch (OperationCanceledException ex)
116             {
117                 throw TwitterApiException.CreateFromException(ex);
118             }
119         }
120
121         public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string> param)
122         {
123             var requestUri = new Uri(RestApiBase, uri);
124
125             if (param != null)
126                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
127
128             try
129             {
130                 return await this.http.GetStreamAsync(requestUri)
131                     .ConfigureAwait(false);
132             }
133             catch (HttpRequestException ex)
134             {
135                 throw TwitterApiException.CreateFromException(ex);
136             }
137             catch (OperationCanceledException ex)
138             {
139                 throw TwitterApiException.CreateFromException(ex);
140             }
141         }
142
143         public async Task<Stream> GetStreamingStreamAsync(Uri uri, IDictionary<string, string> param)
144         {
145             var requestUri = new Uri(RestApiBase, uri);
146
147             if (param != null)
148                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
149
150             try
151             {
152                 return await this.httpStreaming.GetStreamAsync(requestUri)
153                     .ConfigureAwait(false);
154             }
155             catch (HttpRequestException ex)
156             {
157                 throw TwitterApiException.CreateFromException(ex);
158             }
159             catch (OperationCanceledException ex)
160             {
161                 throw TwitterApiException.CreateFromException(ex);
162             }
163         }
164
165         public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param)
166         {
167             var requestUri = new Uri(RestApiBase, uri);
168             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
169
170             using (var postContent = new FormUrlEncodedContent(param))
171             {
172                 request.Content = postContent;
173
174                 HttpResponseMessage response = null;
175                 try
176                 {
177                     response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
178                         .ConfigureAwait(false);
179
180                     await this.CheckStatusCode(response)
181                         .ConfigureAwait(false);
182
183                     var result = new LazyJson<T>(response);
184                     response = null;
185
186                     return result;
187                 }
188                 catch (HttpRequestException ex)
189                 {
190                     throw TwitterApiException.CreateFromException(ex);
191                 }
192                 catch (OperationCanceledException ex)
193                 {
194                     throw TwitterApiException.CreateFromException(ex);
195                 }
196                 finally
197                 {
198                     response?.Dispose();
199                 }
200             }
201         }
202
203         public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param, IDictionary<string, IMediaItem> media)
204         {
205             var requestUri = new Uri(RestApiBase, uri);
206             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
207
208             using (var postContent = new MultipartFormDataContent())
209             {
210                 if (param != null)
211                 {
212                     foreach (var kv in param)
213                         postContent.Add(new StringContent(kv.Value), kv.Key);
214                 }
215                 if (media != null)
216                 {
217                     foreach (var kv in media)
218                         postContent.Add(new StreamContent(kv.Value.OpenRead()), kv.Key, kv.Value.Name);
219                 }
220
221                 request.Content = postContent;
222
223                 HttpResponseMessage response = null;
224                 try
225                 {
226                     response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
227                         .ConfigureAwait(false);
228
229                     await this.CheckStatusCode(response)
230                         .ConfigureAwait(false);
231
232                     var result = new LazyJson<T>(response);
233                     response = null;
234
235                     return result;
236                 }
237                 catch (HttpRequestException ex)
238                 {
239                     throw TwitterApiException.CreateFromException(ex);
240                 }
241                 catch (OperationCanceledException ex)
242                 {
243                     throw TwitterApiException.CreateFromException(ex);
244                 }
245                 finally
246                 {
247                     response?.Dispose();
248                 }
249             }
250         }
251
252         public async Task PostJsonAsync(Uri uri, string json)
253         {
254             var requestUri = new Uri(RestApiBase, uri);
255             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
256
257             using (var postContent = new StringContent(json, Encoding.UTF8, "application/json"))
258             {
259                 request.Content = postContent;
260
261                 try
262                 {
263                     using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
264                         .ConfigureAwait(false))
265                     {
266                         await this.CheckStatusCode(response)
267                             .ConfigureAwait(false);
268                     }
269                 }
270                 catch (HttpRequestException ex)
271                 {
272                     throw TwitterApiException.CreateFromException(ex);
273                 }
274                 catch (OperationCanceledException ex)
275                 {
276                     throw TwitterApiException.CreateFromException(ex);
277                 }
278             }
279         }
280
281         protected async Task CheckStatusCode(HttpResponseMessage response)
282         {
283             var statusCode = response.StatusCode;
284             if (statusCode == HttpStatusCode.OK)
285             {
286                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
287                 return;
288             }
289
290             string responseText;
291             using (var content = response.Content)
292             {
293                 responseText = await content.ReadAsStringAsync()
294                     .ConfigureAwait(false);
295             }
296
297             if (string.IsNullOrWhiteSpace(responseText))
298             {
299                 if (statusCode == HttpStatusCode.Unauthorized)
300                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
301
302                 throw new TwitterApiException(statusCode, responseText);
303             }
304
305             try
306             {
307                 var error = TwitterError.ParseJson(responseText);
308
309                 if (error?.Errors == null || error.Errors.Length == 0)
310                     throw new TwitterApiException(statusCode, responseText);
311
312                 var errorCodes = error.Errors.Select(x => x.Code);
313                 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
314                 {
315                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
316                 }
317
318                 throw new TwitterApiException(error, responseText);
319             }
320             catch (SerializationException)
321             {
322                 throw new TwitterApiException(statusCode, responseText);
323             }
324         }
325
326         public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
327         {
328             var uri = new Uri(RestApiBase, authServiceProvider);
329
330             return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri,
331                 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
332                 this.AccessToken, this.AccessSecret, realm);
333         }
334
335         public void Dispose()
336         {
337             this.Dispose(true);
338             GC.SuppressFinalize(this);
339         }
340
341         protected virtual void Dispose(bool disposing)
342         {
343             if (this.IsDisposed)
344                 return;
345
346             this.IsDisposed = true;
347
348             if (disposing)
349             {
350                 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
351                 this.http.Dispose();
352                 this.httpStreaming.Dispose();
353             }
354         }
355
356         ~TwitterApiConnection()
357         {
358             this.Dispose(false);
359         }
360
361         private void Networking_WebProxyChanged(object sender, EventArgs e)
362         {
363             this.InitializeHttpClients();
364         }
365
366         public static async Task<Tuple<string, string>> GetRequestTokenAsync()
367         {
368             var param = new Dictionary<string, string>
369             {
370                 ["oauth_callback"] = "oob",
371             };
372             var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, oauthToken: null)
373                 .ConfigureAwait(false);
374
375             return Tuple.Create(response["oauth_token"], response["oauth_token_secret"]);
376         }
377
378         public static Uri GetAuthorizeUri(Tuple<string, string> requestToken, string screenName = null)
379         {
380             var param = new Dictionary<string, string>
381             {
382                 ["oauth_token"] = requestToken.Item1,
383             };
384
385             if (screenName != null)
386                 param["screen_name"] = screenName;
387
388             return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
389         }
390
391         public static async Task<IDictionary<string, string>> GetAccessTokenAsync(Tuple<string, string> requestToken, string verifier)
392         {
393             var param = new Dictionary<string, string>
394             {
395                 ["oauth_verifier"] = verifier,
396             };
397             var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, requestToken)
398                 .ConfigureAwait(false);
399
400             return response;
401         }
402
403         private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(Uri uri, IDictionary<string, string> param,
404             Tuple<string, string> oauthToken)
405         {
406             HttpClient authorizeClient;
407             if (oauthToken != null)
408                 authorizeClient = InitializeHttpClient(oauthToken.Item1, oauthToken.Item2);
409             else
410                 authorizeClient = InitializeHttpClient("", "");
411
412             var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
413
414             try
415             {
416                 using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))
417                 using (var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
418                     .ConfigureAwait(false))
419                 using (var content = response.Content)
420                 {
421                     var responseText = await content.ReadAsStringAsync()
422                         .ConfigureAwait(false);
423
424                     if (!response.IsSuccessStatusCode)
425                         throw new TwitterApiException(response.StatusCode, responseText);
426
427                     var responseParams = HttpUtility.ParseQueryString(responseText);
428
429                     return responseParams.Cast<string>()
430                         .ToDictionary(x => x, x => responseParams[x]);
431                 }
432             }
433             catch (HttpRequestException ex)
434             {
435                 throw TwitterApiException.CreateFromException(ex);
436             }
437             catch (OperationCanceledException ex)
438             {
439                 throw TwitterApiException.CreateFromException(ex);
440             }
441         }
442
443         private static HttpClient InitializeHttpClient(string accessToken, string accessSecret, bool disableGzip = false)
444         {
445             var innerHandler = Networking.CreateHttpClientHandler();
446             innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
447
448             if (!disableGzip)
449                 innerHandler.AutomaticDecompression = DecompressionMethods.None;
450
451             var handler = new OAuthHandler(innerHandler,
452                 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
453                 accessToken, accessSecret);
454
455             return Networking.CreateHttpClient(handler);
456         }
457     }
458 }