From e016aeaeb2e73ecdd057df529657edb386c89826 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 11 Dec 2023 23:53:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?OpenTween.Tests/Resources=E4=BB=A5=E4=B8=8B?= =?UTF-8?q?=E3=81=AB=E3=81=82=E3=82=8B=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AECopyToOutputDirectory=E3=82=92=E4=B8=80=E6=8B=AC?= =?UTF-8?q?=E3=81=A7=E8=A8=AD=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/OpenTween.Tests.csproj | 74 +------------------------- 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index dbaad23f1..6d5f37fd7 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -44,79 +44,7 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest From cda0532adc31dc7d6032522fefad86065b3f5eac Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Tue, 12 Dec 2023 00:27:29 +0900 Subject: [PATCH 2/2] =?UTF-8?q?TimelineTweet=E3=81=AB=E5=90=AB=E3=81=BE?= =?UTF-8?q?=E3=82=8C=E3=82=8B=E5=BC=95=E7=94=A8=E3=83=84=E3=82=A4=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E5=8F=96=E5=BE=97=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.txt | 1 + .../Api/GraphQL/TimelineTweetTest.cs | 28 ++ .../Responses/TimelineTweet_QuotedTweet.json | 252 ++++++++++++++++++ .../TimelineTweet_QuotedTweet_Tombstone.json | 159 +++++++++++ OpenTween/Api/GraphQL/TimelineTweet.cs | 17 +- 5 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json create mode 100644 OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1b712a082..826fc3c39 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,7 @@ 更新履歴 ==== Unreleased + * NEW: graphqlエンドポイント経由で取得した引用ツイートの表示に対応 ==== Ver 3.9.0(2023/12/03) * NEW: graphqlエンドポイントに対するレートリミットの表示に対応 diff --git a/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs b/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs index 6747ad158..f1958321e 100644 --- a/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs +++ b/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs @@ -139,6 +139,34 @@ public void ToStatus_WithTwitterPostFactory_SelfThread_Test() Assert.Equal(40480664L, post.UserId); } + [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()); + + 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()); + + Assert.Equal("1614653321310253057", post.StatusId.Id); + var quotedPostId = Assert.Single(post.QuoteStatusIds); + Assert.Equal("1614650279194136576", quotedPostId.Id); + } + [Fact] public void ToStatus_WithTwitterPostFactory_PromotedTweet_Test() { diff --git a/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json new file mode 100644 index 000000000..67e4fe360 --- /dev/null +++ b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json @@ -0,0 +1,252 @@ +{ + "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": "OpenTween (dev)", + "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": "OpenTween (dev)", + "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 +} diff --git a/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json new file mode 100644 index 000000000..d1de5794e --- /dev/null +++ b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json @@ -0,0 +1,159 @@ +{ + "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": "OpenTween (dev)", + "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" +} diff --git a/OpenTween/Api/GraphQL/TimelineTweet.cs b/OpenTween/Api/GraphQL/TimelineTweet.cs index dd20e3401..af7b1764d 100644 --- a/OpenTween/Api/GraphQL/TimelineTweet.cs +++ b/OpenTween/Api/GraphQL/TimelineTweet.cs @@ -92,7 +92,7 @@ public void ThrowIfTweetIsTombstone() 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(), @@ -102,12 +102,18 @@ public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm) 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(); @@ -168,6 +174,15 @@ static string GetText(XElement elm, string name) }, 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"), + }, }; }