Skip to content

Commit

Permalink
graphqlエンドポイントを使用したユーザータイムラインの取得に対応
Browse files Browse the repository at this point in the history
  • Loading branch information
upsilon committed Nov 24, 2023
1 parent a477e62 commit 3b20d4c
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
==== Unreleased
* NEW: graphqlエンドポイントを使用した検索タイムラインの取得に対応
* NEW: graphqlエンドポイントを使用したプロフィール情報の取得に対応
* NEW: graphqlエンドポイントを使用したユーザータイムラインの取得に対応
* CHG: タイムライン更新が停止する不具合が報告される件への暫定的な対処
- タイムライン更新に30秒以上掛かっている場合は完了を待機せず次のタイマーを開始させる
- タイムライン更新の次回実行が1時間以上先になる場合は異常値としてタイマーをリセットする
Expand Down
96 changes: 96 additions & 0 deletions OpenTween.Tests/Api/GraphQL/UserTweetsRequestTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// OpenTween - Client of Twitter
// Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moq;
using OpenTween.Connection;
using Xunit;

namespace OpenTween.Api.GraphQL
{
public class UserTweetsRequestTest
{
[Fact]
public async Task Send_Test()
{
using var responseStream = File.OpenRead("Resources/Responses/UserTweets_SimpleTweet.json");

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/2GIWTr7XwadIixZDtyXd4A/UserTweets"), url);
Assert.Equal(3, param.Count);
Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withQuickPromoteEligibilityTweetFields":true,"withVoice":true,"withV2Timeline":true}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.True(param.ContainsKey("fieldToggles"));
})
.ReturnsAsync(responseStream);

var request = new UserTweetsRequest(userId: "40480664")
{
Count = 20,
};

var response = await request.Send(mock.Object).ConfigureAwait(false);
Assert.Single(response.Tweets);
Assert.Equal("DAABCgABF_tTnZu__-0KAAIWZa6KTRoAAwgAAwAAAAIAAA", response.CursorBottom);

mock.VerifyAll();
}

[Fact]
public async Task Send_RequestCursor_Test()
{
using var responseStream = File.OpenRead("Resources/Responses/UserTweets_SimpleTweet.json");

var mock = new Mock<IApiConnection>();
mock.Setup(x =>
x.GetStreamAsync(It.IsAny<Uri>(), It.IsAny<IDictionary<string, string>>())
)
.Callback<Uri, IDictionary<string, string>>((url, param) =>
{
Assert.Equal(new("https://twitter.com/i/api/graphql/2GIWTr7XwadIixZDtyXd4A/UserTweets"), url);
Assert.Equal(3, param.Count);
Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withQuickPromoteEligibilityTweetFields":true,"withVoice":true,"withV2Timeline":true,"cursor":"aaa"}""", param["variables"]);
Assert.True(param.ContainsKey("features"));
Assert.True(param.ContainsKey("fieldToggles"));
})
.ReturnsAsync(responseStream);

var request = new UserTweetsRequest(userId: "40480664")
{
Count = 20,
Cursor = "aaa",
};

await request.Send(mock.Object).ConfigureAwait(false);
mock.VerifyAll();
}
}
}
3 changes: 3 additions & 0 deletions OpenTween.Tests/OpenTween.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,8 @@
<None Update="Resources\Responses\UserByScreenName.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Responses\UserTweets_SimpleTweet.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
185 changes: 185 additions & 0 deletions OpenTween.Tests/Resources/Responses/UserTweets_SimpleTweet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
{
"data": {
"user": {
"result": {
"__typename": "User",
"timeline_v2": {
"timeline": {
"instructions": [
{
"type": "TimelineClearCache"
},
{
"type": "TimelineAddEntries",
"entries": [
{
"entryId": "tweet-1612926719035600906",
"sortIndex": "1728066818414739426",
"content": {
"entryType": "TimelineTimelineItem",
"__typename": "TimelineTimelineItem",
"itemContent": {
"itemType": "TimelineTweet",
"__typename": "TimelineTweet",
"tweet_results": {
"result": {
"__typename": "Tweet",
"rest_id": "1612926719035600906",
"core": {
"user_results": {
"result": {
"__typename": "User",
"id": "VXNlcjo0MDQ4MDY2NA==",
"rest_id": "40480664",
"affiliates_highlighted_label": {},
"has_graduated_access": true,
"is_blue_verified": false,
"profile_image_shape": "Circle",
"legacy": {
"following": true,
"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": "OpenTween Project 言い出しっぺ",
"entities": {
"description": {
"urls": []
},
"url": {
"urls": [
{
"display_url": "m.upsilo.net/@upsilon",
"expanded_url": "https://m.upsilo.net/@upsilon",
"url": "https://t.co/vNMmyHHOQD",
"indices": [
0,
23
]
}
]
}
},
"fast_followers_count": 0,
"favourites_count": 215409,
"followers_count": 1288,
"friends_count": 2,
"has_custom_timelines": false,
"is_translator": false,
"listed_count": 92,
"location": "Funabashi, Chiba, Japan",
"media_count": 876,
"name": "upsilon",
"normal_followers_count": 1288,
"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/vNMmyHHOQD",
"verified": false,
"want_retweets": true,
"withheld_in_countries": []
}
}
}
},
"unmention_data": {},
"edit_control": {
"edit_tweet_ids": [
"1612926719035600906"
],
"editable_until_msecs": "1673388471000",
"is_edit_eligible": true,
"edits_remaining": "5"
},
"is_translatable": true,
"views": {
"count": "288",
"state": "EnabledWithCount"
},
"source": "<a href=\"https://www.opentween.org/\" rel=\"nofollow\">OpenTween (dev)</a>",
"legacy": {
"bookmark_count": 0,
"bookmarked": false,
"created_at": "Tue Jan 10 21:37:51 +0000 2023",
"conversation_id_str": "1612926719035600906",
"display_text_range": [
0,
5
],
"entities": {
"user_mentions": [],
"urls": [],
"hashtags": [],
"symbols": []
},
"favorite_count": 2,
"favorited": false,
"full_text": "よろしくね",
"is_quote_status": false,
"lang": "ja",
"quote_count": 0,
"reply_count": 0,
"retweet_count": 0,
"retweeted": false,
"user_id_str": "40480664",
"id_str": "1612926719035600906"
},
"quick_promote_eligibility": {
"eligibility": "IneligibleNotProfessional"
}
}
},
"tweetDisplayType": "Tweet"
},
"clientEventInfo": {
"component": "tweet",
"element": "tweet",
"details": {
"timelinesDetails": {
"injectionType": "RankedOrganicTweet",
"controllerData": "DAACDAABDAABCgABAAAAAAAAAAAKAAkAAAAAZE0+pwAAAAA="
}
}
}
}
},
{
"entryId": "cursor-top-1728066818414739457",
"sortIndex": "1728066818414739457",
"content": {
"entryType": "TimelineTimelineCursor",
"__typename": "TimelineTimelineCursor",
"value": "DAABCgABF_tTnZvAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA",
"cursorType": "Top"
}
},
{
"entryId": "cursor-bottom-1728066818414739439",
"sortIndex": "1728066818414739439",
"content": {
"entryType": "TimelineTimelineCursor",
"__typename": "TimelineTimelineCursor",
"value": "DAABCgABF_tTnZu__-0KAAIWZa6KTRoAAwgAAwAAAAIAAA",
"cursorType": "Bottom"
}
}
]
}
],
"metadata": {
"scribeConfig": {
"page": "profileBest"
}
}
}
}
}
}
}
}
Loading

0 comments on commit 3b20d4c

Please sign in to comment.