OSDN Git Service

Merge pull request #305 from opentween/mentions-endpoint
[opentween/open-tween.git] / OpenTween / Api / TwitterV2 / NotificationsMentionsRequest.cs
diff --git a/OpenTween/Api/TwitterV2/NotificationsMentionsRequest.cs b/OpenTween/Api/TwitterV2/NotificationsMentionsRequest.cs
new file mode 100644 (file)
index 0000000..08063ea
--- /dev/null
@@ -0,0 +1,188 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using OpenTween.Api.DataModel;
+using OpenTween.Api.GraphQL;
+using OpenTween.Connection;
+
+namespace OpenTween.Api.TwitterV2
+{
+    public class NotificationsMentionsRequest
+    {
+        public static readonly string EndpointName = "/2/notifications/mentions";
+
+        private static readonly Uri EndpointUri = new("https://twitter.com/i/api/2/notifications/mentions.json");
+
+        public int Count { get; set; } = 100;
+
+        public string? Cursor { get; set; }
+
+        public Dictionary<string, string> CreateParameters()
+        {
+            var param = new Dictionary<string, string>()
+            {
+                ["include_profile_interstitial_type"] = "1",
+                ["include_blocking"] = "1",
+                ["include_blocked_by"] = "1",
+                ["include_followed_by"] = "1",
+                ["include_want_retweets"] = "1",
+                ["include_mute_edge"] = "1",
+                ["include_can_dm"] = "1",
+                ["include_can_media_tag"] = "1",
+                ["include_ext_has_nft_avatar"] = "1",
+                ["include_ext_is_blue_verified"] = "1",
+                ["include_ext_verified_type"] = "1",
+                ["include_ext_profile_image_shape"] = "1",
+                ["skip_status"] = "1",
+                ["cards_platform"] = "Web-12",
+                ["include_cards"] = "1",
+                ["include_ext_alt_text"] = "true",
+                ["include_ext_limited_action_results"] = "true",
+                ["include_quote_count"] = "true",
+                ["include_reply_count"] = "1",
+                ["tweet_mode"] = "extended",
+                ["include_ext_views"] = "true",
+                ["include_entities"] = "true",
+                ["include_user_entities"] = "true",
+                ["include_ext_media_color"] = "true",
+                ["include_ext_media_availability"] = "true",
+                ["include_ext_sensitive_media_warning"] = "true",
+                ["include_ext_trusted_friends_metadata"] = "true",
+                ["send_error_codes"] = "true",
+                ["simple_quoted_tweet"] = "true",
+                ["requestContext"] = "ptr",
+                ["ext"] = "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,superFollowMetadata,unmentionInfo,editControl",
+                ["count"] = this.Count.ToString(CultureInfo.InvariantCulture),
+            };
+
+            if (!MyCommon.IsNullOrEmpty(this.Cursor))
+                param["cursor"] = this.Cursor;
+
+            return param;
+        }
+
+        public async Task<NotificationsResponse> Send(IApiConnection apiConnection)
+        {
+            var request = new GetRequest
+            {
+                RequestUri = EndpointUri,
+                Query = this.CreateParameters(),
+                EndpointName = EndpointName,
+            };
+
+            using var response = await apiConnection.SendAsync(request)
+                .ConfigureAwait(false);
+
+            var responseBytes = await response.ReadAsBytes()
+                .ConfigureAwait(false);
+
+            ResponseRoot parsedObjects;
+            XElement rootElm;
+            try
+            {
+                parsedObjects = MyCommon.CreateDataFromJson<ResponseRoot>(responseBytes);
+
+                using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(
+                    responseBytes,
+                    XmlDictionaryReaderQuotas.Max
+                );
+
+                rootElm = XElement.Load(jsonReader);
+            }
+            catch (SerializationException ex)
+            {
+                var responseText = Encoding.UTF8.GetString(responseBytes);
+                throw TwitterApiException.CreateFromException(ex, responseText);
+            }
+            catch (XmlException ex)
+            {
+                var responseText = Encoding.UTF8.GetString(responseBytes);
+                throw new TwitterApiException("Invalid JSON", ex) { ResponseText = responseText };
+            }
+
+            ErrorResponse.ThrowIfError(rootElm);
+
+            var tweetIds = rootElm.XPathSelectElements("//content/item/content/tweet/id")
+                .Select(x => x.Value)
+                .ToArray();
+
+            var statuses = new List<TwitterStatus>(tweetIds.Length);
+            foreach (var tweetId in tweetIds)
+            {
+                if (!parsedObjects.GlobalObjects.Tweets.TryGetValue(tweetId, out var tweet))
+                    continue;
+
+                var userId = tweet.UserId;
+                if (!parsedObjects.GlobalObjects.Users.TryGetValue(userId, out var user))
+                    continue;
+
+                tweet.User = user;
+                statuses.Add(tweet);
+            }
+
+            var tweets = TimelineTweet.ExtractTimelineTweets(rootElm);
+            var cursorTop = rootElm.XPathSelectElement("//content/operation/cursor[cursorType[text()='Top']]/value")?.Value;
+            var cursorBottom = rootElm.XPathSelectElement("//content/operation/cursor[cursorType[text()='Bottom']]/value")?.Value;
+
+            return new(statuses.ToArray(), cursorTop, cursorBottom);
+        }
+
+        [DataContract]
+        private record ResponseRoot(
+            [property: DataMember(Name = "globalObjects")]
+            ResponseGlobalObjects GlobalObjects
+        );
+
+        [DataContract]
+        private record ResponseGlobalObjects(
+            [property: DataMember(Name = "users")]
+            Dictionary<string, TwitterUser> Users,
+            [property: DataMember(Name = "tweets")]
+            Dictionary<string, ResponseTweet> Tweets
+        );
+
+        [DataContract]
+        private class ResponseTweet : TwitterStatus
+        {
+            [DataMember(Name = "user_id")]
+            public string UserId { get; set; } = "";
+        }
+
+        public readonly record struct NotificationsResponse(
+            TwitterStatus[] Statuses,
+            string? CursorTop,
+            string? CursorBottom
+        );
+    }
+}