OSDN Git Service

発言に含まれる短縮URLの展開処理をPostUrlExpanderクラスに移動
authorKimura Youichi <kim.upsilon@bucyou.net>
Mon, 11 Dec 2023 14:19:10 +0000 (23:19 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Mon, 11 Dec 2023 14:23:43 +0000 (23:23 +0900)
OpenTween.Tests/Models/PostClassTest.cs
OpenTween.Tests/Models/PostUrlExpanderTest.cs [new file with mode: 0644]
OpenTween.Tests/Models/TwitterPostFactoryTest.cs
OpenTween/Models/PostClass.cs
OpenTween/Models/PostUrlExpander.cs [new file with mode: 0644]
OpenTween/Twitter.cs

index f4f32a7..c7bbfb1 100644 (file)
@@ -305,64 +305,5 @@ namespace OpenTween.Models
 
             Assert.Throws<InvalidOperationException>(() => post.ConvertToOriginalPost());
         }
-
-        private class FakeExpandedUrlInfo : PostClass.ExpandedUrlInfo
-        {
-            public TaskCompletionSource<string> FakeResult = new();
-
-            public FakeExpandedUrlInfo(string url, string expandedUrl, bool deepExpand)
-                : base(url, expandedUrl, deepExpand)
-            {
-            }
-
-            protected override async Task DeepExpandAsync()
-                => this.expandedUrl = await this.FakeResult.Task;
-        }
-
-        [Fact]
-        public async Task ExpandedUrls_BasicScenario()
-        {
-            PostClass.ExpandedUrlInfo.AutoExpand = true;
-
-            var post = new PostClass
-            {
-                Text = """<a href="http://t.co/aaaaaaa" title="http://t.co/aaaaaaa">bit.ly/abcde</a>""",
-                ExpandedUrls = new[]
-                {
-                    new FakeExpandedUrlInfo(
-                        // 展開前の t.co ドメインの URL
-                        url: "http://t.co/aaaaaaa",
-
-                        // Entity の expanded_url に含まれる URL
-                        expandedUrl: "http://bit.ly/abcde",
-
-                        // expandedUrl をさらに ShortUrl クラスで再帰的に展開する
-                        deepExpand: true
-                    ),
-                },
-            };
-
-            var urlInfo = (FakeExpandedUrlInfo)post.ExpandedUrls.Single();
-
-            // ExpandedUrlInfo による展開が完了していない状態
-            //   → この段階では Entity に含まれる expanded_url の URL が使用される
-            Assert.False(urlInfo.ExpandedCompleted);
-            Assert.Equal("http://bit.ly/abcde", urlInfo.ExpandedUrl);
-            Assert.Equal("http://bit.ly/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa"));
-            Assert.Equal(new[] { "http://bit.ly/abcde" }, post.GetExpandedUrls());
-            Assert.Equal("""<a href="http://t.co/aaaaaaa" title="http://bit.ly/abcde">bit.ly/abcde</a>""", post.Text);
-
-            // bit.ly 展開後の URL は「http://example.com/abcde」
-            urlInfo.FakeResult.SetResult("http://example.com/abcde");
-            await urlInfo.ExpandTask;
-
-            // ExpandedUrlInfo による展開が完了した後の状態
-            //   → 再帰的な展開後の URL が使用される
-            Assert.True(urlInfo.ExpandedCompleted);
-            Assert.Equal("http://example.com/abcde", urlInfo.ExpandedUrl);
-            Assert.Equal("http://example.com/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa"));
-            Assert.Equal(new[] { "http://example.com/abcde" }, post.GetExpandedUrls());
-            Assert.Equal("""<a href="http://t.co/aaaaaaa" title="http://example.com/abcde">bit.ly/abcde</a>""", post.Text);
-        }
     }
 }
diff --git a/OpenTween.Tests/Models/PostUrlExpanderTest.cs b/OpenTween.Tests/Models/PostUrlExpanderTest.cs
new file mode 100644 (file)
index 0000000..e662060
--- /dev/null
@@ -0,0 +1,91 @@
+// 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.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace OpenTween.Models
+{
+    public class PostUrlExpanderTest
+    {
+        [Fact]
+        public async Task Expand_Test()
+        {
+            var handler = new HttpMessageHandlerMock();
+            using var http = new HttpClient(handler);
+            var shortUrl = new ShortUrl(http);
+
+            // https://bit.ly/abcde -> https://example.com/abcde
+            handler.Enqueue(x =>
+            {
+                Assert.Equal(HttpMethod.Head, x.Method);
+                Assert.Equal(new Uri("https://bit.ly/abcde"), x.RequestUri);
+
+                return new HttpResponseMessage(HttpStatusCode.TemporaryRedirect)
+                {
+                    Headers = { Location = new Uri("https://example.com/abcde") },
+                };
+            });
+
+            var post = new PostClass
+            {
+                Text = """<a href="https://t.co/aaaaaaa" title="https://t.co/aaaaaaa">bit.ly/abcde</a>""",
+                ExpandedUrls = new[]
+                {
+                    new PostClass.ExpandedUrlInfo(
+                        // 展開前の t.co ドメインの URL
+                        Url: "https://t.co/aaaaaaa",
+
+                        // Entity の expanded_url に含まれる URL
+                        ExpandedUrl: "https://bit.ly/abcde"
+                    ),
+                },
+            };
+
+            var urlInfo = post.ExpandedUrls.Single();
+
+            // 短縮 URL の展開が完了していない状態
+            //   → この段階では Entity に含まれる expanded_url の URL が使用される
+            Assert.False(urlInfo.ExpandCompleted);
+            Assert.Equal("https://bit.ly/abcde", urlInfo.ExpandedUrl);
+            Assert.Equal("https://bit.ly/abcde", post.GetExpandedUrl("https://t.co/aaaaaaa"));
+            Assert.Equal(new[] { "https://bit.ly/abcde" }, post.GetExpandedUrls());
+            Assert.Equal("""<a href="https://t.co/aaaaaaa" title="https://bit.ly/abcde">bit.ly/abcde</a>""", post.Text);
+
+            // bit.ly 展開後の URL は「https://example.com/abcde」
+            var expander = new PostUrlExpander(shortUrl);
+            await expander.Expand(post);
+
+            // 短縮 URL の展開が完了した後の状態
+            //   → 再帰的な展開後の URL が使用される
+            urlInfo = post.ExpandedUrls.Single();
+            Assert.True(urlInfo.ExpandCompleted);
+            Assert.Equal("https://example.com/abcde", urlInfo.ExpandedUrl);
+            Assert.Equal("https://example.com/abcde", post.GetExpandedUrl("https://t.co/aaaaaaa"));
+            Assert.Equal(new[] { "https://example.com/abcde" }, post.GetExpandedUrls());
+            Assert.Equal("""<a href="https://t.co/aaaaaaa" title="https://example.com/abcde">bit.ly/abcde</a>""", post.Text);
+        }
+    }
+}
index c12a7d0..88718fd 100644 (file)
@@ -32,9 +32,6 @@ namespace OpenTween.Models
 
         private readonly Random random = new();
 
-        public TwitterPostFactoryTest()
-            => PostClass.ExpandedUrlInfo.AutoExpand = false;
-
         private TabInformations CreateTabinfo()
         {
             var tabinfo = new TabInformations();
index d73455f..b1791c8 100644 (file)
@@ -117,60 +117,12 @@ namespace OpenTween.Models
 
         public bool IsPromoted { get; set; }
 
-        /// <summary>
-        /// <see cref="PostClass"/> に含まれる t.co の展開後の URL を保持するクラス
-        /// </summary>
-        public class ExpandedUrlInfo : ICloneable
+        public record ExpandedUrlInfo(
+            string Url,
+            string ExpandedUrl
+        )
         {
-            public static bool AutoExpand { get; set; } = true;
-
-            /// <summary>展開前の t.co ドメインの URL</summary>
-            public string Url { get; }
-
-            /// <summary>展開後の URL</summary>
-            /// <remarks>
-            /// <see cref="ShortUrl"/> による展開が完了するまでは Entity に含まれる expanded_url の値を返します
-            /// </remarks>
-            public string ExpandedUrl => this.expandedUrl;
-
-            /// <summary><see cref="ShortUrl"/> による展開を行うタスク</summary>
-            public Task ExpandTask { get; private set; }
-
-            /// <summary><see cref="DeepExpandAsync"/> による展開が完了したか否か</summary>
-            public bool ExpandedCompleted => this.ExpandTask.IsCompleted;
-
-            protected string expandedUrl;
-
-            public ExpandedUrlInfo(string url, string expandedUrl)
-                : this(url, expandedUrl, deepExpand: true)
-            {
-            }
-
-            public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand)
-            {
-                this.Url = url;
-                this.expandedUrl = expandedUrl;
-
-                if (AutoExpand && deepExpand)
-                    this.ExpandTask = this.DeepExpandAsync();
-                else
-                    this.ExpandTask = Task.CompletedTask;
-            }
-
-            protected virtual async Task DeepExpandAsync()
-            {
-                var origUrl = this.expandedUrl;
-                var newUrl = await ShortUrl.Instance.ExpandUrlAsync(origUrl)
-                    .ConfigureAwait(false);
-
-                Interlocked.CompareExchange(ref this.expandedUrl, newUrl, origUrl);
-            }
-
-            public ExpandedUrlInfo Clone()
-                => new(this.Url, this.ExpandedUrl, deepExpand: false);
-
-            object ICloneable.Clone()
-                => this.Clone();
+            public bool ExpandCompleted { get; init; }
         }
 
         [Flags]
@@ -333,7 +285,7 @@ namespace OpenTween.Models
 
             foreach (var urlInfo in this.ExpandedUrls)
             {
-                if (!urlInfo.ExpandedCompleted)
+                if (!urlInfo.ExpandCompleted)
                     completedAll = false;
 
                 var tcoUrl = urlInfo.Url;
diff --git a/OpenTween/Models/PostUrlExpander.cs b/OpenTween/Models/PostUrlExpander.cs
new file mode 100644 (file)
index 0000000..7ee9a20
--- /dev/null
@@ -0,0 +1,65 @@
+// 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.
+
+#nullable enable
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace OpenTween.Models
+{
+    public class PostUrlExpander
+    {
+        private readonly ShortUrl shortUrl;
+
+        public PostUrlExpander(ShortUrl shortUrl)
+            => this.shortUrl = shortUrl;
+
+        public async Task Expand(PostClass post)
+        {
+            var urls = post.ExpandedUrls;
+            if (urls.Length == 0)
+                return;
+
+            var tasks = MyCommon.CountUp(0, urls.Length - 1)
+                .Select(i => this.UpdateUrlItem(urls, i));
+
+            await Task.WhenAll(tasks);
+        }
+
+        public async Task UpdateUrlItem(PostClass.ExpandedUrlInfo[] urls, int index)
+        {
+            var urlItem = urls[index];
+
+            var expandedUrl = await this.shortUrl.ExpandUrlAsync(urlItem.ExpandedUrl)
+                .ConfigureAwait(false);
+
+            var newUrlItem = urlItem with
+            {
+                ExpandedUrl = expandedUrl,
+                ExpandCompleted = true,
+            };
+            Interlocked.Exchange(ref urls[index], newUrlItem);
+        }
+    }
+}
index 3b473a1..ba09b07 100644 (file)
@@ -173,12 +173,14 @@ namespace OpenTween
         private long[] noRTId = Array.Empty<long>();
 
         private readonly TwitterPostFactory postFactory;
+        private readonly PostUrlExpander urlExpander;
 
         private string? previousStatusId = null;
 
         public Twitter(TwitterApi api)
         {
             this.postFactory = new(TabInformations.GetInstance());
+            this.urlExpander = new(ShortUrl.Instance);
 
             this.Api = api;
             this.Configuration = TwitterConfiguration.DefaultConfiguration();
@@ -752,7 +754,12 @@ namespace OpenTween
             => this.CreatePostsFromStatusData(status, favTweet: false);
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
-            => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet);
+        {
+            var post = this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet);
+            _ = this.urlExpander.Expand(post);
+
+            return post;
+        }
 
         private PostId? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read)
         {
@@ -1195,6 +1202,7 @@ namespace OpenTween
             foreach (var eventItem in events)
             {
                 var post = this.postFactory.CreateFromDirectMessageEvent(eventItem, users, apps, this.UserId);
+                _ = this.urlExpander.Expand(post);
 
                 post.IsRead = read;
                 if (post.IsMe && !read && this.ReadOwnPost)