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);
- }
}
}
--- /dev/null
+// 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);
+ }
+ }
+}
private readonly Random random = new();
- public TwitterPostFactoryTest()
- => PostClass.ExpandedUrlInfo.AutoExpand = false;
-
private TabInformations CreateTabinfo()
{
var tabinfo = new TabInformations();
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]
foreach (var urlInfo in this.ExpandedUrls)
{
- if (!urlInfo.ExpandedCompleted)
+ if (!urlInfo.ExpandCompleted)
completedAll = false;
var tcoUrl = urlInfo.Url;
--- /dev/null
+// 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);
+ }
+ }
+}
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();
=> 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)
{
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)