OSDN Git Service

TimelineTweetに含まれる引用ツイートの取得に対応
authorKimura Youichi <kim.upsilon@bucyou.net>
Mon, 11 Dec 2023 15:27:29 +0000 (00:27 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Mon, 11 Dec 2023 15:29:28 +0000 (00:29 +0900)
CHANGELOG.txt
OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs
OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json [new file with mode: 0644]
OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json [new file with mode: 0644]
OpenTween/Api/GraphQL/TimelineTweet.cs

index 1b712a0..826fc3c 100644 (file)
@@ -1,6 +1,7 @@
 更新履歴
 
 ==== Unreleased
+ * NEW: graphqlエンドポイント経由で取得した引用ツイートの表示に対応
 
 ==== Ver 3.9.0(2023/12/03)
  * NEW: graphqlエンドポイントに対するレートリミットの表示に対応
index 6747ad1..f195832 100644 (file)
@@ -140,6 +140,34 @@ namespace OpenTween.Api.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");
diff --git a/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json
new file mode 100644 (file)
index 0000000..67e4fe3
--- /dev/null
@@ -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": "<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
+}
diff --git a/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json
new file mode 100644 (file)
index 0000000..d1de579
--- /dev/null
@@ -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": "<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"
+}
index dd20e34..af7b176 100644 (file)
@@ -92,7 +92,7 @@ namespace OpenTween.Api.GraphQL
 
         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 @@ namespace OpenTween.Api.GraphQL
             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 @@ namespace OpenTween.Api.GraphQL
                 },
                 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"),
+                },
             };
         }