Skip to content

Commit

Permalink
発言に含まれる短縮URLの展開処理をPostUrlExpanderクラスに移動
Browse files Browse the repository at this point in the history
  • Loading branch information
upsilon committed Dec 11, 2023
1 parent 02b8f6b commit 2ab613d
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 117 deletions.
59 changes: 0 additions & 59 deletions OpenTween.Tests/Models/PostClassTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,64 +305,5 @@ public void ConvertToOriginalPost_ErrorTest()

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);
}
}
}
91 changes: 91 additions & 0 deletions OpenTween.Tests/Models/PostUrlExpanderTest.cs
Original file line number Diff line number Diff line change
@@ -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();

// ExpandedUrlInfo による展開が完了していない状態
// → この段階では 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);

// ExpandedUrlInfo による展開が完了した後の状態
// → 再帰的な展開後の 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);
}
}
}
3 changes: 0 additions & 3 deletions OpenTween.Tests/Models/TwitterPostFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ public class TwitterPostFactoryTest

private readonly Random random = new();

public TwitterPostFactoryTest()
=> PostClass.ExpandedUrlInfo.AutoExpand = false;

private TabInformations CreateTabinfo()
{
var tabinfo = new TabInformations();
Expand Down
60 changes: 6 additions & 54 deletions OpenTween/Models/PostClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,60 +117,12 @@ public string Text

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]
Expand Down Expand Up @@ -333,7 +285,7 @@ private string ReplaceToExpandedUrl(string html, out bool completedAll)

foreach (var urlInfo in this.ExpandedUrls)
{
if (!urlInfo.ExpandedCompleted)
if (!urlInfo.ExpandCompleted)
completedAll = false;

var tcoUrl = urlInfo.Url;
Expand Down
65 changes: 65 additions & 0 deletions OpenTween/Models/PostUrlExpander.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
10 changes: 9 additions & 1 deletion OpenTween/Twitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,14 @@ public class Twitter : IDisposable
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();
Expand Down Expand Up @@ -752,7 +754,12 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status)
=> 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)
{
Expand Down Expand Up @@ -1195,6 +1202,7 @@ private void CreateDirectMessagesEventFromJson(
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)
Expand Down

0 comments on commit 2ab613d

Please sign in to comment.