OSDN Git Service

C# 8.0 のnull許容参照型を有効化
[opentween/open-tween.git] / OpenTween / Thumbnail / Services / ImgAzyobuziNet.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.Linq;
27 using System.Net.Http;
28 using System.Runtime.Serialization.Json;
29 using System.Xml;
30 using System.Xml.Linq;
31 using System.Text.RegularExpressions;
32 using System.Threading;
33 using System.Threading.Tasks;
34 using OpenTween.Connection;
35 using OpenTween.Models;
36
37 namespace OpenTween.Thumbnail.Services
38 {
39     class ImgAzyobuziNet : IThumbnailService, IDisposable
40     {
41         protected string[] ApiHosts = {
42             "https://img.azyobuzi.net/api/",
43             "https://img.opentween.org/api/",
44         };
45
46         protected string[] ExcludedServiceNames =
47         {
48             "Instagram",
49             "Twitter",
50             "Tumblr",
51             "Gyazo",
52         };
53
54         protected string? ApiBase;
55         protected IEnumerable<Regex>? UrlRegex = null;
56         protected Timer UpdateTimer;
57
58         protected HttpClient http
59             => this.localHttpClient ?? Networking.Http;
60
61         private readonly HttpClient? localHttpClient;
62
63         private readonly object LockObj = new object();
64
65         public ImgAzyobuziNet(bool autoupdate)
66             : this(null, autoupdate)
67         {
68         }
69
70         public ImgAzyobuziNet(HttpClient? http)
71             : this(http, autoupdate: false)
72         {
73         }
74
75         public ImgAzyobuziNet(HttpClient? http, bool autoupdate)
76         {
77             this.UpdateTimer = new Timer(async _ => await this.LoadRegexAsync());
78             this.AutoUpdate = autoupdate;
79
80             this.Enabled = true;
81             this.DisabledInDM = true;
82
83             this.localHttpClient = http;
84         }
85
86         public bool AutoUpdate
87         {
88             get => this._AutoUpdate;
89             set
90             {
91                 if (value)
92                     this.StartAutoUpdate();
93                 else
94                     this.StopAutoUpdate();
95
96                 this._AutoUpdate = value;
97             }
98         }
99         private bool _AutoUpdate = false;
100
101         /// <summary>
102         /// img.azyobizi.net によるサムネイル情報の取得を有効にするか
103         /// </summary>
104         public bool Enabled { get; set; }
105
106         /// <summary>
107         /// ダイレクトメッセージに含まれる画像URLに対して img.azyobuzi.net を使用しない
108         /// </summary>
109         public bool DisabledInDM { get; set; }
110
111         protected void StartAutoUpdate()
112             => this.UpdateTimer.Change(0, 30 * 60 * 1000); // 30分おきに更新
113
114         protected void StopAutoUpdate()
115             => this.UpdateTimer.Change(Timeout.Infinite, Timeout.Infinite);
116
117         public async Task LoadRegexAsync()
118         {
119             foreach (var host in this.ApiHosts)
120             {
121                 try
122                 {
123                     var result = await this.LoadRegexAsync(host)
124                         .ConfigureAwait(false);
125
126                     if (result) return;
127                 }
128                 catch (Exception)
129                 {
130 #if DEBUG
131                     throw;
132 #endif
133                 }
134             }
135
136             // どのサーバーも使用できない場合
137             lock (this.LockObj)
138             {
139                 this.UrlRegex = null;
140                 this.ApiBase = null;
141             }
142         }
143
144         public async Task<bool> LoadRegexAsync(string apiBase)
145         {
146             try
147             {
148                 var jsonBytes = await this.FetchRegexAsync(apiBase)
149                     .ConfigureAwait(false);
150
151                 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(jsonBytes, XmlDictionaryReaderQuotas.Max);
152                 var xElm = XElement.Load(jsonReader);
153
154                 if (xElm.Element("error") != null)
155                     return false;
156
157                 lock (this.LockObj)
158                 {
159                     this.UrlRegex = xElm.Elements("item")
160                         .Where(x => !this.ExcludedServiceNames.Contains(x.Element("name").Value))
161                         .Select(e => new Regex(e.Element("regex").Value, RegexOptions.IgnoreCase))
162                         .ToArray();
163
164                     this.ApiBase = apiBase;
165                 }
166
167                 return true;
168             }
169             catch (HttpRequestException) { } // サーバーが2xx以外のステータスコードを返した場合
170             catch (OperationCanceledException) { } // リクエストがタイムアウトした場合
171             catch (XmlException) { } // サーバーが不正なJSONを返した場合
172
173             return false;
174         }
175
176         protected virtual async Task<byte[]> FetchRegexAsync(string apiBase)
177         {
178             using var cts = new CancellationTokenSource(millisecondsDelay: 1000);
179             using var response = await this.http.GetAsync(apiBase + "regex.json", cts.Token)
180                 .ConfigureAwait(false);
181
182             response.EnsureSuccessStatusCode();
183
184             return await response.Content.ReadAsByteArrayAsync()
185                 .ConfigureAwait(false);
186         }
187
188         public override Task<ThumbnailInfo?> GetThumbnailInfoAsync(string url, PostClass post, CancellationToken token)
189         {
190             return Task.Run(() =>
191             {
192                 if (!this.Enabled)
193                     return null;
194
195                 if (this.DisabledInDM && post != null && post.IsDm)
196                     return null;
197
198                 lock (this.LockObj)
199                 {
200                     if (this.UrlRegex == null)
201                         return null;
202
203                     foreach (var regex in this.UrlRegex)
204                     {
205                         if (regex.IsMatch(url))
206                         {
207                             return new ThumbnailInfo
208                             {
209                                 MediaPageUrl = url,
210                                 ThumbnailImageUrl = this.ApiBase + "redirect?size=large&uri=" + Uri.EscapeDataString(url),
211                                 FullSizeImageUrl = this.ApiBase + "redirect?size=full&uri=" + Uri.EscapeDataString(url),
212                                 TooltipText = null,
213                             };
214                         }
215                     }
216                 }
217
218                 return null;
219             }, token);
220         }
221
222         public virtual void Dispose()
223             => this.UpdateTimer.Dispose();
224     }
225 }