OSDN Git Service

f8bb804d38c56132fd50aa89d143e87dd5f73e58
[opentween/open-tween.git] / OpenTween / Api / GraphQL / TimelineTweet.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2023 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.Text;
28 using System.Threading.Tasks;
29 using System.Xml.Linq;
30 using System.Xml.XPath;
31 using OpenTween.Api.DataModel;
32
33 namespace OpenTween.Api.GraphQL
34 {
35     public class TimelineTweet
36     {
37         public const string TypeName = nameof(TimelineTweet);
38
39         public XElement Element { get; }
40
41         public TimelineTweet(XElement element)
42         {
43             var typeName = element.Element("itemType")?.Value;
44             if (typeName != TypeName)
45                 throw new ArgumentException($"Invalid itemType: {typeName}", nameof(element));
46
47             this.Element = element;
48         }
49
50         public TwitterStatus ToTwitterStatus()
51         {
52             try
53             {
54                 var resultElm = this.Element.Element("tweet_results")?.Element("result") ?? throw CreateParseError();
55                 return this.ParseTweetUnion(resultElm);
56             }
57             catch (WebApiException ex)
58             {
59                 ex.ResponseText = JsonUtils.JsonXmlToString(this.Element);
60                 MyCommon.TraceOut(ex);
61                 throw;
62             }
63         }
64
65         private TwitterStatus ParseTweetUnion(XElement tweetUnionElm)
66         {
67             var tweetElm = tweetUnionElm.Element("__typename")?.Value switch
68             {
69                 "Tweet" => tweetUnionElm,
70                 "TweetWithVisibilityResults" => tweetUnionElm.Element("tweet") ?? throw CreateParseError(),
71                 _ => throw CreateParseError(),
72             };
73
74             return this.ParseTweet(tweetElm);
75         }
76
77         private TwitterStatus ParseTweet(XElement tweetElm)
78         {
79             var tweetLegacyElm = tweetElm.Element("legacy") ?? throw CreateParseError();
80             var userElm = tweetElm.Element("core")?.Element("user_results")?.Element("result") ?? throw CreateParseError();
81             var userLegacyElm = userElm.Element("legacy") ?? throw CreateParseError();
82             var retweetedTweetElm = tweetLegacyElm.Element("retweeted_status_result")?.Element("result");
83
84             static string GetText(XElement elm, string name)
85                 => elm.Element(name)?.Value ?? throw CreateParseError();
86
87             static string? GetTextOrNull(XElement elm, string name)
88                 => elm.Element(name)?.Value;
89
90             return new()
91             {
92                 IdStr = GetText(tweetElm, "rest_id"),
93                 Source = GetText(tweetElm, "source"),
94                 CreatedAt = GetText(tweetLegacyElm, "created_at"),
95                 FullText = GetText(tweetLegacyElm, "full_text"),
96                 InReplyToScreenName = GetTextOrNull(tweetLegacyElm, "in_reply_to_screen_name"),
97                 InReplyToStatusIdStr = GetTextOrNull(tweetLegacyElm, "in_reply_to_status_id_str"),
98                 InReplyToUserId = GetTextOrNull(tweetLegacyElm, "in_reply_to_user_id_str") is string userId ? long.Parse(userId) : null,
99                 Favorited = GetTextOrNull(tweetLegacyElm, "favorited") is string favorited ? favorited == "true" : null,
100                 Entities = new()
101                 {
102                     UserMentions = tweetLegacyElm.XPathSelectElements("entities/user_mentions/item")
103                         .Select(x => new TwitterEntityMention()
104                         {
105                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
106                             ScreenName = GetText(x, "screen_name"),
107                         })
108                         .ToArray(),
109                     Urls = tweetLegacyElm.XPathSelectElements("entities/urls/item")
110                         .Select(x => new TwitterEntityUrl()
111                         {
112                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
113                             DisplayUrl = GetText(x, "display_url"),
114                             ExpandedUrl = GetText(x, "expanded_url"),
115                             Url = GetText(x, "url"),
116                         })
117                         .ToArray(),
118                     Hashtags = tweetLegacyElm.XPathSelectElements("entities/hashtags/item")
119                         .Select(x => new TwitterEntityHashtag()
120                         {
121                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
122                             Text = GetText(x, "text"),
123                         })
124                         .ToArray(),
125                 },
126                 ExtendedEntities = new()
127                 {
128                     Media = tweetLegacyElm.XPathSelectElements("extended_entities/media/item")
129                         .Select(x => new TwitterEntityMedia()
130                         {
131                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
132                             DisplayUrl = GetText(x, "display_url"),
133                             ExpandedUrl = GetText(x, "expanded_url"),
134                             Url = GetText(x, "url"),
135                             MediaUrlHttps = GetText(x, "media_url_https"),
136                             Type = GetText(x, "type"),
137                             AltText = GetTextOrNull(x, "ext_alt_text"),
138                         })
139                         .ToArray(),
140                 },
141                 User = new()
142                 {
143                     Id = long.Parse(GetText(userElm, "rest_id")),
144                     IdStr = GetText(userElm, "rest_id"),
145                     Name = GetText(userLegacyElm, "name"),
146                     ProfileImageUrlHttps = GetText(userLegacyElm, "profile_image_url_https"),
147                     ScreenName = GetText(userLegacyElm, "screen_name"),
148                     Protected = GetTextOrNull(userLegacyElm, "protected") == "true",
149                 },
150                 RetweetedStatus = retweetedTweetElm != null ? this.ParseTweetUnion(retweetedTweetElm) : null,
151             };
152         }
153
154         private static Exception CreateParseError()
155             => throw new WebApiException("Parse error on TimelineTweet");
156
157         public static TimelineTweet[] ExtractTimelineTweets(XElement element)
158         {
159             return element.XPathSelectElements($"//itemContent[itemType[text()='{TypeName}']][tweetDisplayType[text()='Tweet']]")
160                 .Select(x => new TimelineTweet(x))
161                 .ToArray();
162         }
163     }
164 }