更新履歴
==== Unreleased
+ * NEW: graphqlエンドポイント経由で取得した引用ツイートの表示に対応
==== Ver 3.9.0(2023/12/03)
* NEW: graphqlエンドポイントに対するレートリミットの表示に対応
}
[Fact]
+ public void ToStatus_WithTwitterPostFactory_QuotedTweet_Test()
+ {
+ var rootElm = this.LoadResponseDocument("TimelineTweet_QuotedTweet.json");
+ var timelineTweet = new TimelineTweet(rootElm);
+ var status = timelineTweet.ToTwitterStatus();
+ var postFactory = new TwitterPostFactory(this.CreateTabInfo());
+ var post = postFactory.CreateFromStatus(status, selfUserId: 1L, new HashSet<long>());
+
+ Assert.Equal("1588614645866147840", post.StatusId.Id);
+ var quotedPostId = Assert.Single(post.QuoteStatusIds);
+ Assert.Equal("1583108196868116480", quotedPostId.Id);
+ }
+
+ [Fact]
+ public void ToStatus_WithTwitterPostFactory_QuotedTweet_Tombstone_Test()
+ {
+ var rootElm = this.LoadResponseDocument("TimelineTweet_QuotedTweet_Tombstone.json");
+ var timelineTweet = new TimelineTweet(rootElm);
+ var status = timelineTweet.ToTwitterStatus();
+ var postFactory = new TwitterPostFactory(this.CreateTabInfo());
+ var post = postFactory.CreateFromStatus(status, selfUserId: 1L, new HashSet<long>());
+
+ Assert.Equal("1614653321310253057", post.StatusId.Id);
+ var quotedPostId = Assert.Single(post.QuoteStatusIds);
+ Assert.Equal("1614650279194136576", quotedPostId.Id);
+ }
+
+ [Fact]
public void ToStatus_WithTwitterPostFactory_PromotedTweet_Test()
{
var rootElm = this.LoadResponseDocument("TimelineTweet_PromotedTweet.json");
--- /dev/null
+{
+ "itemType": "TimelineTweet",
+ "__typename": "TimelineTweet",
+ "tweet_results": {
+ "result": {
+ "__typename": "Tweet",
+ "rest_id": "1588614645866147840",
+ "has_birdwatch_notes": false,
+ "core": {
+ "user_results": {
+ "result": {
+ "__typename": "User",
+ "id": "VXNlcjo0MDQ4MDY2NA==",
+ "rest_id": "40480664",
+ "affiliates_highlighted_label": {},
+ "has_graduated_access": false,
+ "is_blue_verified": false,
+ "profile_image_shape": "Circle",
+ "legacy": {
+ "can_dm": true,
+ "can_media_tag": true,
+ "created_at": "Sat May 16 15:20:01 +0000 2009",
+ "default_profile": false,
+ "default_profile_image": false,
+ "description": "",
+ "entities": {
+ "description": {
+ "urls": []
+ },
+ "url": {
+ "urls": [
+ {
+ "display_url": "m.upsilo.net/@upsilon",
+ "expanded_url": "https://m.upsilo.net/@upsilon",
+ "url": "https://t.co/vNMmyHHh15",
+ "indices": [
+ 0,
+ 23
+ ]
+ }
+ ]
+ }
+ },
+ "fast_followers_count": 0,
+ "favourites_count": 215369,
+ "followers_count": 1287,
+ "friends_count": 1,
+ "has_custom_timelines": false,
+ "is_translator": false,
+ "listed_count": 92,
+ "location": "Funabashi, Chiba, Japan",
+ "media_count": 876,
+ "name": "upsilon",
+ "needs_phone_verification": false,
+ "normal_followers_count": 1287,
+ "pinned_tweet_ids_str": [],
+ "possibly_sensitive": false,
+ "profile_banner_url": "https://pbs.twimg.com/profile_banners/40480664/1349188016",
+ "profile_image_url_https": "https://pbs.twimg.com/profile_images/719076434/____normal.png",
+ "profile_interstitial_type": "",
+ "screen_name": "kim_upsilon",
+ "statuses_count": 10081,
+ "translator_type": "regular",
+ "url": "https://t.co/vNMmyHHh15",
+ "verified": false,
+ "want_retweets": false,
+ "withheld_in_countries": []
+ }
+ }
+ }
+ },
+ "unmention_data": {},
+ "unified_card": {
+ "card_fetch_state": "NoCard"
+ },
+ "edit_control": {
+ "edit_tweet_ids": [
+ "1588614645866147840"
+ ],
+ "editable_until_msecs": "1667592021000",
+ "is_edit_eligible": false,
+ "edits_remaining": "5"
+ },
+ "is_translatable": true,
+ "views": {
+ "state": "Enabled"
+ },
+ "source": "<a href=\"https://www.opentween.org/\" rel=\"nofollow\">OpenTween (dev)</a>",
+ "quoted_status_result": {
+ "result": {
+ "__typename": "Tweet",
+ "rest_id": "1583108196868116480",
+ "has_birdwatch_notes": false,
+ "core": {
+ "user_results": {
+ "result": {
+ "__typename": "User",
+ "id": "VXNlcjo0MDQ4MDY2NA==",
+ "rest_id": "40480664",
+ "affiliates_highlighted_label": {},
+ "has_graduated_access": false,
+ "is_blue_verified": false,
+ "profile_image_shape": "Circle",
+ "legacy": {
+ "can_dm": true,
+ "can_media_tag": true,
+ "created_at": "Sat May 16 15:20:01 +0000 2009",
+ "default_profile": false,
+ "default_profile_image": false,
+ "description": "",
+ "entities": {
+ "description": {
+ "urls": []
+ },
+ "url": {
+ "urls": [
+ {
+ "display_url": "m.upsilo.net/@upsilon",
+ "expanded_url": "https://m.upsilo.net/@upsilon",
+ "url": "https://t.co/vNMmyHHh15",
+ "indices": [
+ 0,
+ 23
+ ]
+ }
+ ]
+ }
+ },
+ "fast_followers_count": 0,
+ "favourites_count": 215369,
+ "followers_count": 1287,
+ "friends_count": 1,
+ "has_custom_timelines": false,
+ "is_translator": false,
+ "listed_count": 92,
+ "location": "Funabashi, Chiba, Japan",
+ "media_count": 876,
+ "name": "upsilon",
+ "needs_phone_verification": false,
+ "normal_followers_count": 1287,
+ "pinned_tweet_ids_str": [],
+ "possibly_sensitive": false,
+ "profile_banner_url": "https://pbs.twimg.com/profile_banners/40480664/1349188016",
+ "profile_image_url_https": "https://pbs.twimg.com/profile_images/719076434/____normal.png",
+ "profile_interstitial_type": "",
+ "screen_name": "kim_upsilon",
+ "statuses_count": 10081,
+ "translator_type": "regular",
+ "url": "https://t.co/vNMmyHHh15",
+ "verified": false,
+ "want_retweets": false,
+ "withheld_in_countries": []
+ }
+ }
+ }
+ },
+ "unmention_data": {},
+ "edit_control": {
+ "edit_tweet_ids": [
+ "1583108196868116480"
+ ],
+ "editable_until_msecs": "1666279181000",
+ "is_edit_eligible": true,
+ "edits_remaining": "5"
+ },
+ "is_translatable": true,
+ "views": {
+ "state": "Enabled"
+ },
+ "source": "<a href=\"https://www.opentween.org/\" rel=\"nofollow\">OpenTween (dev)</a>",
+ "legacy": {
+ "bookmark_count": 0,
+ "bookmarked": false,
+ "created_at": "Thu Oct 20 14:49:41 +0000 2022",
+ "conversation_id_str": "1583108196868116480",
+ "display_text_range": [
+ 0,
+ 97
+ ],
+ "entities": {
+ "user_mentions": [],
+ "urls": [],
+ "hashtags": [],
+ "symbols": []
+ },
+ "favorite_count": 2,
+ "favorited": false,
+ "full_text": "AppVeyorでビルドした時と自分の開発環境でビルドした時でなぜか sgen.exe の出力が異なって生成物のハッシュ値が一致しなくなる問題に悩み中。Reproducible Buildむずい",
+ "is_quote_status": false,
+ "lang": "ja",
+ "quote_count": 1,
+ "reply_count": 0,
+ "retweet_count": 0,
+ "retweeted": false,
+ "user_id_str": "40480664",
+ "id_str": "1583108196868116480"
+ }
+ }
+ },
+ "legacy": {
+ "bookmark_count": 0,
+ "bookmarked": false,
+ "created_at": "Fri Nov 04 19:30:21 +0000 2022",
+ "conversation_id_str": "1588614645866147840",
+ "display_text_range": [
+ 0,
+ 63
+ ],
+ "entities": {
+ "user_mentions": [],
+ "urls": [
+ {
+ "display_url": "twitter.com/kim_upsilon/st…",
+ "expanded_url": "https://twitter.com/kim_upsilon/status/1583108196868116480",
+ "url": "https://t.co/mb89Ecojqd",
+ "indices": [
+ 40,
+ 63
+ ]
+ }
+ ],
+ "hashtags": [],
+ "symbols": []
+ },
+ "favorite_count": 2,
+ "favorited": false,
+ "full_text": "これ結局原因が分からないまま sgen.exe を使うのを止めることで解決した https://t.co/mb89Ecojqd",
+ "is_quote_status": true,
+ "lang": "ja",
+ "possibly_sensitive": false,
+ "possibly_sensitive_editable": true,
+ "quote_count": 0,
+ "quoted_status_id_str": "1583108196868116480",
+ "quoted_status_permalink": {
+ "url": "https://t.co/mb89Ecojqd",
+ "expanded": "https://twitter.com/kim_upsilon/status/1583108196868116480",
+ "display": "twitter.com/kim_upsilon/st…"
+ },
+ "reply_count": 1,
+ "retweet_count": 0,
+ "retweeted": false,
+ "user_id_str": "40480664",
+ "id_str": "1588614645866147840"
+ },
+ "quick_promote_eligibility": {
+ "eligibility": "IneligibleNotProfessional"
+ }
+ }
+ },
+ "tweetDisplayType": "SelfThread",
+ "hasModeratedReplies": false
+}
--- /dev/null
+{
+ "itemType": "TimelineTweet",
+ "__typename": "TimelineTweet",
+ "tweet_results": {
+ "result": {
+ "__typename": "Tweet",
+ "rest_id": "1614653321310253057",
+ "core": {
+ "user_results": {
+ "result": {
+ "__typename": "User",
+ "id": "VXNlcjo0MDQ4MDY2NA==",
+ "rest_id": "40480664",
+ "affiliates_highlighted_label": {},
+ "has_graduated_access": false,
+ "is_blue_verified": false,
+ "profile_image_shape": "Circle",
+ "legacy": {
+ "can_dm": false,
+ "can_media_tag": false,
+ "created_at": "Sat May 16 15:20:01 +0000 2009",
+ "default_profile": false,
+ "default_profile_image": false,
+ "description": "",
+ "entities": {
+ "description": {
+ "urls": []
+ },
+ "url": {
+ "urls": [
+ {
+ "display_url": "m.upsilo.net/@upsilon",
+ "expanded_url": "https://m.upsilo.net/@upsilon",
+ "url": "https://t.co/vNMmyHHh15",
+ "indices": [
+ 0,
+ 23
+ ]
+ }
+ ]
+ }
+ },
+ "fast_followers_count": 0,
+ "favourites_count": 215391,
+ "followers_count": 1287,
+ "friends_count": 1,
+ "has_custom_timelines": false,
+ "is_translator": false,
+ "listed_count": 92,
+ "location": "Funabashi, Chiba, Japan",
+ "media_count": 876,
+ "name": "upsilon",
+ "normal_followers_count": 1287,
+ "pinned_tweet_ids_str": [],
+ "possibly_sensitive": false,
+ "profile_banner_url": "https://pbs.twimg.com/profile_banners/40480664/1349188016",
+ "profile_image_url_https": "https://pbs.twimg.com/profile_images/719076434/____normal.png",
+ "profile_interstitial_type": "",
+ "screen_name": "kim_upsilon",
+ "statuses_count": 10081,
+ "translator_type": "regular",
+ "url": "https://t.co/vNMmyHHh15",
+ "verified": false,
+ "want_retweets": false,
+ "withheld_in_countries": []
+ }
+ }
+ }
+ },
+ "unmention_data": {},
+ "unified_card": {
+ "card_fetch_state": "NoCard"
+ },
+ "edit_control": {
+ "edit_tweet_ids": [
+ "1614653321310253057"
+ ],
+ "editable_until_msecs": "1673800125000",
+ "is_edit_eligible": false,
+ "edits_remaining": "5"
+ },
+ "is_translatable": true,
+ "views": {
+ "count": "1779",
+ "state": "EnabledWithCount"
+ },
+ "source": "<a href=\"https://www.opentween.org/\" rel=\"nofollow\">OpenTween (dev)</a>",
+ "quoted_status_result": {
+ "result": {
+ "__typename": "TweetTombstone",
+ "tombstone": {
+ "__typename": "TextTombstone",
+ "text": {
+ "rtl": false,
+ "text": "This Post is from a suspended account. Learn more",
+ "entities": [
+ {
+ "fromIndex": 39,
+ "toIndex": 49,
+ "ref": {
+ "type": "TimelineUrl",
+ "url": "https://help.twitter.com/rules-and-policies/notices-on-twitter",
+ "urlType": "ExternalUrl"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "legacy": {
+ "bookmark_count": 0,
+ "bookmarked": false,
+ "created_at": "Sun Jan 15 15:58:45 +0000 2023",
+ "conversation_id_str": "1614653321310253057",
+ "display_text_range": [
+ 0,
+ 45
+ ],
+ "entities": {
+ "user_mentions": [],
+ "urls": [
+ {
+ "display_url": "twitter.com/omlll/status/1…",
+ "expanded_url": "https://twitter.com/omlll/status/1614650279194136576",
+ "url": "https://t.co/l1XzDghegz",
+ "indices": [
+ 22,
+ 45
+ ]
+ }
+ ],
+ "hashtags": [],
+ "symbols": []
+ },
+ "favorite_count": 9,
+ "favorited": false,
+ "full_text": "これは間違いなくバカが作ったツールですね…\nhttps://t.co/l1XzDghegz",
+ "is_quote_status": true,
+ "lang": "ja",
+ "possibly_sensitive": false,
+ "possibly_sensitive_editable": true,
+ "quote_count": 0,
+ "quoted_status_id_str": "1614650279194136576",
+ "quoted_status_permalink": {
+ "url": "https://t.co/l1XzDghegz",
+ "expanded": "https://twitter.com/omlll/status/1614650279194136576",
+ "display": "twitter.com/omlll/status/1…"
+ },
+ "reply_count": 1,
+ "retweet_count": 2,
+ "retweeted": false,
+ "user_id_str": "40480664",
+ "id_str": "1614653321310253057"
+ }
+ }
+ },
+ "tweetDisplayType": "Tweet"
+}
public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm)
{
- var tweetElm = tweetUnionElm.Element("__typename")?.Value switch
+ var tweetElm = GetTweetTypeName(tweetUnionElm) switch
{
"Tweet" => tweetUnionElm,
"TweetWithVisibilityResults" => tweetUnionElm.Element("tweet") ?? throw CreateParseError(),
return TimelineTweet.ParseTweet(tweetElm);
}
+ public static string GetTweetTypeName(XElement tweetUnionElm)
+ => tweetUnionElm.Element("__typename")?.Value ?? throw CreateParseError();
+
public static TwitterStatus ParseTweet(XElement tweetElm)
{
var tweetLegacyElm = tweetElm.Element("legacy") ?? throw CreateParseError();
var userElm = tweetElm.Element("core")?.Element("user_results")?.Element("result") ?? throw CreateParseError();
var retweetedTweetElm = tweetLegacyElm.Element("retweeted_status_result")?.Element("result");
var user = new TwitterGraphqlUser(userElm);
+ var quotedTweetElm = tweetElm.Element("quoted_status_result")?.Element("result") ?? null;
+ var quotedStatusPermalink = tweetLegacyElm.Element("quoted_status_permalink") ?? null;
+ var isQuotedTweetTombstone = quotedTweetElm != null && GetTweetTypeName(quotedTweetElm) == "TweetTombstone";
static string GetText(XElement elm, string name)
=> elm.Element(name)?.Value ?? throw CreateParseError();
},
User = user.ToTwitterUser(),
RetweetedStatus = retweetedTweetElm != null ? TimelineTweet.ParseTweetUnion(retweetedTweetElm) : null,
+ IsQuoteStatus = GetTextOrNull(tweetLegacyElm, "is_quote_status") == "true",
+ QuotedStatus = quotedTweetElm != null && !isQuotedTweetTombstone ? TimelineTweet.ParseTweetUnion(quotedTweetElm) : null,
+ QuotedStatusIdStr = GetTextOrNull(tweetLegacyElm, "quoted_status_id_str"),
+ QuotedStatusPermalink = quotedStatusPermalink == null ? null : new()
+ {
+ Url = GetText(quotedStatusPermalink, "url"),
+ Expanded = GetText(quotedStatusPermalink, "expanded"),
+ Display = GetText(quotedStatusPermalink, "display"),
+ },
};
}