OSDN Git Service

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