OSDN Git Service

C# 8.0 のnull許容参照型を有効化
[opentween/open-tween.git] / OpenTween / Thumbnail / Services / FoursquareCheckin.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2012 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 #nullable enable
23
24 using System;
25 using System.Collections.Generic;
26 using System.Globalization;
27 using System.Net.Http;
28 using System.Runtime.Serialization.Json;
29 using System.Text.RegularExpressions;
30 using System.Threading;
31 using System.Threading.Tasks;
32 using System.Web;
33 using System.Xml;
34 using System.Xml.Linq;
35 using System.Xml.XPath;
36 using OpenTween.Connection;
37 using OpenTween.Models;
38
39 namespace OpenTween.Thumbnail.Services
40 {
41     class FoursquareCheckin : IThumbnailService
42     {
43         public static readonly Regex UrlPatternRegex =
44             new Regex(@"^https?://www\.swarmapp\.com/c/(?<checkin_id>[0-9a-zA-Z]+)");
45
46         public static readonly Regex LegacyUrlPatternRegex =
47             new Regex(@"^https?://(?:foursquare\.com|www\.swarmapp\.com)/.+?/checkin/(?<checkin_id>[0-9a-z]+)(?:\?s=(?<signature>[^&]+))?");
48
49         public static readonly string ApiBase = "https://api.foursquare.com/v2";
50
51         protected HttpClient http
52             => this.localHttpClient ?? Networking.Http;
53
54         private readonly HttpClient? localHttpClient;
55
56         public FoursquareCheckin()
57             : this(null)
58         {
59         }
60
61         public FoursquareCheckin(HttpClient? http)
62             => this.localHttpClient = http;
63
64         public override async Task<ThumbnailInfo?> GetThumbnailInfoAsync(string url, PostClass post, CancellationToken token)
65         {
66             // ツイートに位置情報が付与されている場合は何もしない
67             if (post.PostGeo != null)
68                 return null;
69
70             var location = await this.FetchCheckinLocation(url, token)
71                 .ConfigureAwait(false);
72
73             if (location == null)
74             {
75                 location = await this.FetchCheckinLocationLegacy(url, token)
76                     .ConfigureAwait(false);
77             }
78
79             if (location != null)
80             {
81                 var map = MapThumb.GetDefaultInstance();
82
83                 return await map.GetThumbnailInfoAsync(new PostClass.StatusGeo(location.Longitude, location.Latitude))
84                     .ConfigureAwait(false);
85             }
86
87             return null;
88         }
89
90         /// <summary>
91         /// Foursquare のチェックイン URL から位置情報を取得します
92         /// </summary>
93         public async Task<GlobalLocation?> FetchCheckinLocation(string url, CancellationToken token)
94         {
95             var match = UrlPatternRegex.Match(url);
96             if (!match.Success)
97                 return null;
98
99             var checkinIdGroup = match.Groups["checkin_id"];
100
101             try
102             {
103                 // Foursquare のチェックイン情報を取得
104                 // 参照: https://developer.foursquare.com/docs/checkins/resolve
105
106                 var query = new Dictionary<string, string>
107                 {
108                     ["client_id"] = ApplicationSettings.FoursquareClientId,
109                     ["client_secret"] = ApplicationSettings.FoursquareClientSecret,
110                     ["v"] = "20140419", // https://developer.foursquare.com/overview/versioning
111
112                     ["shortId"] = checkinIdGroup.Value,
113                 };
114
115                 var apiUrl = new Uri(ApiBase + "/checkins/resolve?" + MyCommon.BuildQueryString(query));
116
117                 using var response = await this.http.GetAsync(apiUrl, token)
118                     .ConfigureAwait(false);
119
120                 response.EnsureSuccessStatusCode();
121
122                 var jsonBytes = await response.Content.ReadAsByteArrayAsync()
123                     .ConfigureAwait(false);
124
125                 return ParseIntoLocation(jsonBytes);
126             }
127             catch (HttpRequestException)
128             {
129                 return null;
130             }
131         }
132
133         /// <summary>
134         /// Foursquare のチェックイン URL から位置情報を取得します (古い形式の URL)
135         /// </summary>
136         public async Task<GlobalLocation?> FetchCheckinLocationLegacy(string url, CancellationToken token)
137         {
138             var match = LegacyUrlPatternRegex.Match(url);
139
140             if (!match.Success)
141                 return null;
142
143             var checkinIdGroup = match.Groups["checkin_id"];
144             var signatureGroup = match.Groups["signature"];
145
146             try
147             {
148                 // Foursquare のベニュー情報を取得
149                 // 参照: https://developer.foursquare.com/docs/venues/venues
150
151                 var query = new Dictionary<string, string>
152                 {
153                     ["client_id"] = ApplicationSettings.FoursquareClientId,
154                     ["client_secret"] = ApplicationSettings.FoursquareClientSecret,
155                     ["v"] = "20140419", // https://developer.foursquare.com/overview/versioning
156                 };
157
158                 if (signatureGroup.Success)
159                     query["signature"] = signatureGroup.Value;
160
161                 var apiUrl = new Uri(ApiBase + "/checkins/" + checkinIdGroup.Value + "?" + MyCommon.BuildQueryString(query));
162
163                 using var response = await this.http.GetAsync(apiUrl, token)
164                     .ConfigureAwait(false);
165
166                 response.EnsureSuccessStatusCode();
167
168                 var jsonBytes = await response.Content.ReadAsByteArrayAsync()
169                     .ConfigureAwait(false);
170
171                 return ParseIntoLocation(jsonBytes);
172             }
173             catch (HttpRequestException)
174             {
175                 return null;
176             }
177         }
178
179         internal static GlobalLocation? ParseIntoLocation(byte[] jsonBytes)
180         {
181             using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(jsonBytes, XmlDictionaryReaderQuotas.Max);
182             var xElm = XElement.Load(jsonReader);
183
184             var locationElm = xElm.XPathSelectElement("/response/checkin/venue/location");
185
186             // 座標が得られなかった場合
187             if (locationElm == null)
188                 return null;
189
190             // 月など、地球以外の星の座標である場合
191             var planetElm = locationElm.Element("planet");
192             if (planetElm != null && planetElm.Value != "earth")
193                 return null;
194
195             return new GlobalLocation
196             {
197                 Latitude = double.Parse(locationElm.Element("lat").Value, CultureInfo.InvariantCulture),
198                 Longitude = double.Parse(locationElm.Element("lng").Value, CultureInfo.InvariantCulture),
199             };
200         }
201     }
202 }