OSDN Git Service

C# 8.0 のnull許容参照型を有効化
[opentween/open-tween.git] / OpenTween / Api / TwitterApiStatus.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2013 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.Concurrent;
26 using System.Collections.Generic;
27 using System.Collections;
28 using System.Linq;
29 using System.Runtime.Serialization.Json;
30 using System.Text;
31 using System.Xml;
32 using System.Xml.Linq;
33 using System.Xml.XPath;
34 using OpenTween.Api.DataModel;
35 using System.Net.Http.Headers;
36
37 namespace OpenTween.Api
38 {
39     public class TwitterApiStatus
40     {
41         public TwitterApiAccessLevel AccessLevel { get; set; }
42         public EndpointLimits AccessLimit { get; }
43         public ApiLimit? MediaUploadLimit { get; set; }
44
45         public class AccessLimitUpdatedEventArgs : EventArgs
46         {
47             public string? EndpointName { get; }
48
49             public AccessLimitUpdatedEventArgs(string? endpointName)
50                 => this.EndpointName = endpointName;
51         }
52         public event EventHandler<AccessLimitUpdatedEventArgs> AccessLimitUpdated;
53
54         public TwitterApiStatus()
55             => this.AccessLimit = new EndpointLimits(this);
56
57         public void Reset()
58         {
59             this.AccessLevel = TwitterApiAccessLevel.Anonymous;
60             this.AccessLimit.Clear();
61             this.MediaUploadLimit = null;
62         }
63
64         internal static ApiLimit? ParseRateLimit(IDictionary<string, string> header, string prefix)
65         {
66             var limitCount = (int?)ParseHeaderValue(header, prefix + "Limit");
67             var limitRemain = (int?)ParseHeaderValue(header, prefix + "Remaining");
68             var limitReset = ParseHeaderValue(header, prefix + "Reset");
69
70             if (limitCount == null || limitRemain == null || limitReset == null)
71                 return null;
72
73             var limitResetDate = DateTimeUtc.FromUnixTime(limitReset.Value);
74             return new ApiLimit(limitCount.Value, limitRemain.Value, limitResetDate);
75         }
76
77         internal static TwitterApiAccessLevel? ParseAccessLevel(IDictionary<string, string> header, string headerName)
78         {
79             if (!header.ContainsKey(headerName))
80                 return null;
81
82             // たまに出てくる空文字列は無視する
83             if (string.IsNullOrEmpty(header[headerName]))
84                 return null;
85
86             switch (header[headerName])
87             {
88                 case "read-write-directmessages":
89                 case "read-write-privatemessages":
90                     return TwitterApiAccessLevel.ReadWriteAndDirectMessage;
91                 case "read-write":
92                     return TwitterApiAccessLevel.ReadWrite;
93                 case "read":
94                     return TwitterApiAccessLevel.Read;
95                 default:
96                     MyCommon.TraceOut("Unknown ApiAccessLevel:" + header[headerName]);
97                     return TwitterApiAccessLevel.ReadWriteAndDirectMessage;
98             }
99         }
100
101         internal static long? ParseHeaderValue(IDictionary<string, string> dict, params string[] keys)
102         {
103             foreach (var key in keys)
104             {
105                 if (!dict.ContainsKey(key)) continue;
106
107                 if (long.TryParse(dict[key], out var result))
108                     return result;
109             }
110
111             return null;
112         }
113
114         public void UpdateFromHeader(HttpResponseHeaders header, string endpointName)
115             => this.UpdateFromHeader(header.ToDictionary(x => x.Key, x => string.Join(",", x.Value), StringComparer.OrdinalIgnoreCase), endpointName);
116
117         public void UpdateFromHeader(IDictionary<string, string> header, string endpointName)
118         {
119             var rateLimit = TwitterApiStatus.ParseRateLimit(header, "X-Rate-Limit-");
120             if (rateLimit != null)
121                 this.AccessLimit[endpointName] = rateLimit;
122
123             var mediaLimit = TwitterApiStatus.ParseRateLimit(header, "X-MediaRateLimit-");
124             if (mediaLimit != null)
125                 this.MediaUploadLimit = mediaLimit;
126
127             var accessLevel = TwitterApiStatus.ParseAccessLevel(header, "X-Access-Level");
128             if (accessLevel.HasValue)
129                 this.AccessLevel = accessLevel.Value;
130         }
131
132         public void UpdateFromJson(TwitterRateLimits json)
133         {
134             var rateLimits =
135                 from res in json.Resources
136                 from item in res.Value
137                 select (
138                     EndpointName: item.Key,
139                     Limit: new ApiLimit(
140                         item.Value.Limit,
141                         item.Value.Remaining,
142                         DateTimeUtc.FromUnixTime(item.Value.Reset)
143                     )
144                 );
145
146             this.AccessLimit.AddAll(rateLimits.ToDictionary(x => x.EndpointName, x => x.Limit));
147         }
148
149         protected virtual void OnAccessLimitUpdated(AccessLimitUpdatedEventArgs e)
150             => this.AccessLimitUpdated?.Invoke(this, e);
151
152         public class EndpointLimits : IEnumerable<KeyValuePair<string, ApiLimit>>
153         {
154             public TwitterApiStatus Owner { get; }
155
156             private readonly ConcurrentDictionary<string, ApiLimit> innerDict
157                 = new ConcurrentDictionary<string, ApiLimit>();
158
159             public EndpointLimits(TwitterApiStatus owner)
160                 => this.Owner = owner;
161
162             public ApiLimit? this[string endpoint]
163             {
164                 get => this.innerDict.TryGetValue(endpoint, out var limit) ? limit : null;
165                 set
166                 {
167                     if (value == null)
168                         this.innerDict.TryRemove(endpoint, out var _);
169                     else
170                         this.innerDict[endpoint] = value;
171
172                     this.Owner.OnAccessLimitUpdated(new AccessLimitUpdatedEventArgs(endpoint));
173                 }
174             }
175
176             public void Clear()
177             {
178                 this.innerDict.Clear();
179                 this.Owner.OnAccessLimitUpdated(new AccessLimitUpdatedEventArgs(null));
180             }
181
182             public void AddAll(IDictionary<string, ApiLimit> resources)
183             {
184                 foreach (var (key, value) in resources)
185                 {
186                     this.innerDict[key] = value;
187                 }
188
189                 this.Owner.OnAccessLimitUpdated(new AccessLimitUpdatedEventArgs(null));
190             }
191
192             public IEnumerator<KeyValuePair<string, ApiLimit>> GetEnumerator()
193                 => this.innerDict.GetEnumerator();
194
195             IEnumerator IEnumerable.GetEnumerator()
196                 => this.GetEnumerator();
197         }
198     }
199 }