OSDN Git Service

4fc601b8b3b6956c52be4385d326a31fe2fdb360
[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 Uri RestApiBase { get; set; } = new Uri("https://api.twitter.com/1.1/");
41
42         public bool IsDisposed { get; private set; } = false;
43
44         public string AccessToken { get; }
45         public string AccessSecret { get; }
46
47         internal HttpClient http;
48
49         public TwitterApiConnection(string accessToken, string accessSecret)
50         {
51             this.AccessToken = accessToken;
52             this.AccessSecret = accessSecret;
53
54             this.http = InitializeHttpClient(accessToken, accessSecret);
55             Networking.WebProxyChanged += this.Networking_WebProxyChanged;
56         }
57
58         public Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> param)
59             => this.GetAsync<T>(uri, param, null);
60
61         public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> param, string endpointName)
62         {
63             var requestUri = new Uri(this.RestApiBase, uri);
64
65             if (param != null)
66                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
67
68             var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
69
70             try
71             {
72                 using (var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
73                     .ConfigureAwait(false))
74                 {
75                     await this.CheckStatusCode(response)
76                         .ConfigureAwait(false);
77
78                     if (endpointName != null)
79                         MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
80
81                     using (var content = response.Content)
82                     {
83                         var responseText = await content.ReadAsStringAsync()
84                             .ConfigureAwait(false);
85
86                         try
87                         {
88                             return MyCommon.CreateDataFromJson<T>(responseText);
89                         }
90                         catch (SerializationException ex)
91                         {
92                             throw TwitterApiException.CreateFromException(ex, responseText);
93                         }
94                     }
95                 }
96             }
97             catch (OperationCanceledException ex)
98             {
99                 throw TwitterApiException.CreateFromException(ex);
100             }
101         }
102
103         public async Task<Stream> GetStreamAsync(Uri uri, IDictionary<string, string> param)
104         {
105             var requestUri = new Uri(this.RestApiBase, uri);
106
107             if (param != null)
108                 requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param));
109
110             try
111             {
112                 return await this.http.GetStreamAsync(requestUri)
113                     .ConfigureAwait(false);
114             }
115             catch (OperationCanceledException ex)
116             {
117                 throw TwitterApiException.CreateFromException(ex);
118             }
119         }
120
121         public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param)
122         {
123             var requestUri = new Uri(this.RestApiBase, uri);
124             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
125
126             using (var postContent = new FormUrlEncodedContent(param))
127             {
128                 request.Content = postContent;
129
130                 HttpResponseMessage response = null;
131                 try
132                 {
133                     response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
134                         .ConfigureAwait(false);
135
136                     await this.CheckStatusCode(response)
137                         .ConfigureAwait(false);
138
139                     var result = new LazyJson<T>(response);
140                     response = null;
141
142                     return result;
143                 }
144                 catch (OperationCanceledException ex)
145                 {
146                     throw TwitterApiException.CreateFromException(ex);
147                 }
148                 finally
149                 {
150                     response?.Dispose();
151                 }
152             }
153         }
154
155         public async Task<LazyJson<T>> PostLazyAsync<T>(Uri uri, IDictionary<string, string> param, IDictionary<string, IMediaItem> media)
156         {
157             var requestUri = new Uri(this.RestApiBase, uri);
158             var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
159
160             using (var postContent = new MultipartFormDataContent())
161             {
162                 foreach (var kv in param)
163                     postContent.Add(new StringContent(kv.Value), kv.Key);
164
165                 foreach (var kv in media)
166                     postContent.Add(new StreamContent(kv.Value.OpenRead()), kv.Key, kv.Value.Name);
167
168                 request.Content = postContent;
169
170                 HttpResponseMessage response = null;
171                 try
172                 {
173                     response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
174                         .ConfigureAwait(false);
175
176                     await this.CheckStatusCode(response)
177                         .ConfigureAwait(false);
178
179                     var result = new LazyJson<T>(response);
180                     response = null;
181
182                     return result;
183                 }
184                 catch (OperationCanceledException ex)
185                 {
186                     throw TwitterApiException.CreateFromException(ex);
187                 }
188                 finally
189                 {
190                     response?.Dispose();
191                 }
192             }
193         }
194
195         protected async Task CheckStatusCode(HttpResponseMessage response)
196         {
197             var statusCode = response.StatusCode;
198             if (statusCode == HttpStatusCode.OK)
199             {
200                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
201                 return;
202             }
203
204             string responseText;
205             using (var content = response.Content)
206             {
207                 responseText = await content.ReadAsStringAsync()
208                     .ConfigureAwait(false);
209             }
210
211             if (string.IsNullOrWhiteSpace(responseText))
212             {
213                 if (statusCode == HttpStatusCode.Unauthorized)
214                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
215
216                 throw new TwitterApiException(statusCode, responseText);
217             }
218
219             try
220             {
221                 var error = TwitterError.ParseJson(responseText);
222
223                 if (error?.Errors == null || error.Errors.Length == 0)
224                     throw new TwitterApiException(statusCode, responseText);
225
226                 var errorCodes = error.Errors.Select(x => x.Code);
227                 if (errorCodes.Any(x => x == TwitterErrorCode.InternalError || x == TwitterErrorCode.SuspendedAccount))
228                 {
229                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
230                 }
231
232                 throw new TwitterApiException(error, responseText);
233             }
234             catch (SerializationException)
235             {
236                 throw new TwitterApiException(statusCode, responseText);
237             }
238         }
239
240         public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri realm = null)
241         {
242             var uri = new Uri(this.RestApiBase, authServiceProvider);
243
244             return OAuthEchoHandler.CreateHandler(Networking.CreateHttpClientHandler(), uri,
245                 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
246                 this.AccessToken, this.AccessSecret, realm);
247         }
248
249         public void Dispose()
250         {
251             this.Dispose(true);
252             GC.SuppressFinalize(this);
253         }
254
255         protected virtual void Dispose(bool disposing)
256         {
257             if (this.IsDisposed)
258                 return;
259
260             this.IsDisposed = true;
261
262             if (disposing)
263             {
264                 Networking.WebProxyChanged -= this.Networking_WebProxyChanged;
265                 this.http.Dispose();
266             }
267         }
268
269         ~TwitterApiConnection()
270         {
271             this.Dispose(false);
272         }
273
274         private void Networking_WebProxyChanged(object sender, EventArgs e)
275         {
276             this.http = InitializeHttpClient(this.AccessToken, this.AccessSecret);
277         }
278
279         public static async Task<Tuple<string, string>> GetRequestTokenAsync()
280         {
281             var param = new Dictionary<string, string>
282             {
283                 ["oauth_callback"] = "oob",
284             };
285             var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, oauthToken: null)
286                 .ConfigureAwait(false);
287
288             return Tuple.Create(response["oauth_token"], response["oauth_token_secret"]);
289         }
290
291         public static Uri GetAuthorizeUri(Tuple<string, string> requestToken, string screenName = null)
292         {
293             var param = new Dictionary<string, string>
294             {
295                 ["oauth_token"] = requestToken.Item1,
296             };
297
298             if (screenName != null)
299                 param["screen_name"] = screenName;
300
301             return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param));
302         }
303
304         public static async Task<IDictionary<string, string>> GetAccessTokenAsync(Tuple<string, string> requestToken, string verifier)
305         {
306             var param = new Dictionary<string, string>
307             {
308                 ["oauth_verifier"] = verifier,
309             };
310             var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, requestToken)
311                 .ConfigureAwait(false);
312
313             return response;
314         }
315
316         private static async Task<IDictionary<string, string>> GetOAuthTokenAsync(Uri uri, IDictionary<string, string> param,
317             Tuple<string, string> oauthToken)
318         {
319             HttpClient authorizeClient;
320             if (oauthToken != null)
321                 authorizeClient = InitializeHttpClient(oauthToken.Item1, oauthToken.Item2);
322             else
323                 authorizeClient = InitializeHttpClient("", "");
324
325             var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param));
326
327             try
328             {
329                 using (var request = new HttpRequestMessage(HttpMethod.Post, requestUri))
330                 using (var response = await authorizeClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
331                     .ConfigureAwait(false))
332                 using (var content = response.Content)
333                 {
334                     var responseText = await content.ReadAsStringAsync()
335                         .ConfigureAwait(false);
336
337                     if (!response.IsSuccessStatusCode)
338                         throw new TwitterApiException(response.StatusCode, responseText);
339
340                     var responseParams = HttpUtility.ParseQueryString(responseText);
341
342                     return responseParams.Cast<string>()
343                         .ToDictionary(x => x, x => responseParams[x]);
344                 }
345             }
346             catch (OperationCanceledException ex)
347             {
348                 throw TwitterApiException.CreateFromException(ex);
349             }
350         }
351
352         private static HttpClient InitializeHttpClient(string accessToken, string accessSecret)
353         {
354             var innerHandler = Networking.CreateHttpClientHandler();
355             innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
356
357             var handler = new OAuthHandler(innerHandler,
358                 ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret,
359                 accessToken, accessSecret);
360
361             return Networking.CreateHttpClient(handler);
362         }
363     }
364 }