OSDN Git Service

FavoriteTweet/UnfavoriteTweetを使用したFav追加・削除に対応
[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.Diagnostics.CodeAnalysis;
27 using System.Linq;
28 using System.Text;
29 using System.Threading.Tasks;
30 using System.Xml.Linq;
31 using System.Xml.XPath;
32 using OpenTween.Api.DataModel;
33
34 namespace OpenTween.Api.GraphQL
35 {
36     public class TimelineTweet
37     {
38         public const string TypeName = nameof(TimelineTweet);
39
40         public XElement Element { get; }
41
42         public bool IsAvailable
43             => this.resultElm != null && !this.IsTombstoneResult(this.resultElm);
44
45         private readonly XElement? resultElm;
46
47         public TimelineTweet(XElement element)
48         {
49             var typeName = element.Element("itemType")?.Value;
50             if (typeName != TypeName)
51                 throw new ArgumentException($"Invalid itemType: {typeName}", nameof(element));
52
53             this.Element = element;
54             this.resultElm = this.TryGetResultElm();
55         }
56
57         private XElement? TryGetResultElm()
58             => this.Element.XPathSelectElement("tweet_results/result");
59
60         private bool IsTombstoneResult([NotNullWhen(true)]XElement? resultElm)
61             => resultElm?.Element("__typename")?.Value == "TweetTombstone";
62
63         public TwitterStatus ToTwitterStatus()
64         {
65             this.ThrowIfTweetIsNotAvailable();
66
67             try
68             {
69                 var resultElm = this.resultElm ?? throw CreateParseError();
70                 var status = TimelineTweet.ParseTweetUnion(resultElm);
71
72                 if (this.Element.Element("promotedMetadata") != null)
73                     status.IsPromoted = true;
74
75                 return status;
76             }
77             catch (WebApiException ex)
78             {
79                 ex.ResponseText = JsonUtils.JsonXmlToString(this.Element);
80                 MyCommon.TraceOut(ex);
81                 throw;
82             }
83         }
84
85         public void ThrowIfTweetIsNotAvailable()
86         {
87             if (this.IsAvailable)
88                 return;
89
90             string? tombstoneText = null;
91             if (this.IsTombstoneResult(this.resultElm))
92                 tombstoneText = this.resultElm.XPathSelectElement("tombstone/text/text")?.Value;
93
94             var message = tombstoneText ?? "Tweet is not available";
95             var json = JsonUtils.JsonXmlToString(this.Element);
96
97             throw new WebApiException(message, json);
98         }
99
100         public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm)
101         {
102             var tweetElm = GetTweetTypeName(tweetUnionElm) switch
103             {
104                 "Tweet" => tweetUnionElm,
105                 "TweetWithVisibilityResults" => tweetUnionElm.Element("tweet") ?? throw CreateParseError(),
106                 _ => throw CreateParseError(),
107             };
108
109             return TimelineTweet.ParseTweet(tweetElm);
110         }
111
112         public static string GetTweetTypeName(XElement tweetUnionElm)
113             => tweetUnionElm.Element("__typename")?.Value ?? throw CreateParseError();
114
115         public static TwitterStatus ParseTweet(XElement tweetElm)
116         {
117             var tweetLegacyElm = tweetElm.Element("legacy") ?? throw CreateParseError();
118             var userElm = tweetElm.Element("core")?.Element("user_results")?.Element("result") ?? throw CreateParseError();
119             var retweetedTweetElm = tweetLegacyElm.Element("retweeted_status_result")?.Element("result");
120             var user = new TwitterGraphqlUser(userElm);
121             var quotedTweetElm = tweetElm.Element("quoted_status_result")?.Element("result") ?? null;
122             var quotedStatusPermalink = tweetLegacyElm.Element("quoted_status_permalink") ?? null;
123             var isQuotedTweetTombstone = quotedTweetElm != null && GetTweetTypeName(quotedTweetElm) == "TweetTombstone";
124
125             static string GetText(XElement elm, string name)
126                 => elm.Element(name)?.Value ?? throw CreateParseError();
127
128             static string? GetTextOrNull(XElement elm, string name)
129                 => elm.Element(name)?.Value;
130
131             return new()
132             {
133                 IdStr = GetText(tweetElm, "rest_id"),
134                 Source = GetText(tweetElm, "source"),
135                 CreatedAt = GetText(tweetLegacyElm, "created_at"),
136                 FullText = GetText(tweetLegacyElm, "full_text"),
137                 InReplyToScreenName = GetTextOrNull(tweetLegacyElm, "in_reply_to_screen_name"),
138                 InReplyToStatusIdStr = GetTextOrNull(tweetLegacyElm, "in_reply_to_status_id_str"),
139                 InReplyToUserId = GetTextOrNull(tweetLegacyElm, "in_reply_to_user_id_str") is string userId ? long.Parse(userId) : null,
140                 Favorited = GetTextOrNull(tweetLegacyElm, "favorited") is string favorited ? favorited == "true" : null,
141                 Entities = new()
142                 {
143                     UserMentions = tweetLegacyElm.XPathSelectElements("entities/user_mentions/item")
144                         .Select(x => new TwitterEntityMention()
145                         {
146                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
147                             ScreenName = GetText(x, "screen_name"),
148                         })
149                         .ToArray(),
150                     Urls = tweetLegacyElm.XPathSelectElements("entities/urls/item")
151                         .Select(x => new TwitterEntityUrl()
152                         {
153                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
154                             DisplayUrl = GetTextOrNull(x, "display_url"),
155                             ExpandedUrl = GetTextOrNull(x, "expanded_url"),
156                             Url = GetText(x, "url"),
157                         })
158                         .ToArray(),
159                     Hashtags = tweetLegacyElm.XPathSelectElements("entities/hashtags/item")
160                         .Select(x => new TwitterEntityHashtag()
161                         {
162                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
163                             Text = GetText(x, "text"),
164                         })
165                         .ToArray(),
166                 },
167                 ExtendedEntities = new()
168                 {
169                     Media = tweetLegacyElm.XPathSelectElements("extended_entities/media/item")
170                         .Select(x => new TwitterEntityMedia()
171                         {
172                             Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
173                             DisplayUrl = GetText(x, "display_url"),
174                             ExpandedUrl = GetText(x, "expanded_url"),
175                             Url = GetText(x, "url"),
176                             MediaUrlHttps = GetText(x, "media_url_https"),
177                             Type = GetText(x, "type"),
178                             AltText = GetTextOrNull(x, "ext_alt_text"),
179                         })
180                         .ToArray(),
181                 },
182                 User = user.ToTwitterUser(),
183                 RetweetedStatus = retweetedTweetElm != null ? TimelineTweet.ParseTweetUnion(retweetedTweetElm) : null,
184                 IsQuoteStatus = GetTextOrNull(tweetLegacyElm, "is_quote_status") == "true",
185                 QuotedStatus = quotedTweetElm != null && !isQuotedTweetTombstone ? TimelineTweet.ParseTweetUnion(quotedTweetElm) : null,
186                 QuotedStatusIdStr = GetTextOrNull(tweetLegacyElm, "quoted_status_id_str"),
187                 QuotedStatusPermalink = quotedStatusPermalink == null ? null : new()
188                 {
189                     Url = GetText(quotedStatusPermalink, "url"),
190                     Expanded = GetText(quotedStatusPermalink, "expanded"),
191                     Display = GetText(quotedStatusPermalink, "display"),
192                 },
193             };
194         }
195
196         private static Exception CreateParseError()
197             => throw new WebApiException("Parse error on TimelineTweet");
198
199         public static TimelineTweet[] ExtractTimelineTweets(XElement element)
200         {
201             return element.XPathSelectElements($"//itemContent[itemType[text()='{TypeName}']][tweetDisplayType[text()='Tweet' or text()='SelfThread']]")
202                 .Select(x => new TimelineTweet(x))
203                 .ToArray();
204         }
205     }
206 }