OSDN Git Service

/notifications/mentions.json を使用したReplyタブの更新に対応
[opentween/open-tween.git] / OpenTween / Api / TwitterV2 / NotificationsMentionsRequest.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2024 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.Linq;
28 using System.Runtime.Serialization;
29 using System.Runtime.Serialization.Json;
30 using System.Text;
31 using System.Threading.Tasks;
32 using System.Xml;
33 using System.Xml.Linq;
34 using System.Xml.XPath;
35 using OpenTween.Api.DataModel;
36 using OpenTween.Api.GraphQL;
37 using OpenTween.Connection;
38
39 namespace OpenTween.Api.TwitterV2
40 {
41     public class NotificationsMentionsRequest
42     {
43         public static readonly string EndpointName = "/2/notifications/mentions";
44
45         private static readonly Uri EndpointUri = new("https://twitter.com/i/api/2/notifications/mentions.json");
46
47         public int Count { get; set; } = 100;
48
49         public string? Cursor { get; set; }
50
51         public Dictionary<string, string> CreateParameters()
52         {
53             var param = new Dictionary<string, string>()
54             {
55                 ["include_profile_interstitial_type"] = "1",
56                 ["include_blocking"] = "1",
57                 ["include_blocked_by"] = "1",
58                 ["include_followed_by"] = "1",
59                 ["include_want_retweets"] = "1",
60                 ["include_mute_edge"] = "1",
61                 ["include_can_dm"] = "1",
62                 ["include_can_media_tag"] = "1",
63                 ["include_ext_has_nft_avatar"] = "1",
64                 ["include_ext_is_blue_verified"] = "1",
65                 ["include_ext_verified_type"] = "1",
66                 ["include_ext_profile_image_shape"] = "1",
67                 ["skip_status"] = "1",
68                 ["cards_platform"] = "Web-12",
69                 ["include_cards"] = "1",
70                 ["include_ext_alt_text"] = "true",
71                 ["include_ext_limited_action_results"] = "true",
72                 ["include_quote_count"] = "true",
73                 ["include_reply_count"] = "1",
74                 ["tweet_mode"] = "extended",
75                 ["include_ext_views"] = "true",
76                 ["include_entities"] = "true",
77                 ["include_user_entities"] = "true",
78                 ["include_ext_media_color"] = "true",
79                 ["include_ext_media_availability"] = "true",
80                 ["include_ext_sensitive_media_warning"] = "true",
81                 ["include_ext_trusted_friends_metadata"] = "true",
82                 ["send_error_codes"] = "true",
83                 ["simple_quoted_tweet"] = "true",
84                 ["requestContext"] = "ptr",
85                 ["ext"] = "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,superFollowMetadata,unmentionInfo,editControl",
86                 ["count"] = this.Count.ToString(CultureInfo.InvariantCulture),
87             };
88
89             if (!MyCommon.IsNullOrEmpty(this.Cursor))
90                 param["cursor"] = this.Cursor;
91
92             return param;
93         }
94
95         public async Task<NotificationsResponse> Send(IApiConnection apiConnection)
96         {
97             var request = new GetRequest
98             {
99                 RequestUri = EndpointUri,
100                 Query = this.CreateParameters(),
101                 EndpointName = EndpointName,
102             };
103
104             using var response = await apiConnection.SendAsync(request)
105                 .ConfigureAwait(false);
106
107             var responseBytes = await response.ReadAsBytes()
108                 .ConfigureAwait(false);
109
110             ResponseRoot parsedObjects;
111             XElement rootElm;
112             try
113             {
114                 parsedObjects = MyCommon.CreateDataFromJson<ResponseRoot>(responseBytes);
115
116                 using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(
117                     responseBytes,
118                     XmlDictionaryReaderQuotas.Max
119                 );
120
121                 rootElm = XElement.Load(jsonReader);
122             }
123             catch (SerializationException ex)
124             {
125                 var responseText = Encoding.UTF8.GetString(responseBytes);
126                 throw TwitterApiException.CreateFromException(ex, responseText);
127             }
128             catch (XmlException ex)
129             {
130                 var responseText = Encoding.UTF8.GetString(responseBytes);
131                 throw new TwitterApiException("Invalid JSON", ex) { ResponseText = responseText };
132             }
133
134             ErrorResponse.ThrowIfError(rootElm);
135
136             var tweetIds = rootElm.XPathSelectElements("//content/item/content/tweet/id")
137                 .Select(x => x.Value)
138                 .ToArray();
139
140             var statuses = new List<TwitterStatus>(tweetIds.Length);
141             foreach (var tweetId in tweetIds)
142             {
143                 if (!parsedObjects.GlobalObjects.Tweets.TryGetValue(tweetId, out var tweet))
144                     continue;
145
146                 var userId = tweet.UserId;
147                 if (!parsedObjects.GlobalObjects.Users.TryGetValue(userId, out var user))
148                     continue;
149
150                 tweet.User = user;
151                 statuses.Add(tweet);
152             }
153
154             var tweets = TimelineTweet.ExtractTimelineTweets(rootElm);
155             var cursorTop = rootElm.XPathSelectElement("//content/operation/cursor[cursorType[text()='Top']]/value")?.Value;
156             var cursorBottom = rootElm.XPathSelectElement("//content/operation/cursor[cursorType[text()='Bottom']]/value")?.Value;
157
158             return new(statuses.ToArray(), cursorTop, cursorBottom);
159         }
160
161         [DataContract]
162         private record ResponseRoot(
163             [property: DataMember(Name = "globalObjects")]
164             ResponseGlobalObjects GlobalObjects
165         );
166
167         [DataContract]
168         private record ResponseGlobalObjects(
169             [property: DataMember(Name = "users")]
170             Dictionary<string, TwitterUser> Users,
171             [property: DataMember(Name = "tweets")]
172             Dictionary<string, ResponseTweet> Tweets
173         );
174
175         [DataContract]
176         private class ResponseTweet : TwitterStatus
177         {
178             [DataMember(Name = "user_id")]
179             public string UserId { get; set; } = "";
180         }
181
182         public readonly record struct NotificationsResponse(
183             TwitterStatus[] Statuses,
184             string? CursorTop,
185             string? CursorBottom
186         );
187     }
188 }