OSDN Git Service

Merge pull request #44 from upsilon/csharp7
[opentween/open-tween.git] / OpenTween / Api / MicrosoftTranslatorApi.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.Linq;
25 using System.Net.Http;
26 using System.Net.Http.Headers;
27 using System.Runtime.Serialization.Json;
28 using System.Text;
29 using System.Threading.Tasks;
30 using System.Xml;
31 using System.Xml.Linq;
32 using OpenTween.Connection;
33
34 namespace OpenTween.Api
35 {
36     public class MicrosoftTranslatorApi
37     {
38         public static readonly Uri OAuthEndpoint = new Uri("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13");
39         public static readonly Uri TranslateEndpoint = new Uri("https://api.microsofttranslator.com/v2/Http.svc/Translate");
40
41         public string AccessToken { get; internal set; }
42         public DateTime RefreshAccessTokenAt { get; internal set; }
43
44         private HttpClient Http => this.localHttpClient ?? Networking.Http;
45         private readonly HttpClient localHttpClient;
46
47         public MicrosoftTranslatorApi()
48             : this(null)
49         {
50         }
51
52         public MicrosoftTranslatorApi(HttpClient http)
53         {
54             this.localHttpClient = http;
55         }
56
57         public async Task<string> TranslateAsync(string text, string langTo, string langFrom = null)
58         {
59             await this.UpdateAccessTokenIfExpired()
60                 .ConfigureAwait(false);
61
62             var param = new Dictionary<string, string>
63             {
64                 ["text"] = text,
65                 ["to"] = langTo,
66             };
67
68             if (langFrom != null)
69                 param["from"] = langFrom;
70
71             var requestUri = new Uri(TranslateEndpoint, "?" + MyCommon.BuildQueryString(param));
72
73             using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri))
74             {
75                 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.AccessToken);
76
77                 using (var response = await this.Http.SendAsync(request).ConfigureAwait(false))
78                 {
79                     return await response.Content.ReadAsStringAsync()
80                         .ConfigureAwait(false);
81                 }
82             }
83         }
84
85         public async Task UpdateAccessTokenIfExpired()
86         {
87             if (this.AccessToken != null && this.RefreshAccessTokenAt > DateTime.Now)
88                 return;
89
90             var (accessToken, expiresIn) = await this.GetAccessTokenAsync()
91                 .ConfigureAwait(false);
92
93             this.AccessToken = accessToken;
94
95             // expires_in の示す時刻より 30 秒早めに再発行する
96             this.RefreshAccessTokenAt = DateTime.Now + expiresIn - TimeSpan.FromSeconds(30);
97         }
98
99         internal virtual async Task<(string AccessToken, TimeSpan ExpiresIn)> GetAccessTokenAsync()
100         {
101             var param = new Dictionary<string, string>
102             {
103                 ["grant_type"] = "client_credentials",
104                 ["client_id"] = ApplicationSettings.AzureClientId,
105                 ["client_secret"] = ApplicationSettings.AzureClientSecret,
106                 ["scope"] = "http://api.microsofttranslator.com",
107             };
108
109             using (var request = new HttpRequestMessage(HttpMethod.Post, OAuthEndpoint))
110             using (var postContent = new FormUrlEncodedContent(param))
111             {
112                 request.Content = postContent;
113
114                 using (var response = await this.Http.SendAsync(request).ConfigureAwait(false))
115                 {
116                     var responseBytes = await response.Content.ReadAsByteArrayAsync()
117                         .ConfigureAwait(false);
118
119                     return ParseOAuthCredential(responseBytes);
120                 }
121             }
122         }
123
124         internal static (string AccessToken, TimeSpan ExpiresIn) ParseOAuthCredential(byte[] responseBytes)
125         {
126             using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(responseBytes, XmlDictionaryReaderQuotas.Max))
127             {
128                 var xElm = XElement.Load(jsonReader);
129
130                 var tokenTypeElm = xElm.Element("token_type");
131                 if (tokenTypeElm == null)
132                     throw new WebApiException("Property `token_type` required");
133
134                 var accessTokenElm = xElm.Element("access_token");
135                 if (accessTokenElm == null)
136                     throw new WebApiException("Property `access_token` required");
137
138                 var expiresInElm = xElm.Element("expires_in");
139
140                 int expiresInSeconds;
141                 if (expiresInElm != null)
142                 {
143                     if (!int.TryParse(expiresInElm.Value, out expiresInSeconds))
144                         throw new WebApiException("Invalid number: expires_in = " + expiresInElm.Value);
145                 }
146                 else
147                 {
148                     // expires_in が省略された場合は有効期間が不明なので、
149                     // 次回のリクエスト時は経過時間に関わらずアクセストークンの再発行を行う
150                     expiresInSeconds = 0;
151                 }
152
153                 return (accessTokenElm.Value, TimeSpan.FromSeconds(expiresInSeconds));
154             }
155         }
156     }
157 }