OSDN Git Service

FavoriteTweet/UnfavoriteTweetを使用したFav追加・削除に対応
authorKimura Youichi <kim.upsilon@bucyou.net>
Wed, 24 Jan 2024 05:21:01 +0000 (14:21 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Wed, 24 Jan 2024 06:24:03 +0000 (15:24 +0900)
CHANGELOG.txt
OpenTween.Tests/Api/GraphQL/FavoriteTweetRequestTest.cs [new file with mode: 0644]
OpenTween.Tests/Api/GraphQL/UnfavoriteTweetRequestTest.cs [new file with mode: 0644]
OpenTween.Tests/Resources/Responses/FavoriteTweet.json [new file with mode: 0644]
OpenTween.Tests/Resources/Responses/FavoriteTweet_AlreadyFavorited.json [new file with mode: 0644]
OpenTween.Tests/Resources/Responses/UnfavoriteTweet.json [new file with mode: 0644]
OpenTween/Api/GraphQL/FavoriteTweetRequest.cs [new file with mode: 0644]
OpenTween/Api/GraphQL/UnfavoriteTweetRequest.cs [new file with mode: 0644]
OpenTween/Tween.cs
OpenTween/Twitter.cs

index 9c2b682..6dafab1 100644 (file)
@@ -2,6 +2,7 @@
 
 ==== Unreleased
  * NEW: Cookie使用時のReplyタブの更新に対応(/statuses/mentions_timeline.json 廃止に伴う対応)
+ * NEW: Cookie使用時のFav追加・削除に対応
  * FIX: Cookie使用時にツイート検索の言語指定が効かない不具合を修正
  * FIX: ツイート検索のキーワードを後から変更すると検索結果が表示されない不具合を修正
  * FIX: Cookie使用時にステータスバーにRecentタブのレートリミットが表示されない不具合を修正
diff --git a/OpenTween.Tests/Api/GraphQL/FavoriteTweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/FavoriteTweetRequestTest.cs
new file mode 100644 (file)
index 0000000..cef9242
--- /dev/null
@@ -0,0 +1,78 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 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.Threading.Tasks;
+using Moq;
+using OpenTween.Connection;
+using Xunit;
+
+namespace OpenTween.Api.GraphQL
+{
+    public class FavoriteTweetRequestTest
+    {
+        [Fact]
+        public async Task Send_Test()
+        {
+            using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/FavoriteTweet.json");
+
+            var mock = new Mock<IApiConnection>();
+            mock.Setup(x =>
+                    x.SendAsync(It.IsAny<IHttpRequest>())
+                )
+                .Callback<IHttpRequest>(x =>
+                {
+                    var request = Assert.IsType<PostJsonRequest>(x);
+                    Assert.Equal(new("https://twitter.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet"), request.RequestUri);
+                    Assert.Equal("""{"variables":{"tweet_id":"12345"},"queryId":"lI07N6Otwv1PhnEgXILM7A"}""", request.JsonString);
+                })
+                .ReturnsAsync(apiResponse);
+
+            var request = new FavoriteTweetRequest
+            {
+                TweetId = new("12345"),
+            };
+
+            await request.Send(mock.Object);
+
+            mock.VerifyAll();
+        }
+
+        [Fact]
+        public async Task Send_AlreadyFavoritedTest()
+        {
+            using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/FavoriteTweet_AlreadyFavorited.json");
+
+            var mock = new Mock<IApiConnection>();
+            mock.Setup(x => x.SendAsync(It.IsAny<IHttpRequest>()))
+                .ReturnsAsync(apiResponse);
+
+            var request = new FavoriteTweetRequest
+            {
+                TweetId = new("12345"),
+            };
+
+            // 重複によるエラーレスポンスが返っているが例外を発生させない
+            await request.Send(mock.Object);
+
+            mock.VerifyAll();
+        }
+    }
+}
diff --git a/OpenTween.Tests/Api/GraphQL/UnfavoriteTweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/UnfavoriteTweetRequestTest.cs
new file mode 100644 (file)
index 0000000..d1d75f2
--- /dev/null
@@ -0,0 +1,58 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 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.Threading.Tasks;
+using Moq;
+using OpenTween.Connection;
+using Xunit;
+
+namespace OpenTween.Api.GraphQL
+{
+    public class UnfavoriteTweetRequestTest
+    {
+        [Fact]
+        public async Task Send_Test()
+        {
+            using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UnfavoriteTweet.json");
+
+            var mock = new Mock<IApiConnection>();
+            mock.Setup(x =>
+                    x.SendAsync(It.IsAny<IHttpRequest>())
+                )
+                .Callback<IHttpRequest>(x =>
+                {
+                    var request = Assert.IsType<PostJsonRequest>(x);
+                    Assert.Equal(new("https://twitter.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet"), request.RequestUri);
+                    Assert.Equal("""{"variables":{"tweet_id":"12345"},"queryId":"ZYKSe-w7KEslx3JhSIk5LA"}""", request.JsonString);
+                })
+                .ReturnsAsync(apiResponse);
+
+            var request = new UnfavoriteTweetRequest
+            {
+                TweetId = new("12345"),
+            };
+
+            await request.Send(mock.Object);
+
+            mock.VerifyAll();
+        }
+    }
+}
diff --git a/OpenTween.Tests/Resources/Responses/FavoriteTweet.json b/OpenTween.Tests/Resources/Responses/FavoriteTweet.json
new file mode 100644 (file)
index 0000000..a3c2d33
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "data": {
+    "favorite_tweet": "Done"
+  }
+}
diff --git a/OpenTween.Tests/Resources/Responses/FavoriteTweet_AlreadyFavorited.json b/OpenTween.Tests/Resources/Responses/FavoriteTweet_AlreadyFavorited.json
new file mode 100644 (file)
index 0000000..71415d2
--- /dev/null
@@ -0,0 +1,33 @@
+{
+  "errors": [
+    {
+      "message": "Authorization: Actor (uid: 1234567890) has already favorited tweet (tweetId: 1234567890123456789)",
+      "locations": [
+        {
+          "line": 2,
+          "column": 3
+        }
+      ],
+      "path": [
+        "favorite_tweet"
+      ],
+      "extensions": {
+        "name": "AuthorizationError",
+        "source": "Client",
+        "code": 139,
+        "kind": "Permissions",
+        "tracing": {
+          "trace_id": "87bbb9c871e092a6"
+        }
+      },
+      "code": 139,
+      "kind": "Permissions",
+      "name": "AuthorizationError",
+      "source": "Client",
+      "tracing": {
+        "trace_id": "87bbb9c871e092a6"
+      }
+    }
+  ],
+  "data": {}
+}
diff --git a/OpenTween.Tests/Resources/Responses/UnfavoriteTweet.json b/OpenTween.Tests/Resources/Responses/UnfavoriteTweet.json
new file mode 100644 (file)
index 0000000..11e92e2
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "data": {
+    "unfavorite_tweet": "Done"
+  }
+}
diff --git a/OpenTween/Api/GraphQL/FavoriteTweetRequest.cs b/OpenTween/Api/GraphQL/FavoriteTweetRequest.cs
new file mode 100644 (file)
index 0000000..1abe7ec
--- /dev/null
@@ -0,0 +1,73 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 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.
+
+#nullable enable
+
+using System;
+using System.Threading.Tasks;
+using OpenTween.Connection;
+using OpenTween.Models;
+
+namespace OpenTween.Api.GraphQL
+{
+    public class FavoriteTweetRequest
+    {
+        private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet");
+
+        public required TwitterStatusId TweetId { get; set; }
+
+        public string CreateRequestBody()
+        {
+            return $$"""
+            {"variables":{"tweet_id":"{{JsonUtils.EscapeJsonString(this.TweetId.Id)}}"},"queryId":"lI07N6Otwv1PhnEgXILM7A"}
+            """;
+        }
+
+        public async Task<FavoriteTweetResponse> Send(IApiConnection apiConnection)
+        {
+            var request = new PostJsonRequest
+            {
+                RequestUri = EndpointUri,
+                JsonString = this.CreateRequestBody(),
+            };
+
+            using var response = await apiConnection.SendAsync(request)
+                .ConfigureAwait(false);
+
+            var rootElm = await response.ReadAsJsonXml()
+                .ConfigureAwait(false);
+
+            try
+            {
+                ErrorResponse.ThrowIfError(rootElm);
+            }
+            catch (WebApiException ex)
+                when (ex.Message.Contains("has already favorited"))
+            {
+                // 重複リクエストの場合は成功と見なす
+            }
+
+            return new();
+        }
+
+        public readonly record struct FavoriteTweetResponse();
+    }
+}
diff --git a/OpenTween/Api/GraphQL/UnfavoriteTweetRequest.cs b/OpenTween/Api/GraphQL/UnfavoriteTweetRequest.cs
new file mode 100644 (file)
index 0000000..c952da2
--- /dev/null
@@ -0,0 +1,65 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 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.
+
+#nullable enable
+
+using System;
+using System.Threading.Tasks;
+using OpenTween.Connection;
+using OpenTween.Models;
+
+namespace OpenTween.Api.GraphQL
+{
+    public class UnfavoriteTweetRequest
+    {
+        private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet");
+
+        public required TwitterStatusId TweetId { get; set; }
+
+        public string CreateRequestBody()
+        {
+            return $$"""
+            {"variables":{"tweet_id":"{{JsonUtils.EscapeJsonString(this.TweetId.Id)}}"},"queryId":"ZYKSe-w7KEslx3JhSIk5LA"}
+            """;
+        }
+
+        public async Task<UnfavoriteTweetResponse> Send(IApiConnection apiConnection)
+        {
+            var request = new PostJsonRequest
+            {
+                RequestUri = EndpointUri,
+                JsonString = this.CreateRequestBody(),
+            };
+
+            using var response = await apiConnection.SendAsync(request)
+                .ConfigureAwait(false);
+
+            var rootElm = await response.ReadAsJsonXml()
+                .ConfigureAwait(false);
+
+            ErrorResponse.ThrowIfError(rootElm);
+
+            return new();
+        }
+
+        public readonly record struct UnfavoriteTweetResponse();
+    }
+}
index 9e112de..a273dc6 100644 (file)
@@ -1381,17 +1381,9 @@ namespace OpenTween
                 try
                 {
                     var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
-                    try
-                    {
-                        await this.tw.Api.FavoritesCreate(twitterStatusId)
-                            .IgnoreResponse()
-                            .ConfigureAwait(false);
-                    }
-                    catch (TwitterApiException ex)
-                        when (ex.Errors.All(x => x.Code == TwitterErrorCode.AlreadyFavorited))
-                    {
-                        // エラーコード 139 のみの場合は成功と見なす
-                    }
+
+                    await this.tw.PostFavAdd(twitterStatusId)
+                        .ConfigureAwait(false);
 
                     if (this.settings.Common.RestrictFavCheck)
                     {
@@ -1511,9 +1503,7 @@ namespace OpenTween
 
                     try
                     {
-                        await this.tw.Api.FavoritesDestroy(twitterStatusId)
-                            .IgnoreResponse()
-                            .ConfigureAwait(false);
+                        await this.tw.PostFavRemove(twitterStatusId);
                     }
                     catch (WebApiException)
                     {
index f9ab5af..e96c2ab 100644 (file)
@@ -474,6 +474,54 @@ namespace OpenTween
             }
         }
 
+        public async Task PostFavAdd(TwitterStatusId statusId)
+        {
+            if (this.Api.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new FavoriteTweetRequest
+                {
+                    TweetId = statusId,
+                };
+
+                await request.Send(this.Api.Connection)
+                    .ConfigureAwait(false);
+            }
+            else
+            {
+                try
+                {
+                    await this.Api.FavoritesCreate(statusId)
+                        .IgnoreResponse()
+                        .ConfigureAwait(false);
+                }
+                catch (TwitterApiException ex)
+                    when (ex.Errors.All(x => x.Code == TwitterErrorCode.AlreadyFavorited))
+                {
+                    // エラーコード 139 のみの場合は成功と見なす
+                }
+            }
+        }
+
+        public async Task PostFavRemove(TwitterStatusId statusId)
+        {
+            if (this.Api.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new UnfavoriteTweetRequest
+                {
+                    TweetId = statusId,
+                };
+
+                await request.Send(this.Api.Connection)
+                    .ConfigureAwait(false);
+            }
+            else
+            {
+                await this.Api.FavoritesDestroy(statusId)
+                    .IgnoreResponse()
+                    .ConfigureAwait(false);
+            }
+        }
+
         public string Username
             => this.Api.CurrentScreenName;