1 // OpenTween - Client of Twitter
2 // Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
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)
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
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.
25 using System.Collections.Generic;
26 using System.Diagnostics.CodeAnalysis;
29 using System.Threading.Tasks;
30 using System.Xml.Linq;
31 using System.Xml.XPath;
32 using OpenTween.Api.DataModel;
34 namespace OpenTween.Api.GraphQL
36 public class TimelineTweet
38 public const string TypeName = nameof(TimelineTweet);
40 public XElement Element { get; }
42 public bool IsAvailable
43 => this.resultElm != null && !this.IsTombstoneResult(this.resultElm);
45 private readonly XElement? resultElm;
47 public TimelineTweet(XElement element)
49 var typeName = element.Element("itemType")?.Value;
50 if (typeName != TypeName)
51 throw new ArgumentException($"Invalid itemType: {typeName}", nameof(element));
53 this.Element = element;
54 this.resultElm = this.TryGetResultElm();
57 private XElement? TryGetResultElm()
58 => this.Element.XPathSelectElement("tweet_results/result");
60 private bool IsTombstoneResult([NotNullWhen(true)]XElement? resultElm)
61 => resultElm?.Element("__typename")?.Value == "TweetTombstone";
63 public TwitterStatus ToTwitterStatus()
65 this.ThrowIfTweetIsNotAvailable();
69 var resultElm = this.resultElm ?? throw CreateParseError();
70 var status = TimelineTweet.ParseTweetUnion(resultElm);
72 if (this.Element.Element("promotedMetadata") != null)
73 status.IsPromoted = true;
77 catch (WebApiException ex)
79 ex.ResponseText = JsonUtils.JsonXmlToString(this.Element);
80 MyCommon.TraceOut(ex);
85 public void ThrowIfTweetIsNotAvailable()
90 string? tombstoneText = null;
91 if (this.IsTombstoneResult(this.resultElm))
92 tombstoneText = this.resultElm.XPathSelectElement("tombstone/text/text")?.Value;
94 var message = tombstoneText ?? "Tweet is not available";
95 var json = JsonUtils.JsonXmlToString(this.Element);
97 throw new WebApiException(message, json);
100 public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm)
102 var tweetElm = GetTweetTypeName(tweetUnionElm) switch
104 "Tweet" => tweetUnionElm,
105 "TweetWithVisibilityResults" => tweetUnionElm.Element("tweet") ?? throw CreateParseError(),
106 _ => throw CreateParseError(),
109 return TimelineTweet.ParseTweet(tweetElm);
112 public static string GetTweetTypeName(XElement tweetUnionElm)
113 => tweetUnionElm.Element("__typename")?.Value ?? throw CreateParseError();
115 public static TwitterStatus ParseTweet(XElement tweetElm)
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";
125 static string GetText(XElement elm, string name)
126 => elm.Element(name)?.Value ?? throw CreateParseError();
128 static string? GetTextOrNull(XElement elm, string name)
129 => elm.Element(name)?.Value;
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,
143 UserMentions = tweetLegacyElm.XPathSelectElements("entities/user_mentions/item")
144 .Select(x => new TwitterEntityMention()
146 Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
147 ScreenName = GetText(x, "screen_name"),
150 Urls = tweetLegacyElm.XPathSelectElements("entities/urls/item")
151 .Select(x => new TwitterEntityUrl()
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"),
159 Hashtags = tweetLegacyElm.XPathSelectElements("entities/hashtags/item")
160 .Select(x => new TwitterEntityHashtag()
162 Indices = x.XPathSelectElements("indices/item").Select(x => int.Parse(x.Value)).ToArray(),
163 Text = GetText(x, "text"),
167 ExtendedEntities = new()
169 Media = tweetLegacyElm.XPathSelectElements("extended_entities/media/item")
170 .Select(x => new TwitterEntityMedia()
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"),
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()
189 Url = GetText(quotedStatusPermalink, "url"),
190 Expanded = GetText(quotedStatusPermalink, "expanded"),
191 Display = GetText(quotedStatusPermalink, "display"),
196 private static Exception CreateParseError()
197 => throw new WebApiException("Parse error on TimelineTweet");
199 public static TimelineTweet[] ExtractTimelineTweets(XElement element)
201 return element.XPathSelectElements($"//itemContent[itemType[text()='{TypeName}']][tweetDisplayType[text()='Tweet' or text()='SelfThread']]")
202 .Select(x => new TimelineTweet(x))