[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>",
--- /dev/null
+// OpenTween - Client of Twitter
+// Copyright (c) 2013 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 OpenTween.Api.DataModel;
+using Xunit;
+
+namespace OpenTween.Models
+{
+ public class TwitterPostFactoryTest
+ {
+ private static readonly ISet<long> EmptyIdSet = new HashSet<long>();
+
+ private readonly Random random = new();
+
+ public TwitterPostFactoryTest()
+ => PostClass.ExpandedUrlInfo.AutoExpand = false;
+
+ private TabInformations CreateTabinfo()
+ {
+ var tabinfo = new TabInformations();
+ tabinfo.AddDefaultTabs();
+ return tabinfo;
+ }
+
+ private TwitterStatus CreateStatus()
+ {
+ var statusId = this.random.Next(10000);
+
+ return new()
+ {
+ Id = statusId,
+ IdStr = statusId.ToString(),
+ CreatedAt = "Sat Jan 01 00:00:00 +0000 2022",
+ FullText = "hoge",
+ Source = "<a href=\"https://www.opentween.org/\" rel=\"nofollow\">OpenTween</a>",
+ Entities = new(),
+ User = this.CreateUser(),
+ };
+ }
+
+ private TwitterUser CreateUser()
+ {
+ var userId = this.random.Next(10000);
+
+ return new()
+ {
+ Id = userId,
+ IdStr = userId.ToString(),
+ ScreenName = "tetete",
+ Name = "ててて",
+ ProfileImageUrlHttps = "https://example.com/profile.png",
+ };
+ }
+
+ [Fact]
+ public void CreateFromStatus_Test()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+ var status = this.CreateStatus();
+ var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds: EmptyIdSet);
+
+ Assert.Equal(status.Id, post.StatusId);
+ Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt);
+ Assert.Equal("hoge", post.Text);
+ Assert.Equal("hoge", post.TextFromApi);
+ Assert.Equal("hoge", post.TextSingleLine);
+ Assert.Equal("hoge", post.AccessibleText);
+ Assert.Empty(post.ReplyToList);
+ Assert.Empty(post.QuoteStatusIds);
+ Assert.Empty(post.ExpandedUrls);
+ Assert.Empty(post.Media);
+ Assert.Null(post.PostGeo);
+ Assert.Equal("OpenTween", post.Source);
+ Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString);
+ Assert.Equal(0, post.FavoritedCount);
+ Assert.False(post.IsFav);
+ Assert.False(post.IsDm);
+ Assert.False(post.IsDeleted);
+ Assert.False(post.IsRead);
+ Assert.False(post.IsExcludeReply);
+ Assert.False(post.FilterHit);
+ Assert.False(post.IsMark);
+
+ Assert.False(post.IsReply);
+ Assert.Null(post.InReplyToStatusId);
+ Assert.Null(post.InReplyToUserId);
+ Assert.Null(post.InReplyToUser);
+
+ Assert.Null(post.RetweetedId);
+ Assert.Null(post.RetweetedBy);
+ Assert.Null(post.RetweetedByUserId);
+
+ Assert.Equal(status.User.Id, post.UserId);
+ Assert.Equal("tetete", post.ScreenName);
+ Assert.Equal("ててて", post.Nickname);
+ Assert.Equal("https://example.com/profile.png", post.ImageUrl);
+ Assert.False(post.IsProtect);
+ Assert.False(post.IsOwl);
+ Assert.False(post.IsMe);
+ }
+
+ [Fact]
+ public void CreateFromStatus_AuthorTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+ var status = this.CreateStatus();
+ var selfUserId = status.User.Id;
+ var post = factory.CreateFromStatus(status, selfUserId, followerIds: EmptyIdSet);
+
+ Assert.True(post.IsMe);
+ }
+
+ [Fact]
+ public void CreateFromStatus_FollowerTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+ var status = this.CreateStatus();
+ var followerIds = new HashSet<long> { status.User.Id };
+ var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds);
+
+ Assert.False(post.IsOwl);
+ }
+
+ [Fact]
+ public void CreateFromStatus_NotFollowerTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+ var status = this.CreateStatus();
+ var followerIds = new HashSet<long> { 30000L };
+ var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds);
+
+ Assert.True(post.IsOwl);
+ }
+
+ [Fact]
+ public void CreateFromStatus_RetweetTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+ var originalStatus = this.CreateStatus();
+
+ var retweetStatus = this.CreateStatus();
+ retweetStatus.RetweetedStatus = originalStatus;
+ retweetStatus.Source = "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>";
+
+ var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet);
+
+ Assert.Equal(retweetStatus.Id, post.StatusId);
+ Assert.Equal(retweetStatus.User.Id, post.RetweetedByUserId);
+ Assert.Equal(originalStatus.Id, post.RetweetedId);
+ Assert.Equal(originalStatus.User.Id, post.UserId);
+
+ Assert.Equal("OpenTween", post.Source);
+ Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString);
+ }
+
+ private TwitterMessageEvent CreateDirectMessage(string senderId, string recipientId)
+ {
+ var messageId = this.random.Next(10000);
+
+ return new()
+ {
+ Type = "message_create",
+ Id = messageId.ToString(),
+ CreatedTimestamp = "1640995200000",
+ MessageCreate = new()
+ {
+ SenderId = senderId,
+ Target = new()
+ {
+ RecipientId = recipientId,
+ },
+ MessageData = new()
+ {
+ Text = "hoge",
+ Entities = new(),
+ },
+ SourceAppId = "22519141",
+ },
+ };
+ }
+
+ private Dictionary<string, TwitterMessageEventList.App> CreateApps()
+ {
+ return new()
+ {
+ ["22519141"] = new()
+ {
+ Id = "22519141",
+ Name = "OpenTween",
+ Url = "https://www.opentween.org/",
+ },
+ };
+ }
+
+ [Fact]
+ public void CreateFromDirectMessageEvent_Test()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var selfUser = this.CreateUser();
+ var otherUser = this.CreateUser();
+ var eventItem = this.CreateDirectMessage(senderId: otherUser.IdStr, recipientId: selfUser.IdStr);
+ var users = new Dictionary<string, TwitterUser>()
+ {
+ [selfUser.IdStr] = selfUser,
+ [otherUser.IdStr] = otherUser,
+ };
+ var apps = this.CreateApps();
+ var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id);
+
+ Assert.Equal(long.Parse(eventItem.Id), post.StatusId);
+ Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt);
+ Assert.Equal("hoge", post.Text);
+ Assert.Equal("hoge", post.TextFromApi);
+ Assert.Equal("hoge", post.TextSingleLine);
+ Assert.Equal("hoge", post.AccessibleText);
+ Assert.Empty(post.ReplyToList);
+ Assert.Empty(post.QuoteStatusIds);
+ Assert.Empty(post.ExpandedUrls);
+ Assert.Empty(post.Media);
+ Assert.Null(post.PostGeo);
+ Assert.Equal("OpenTween", post.Source);
+ Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString);
+ Assert.Equal(0, post.FavoritedCount);
+ Assert.False(post.IsFav);
+ Assert.True(post.IsDm);
+ Assert.False(post.IsDeleted);
+ Assert.False(post.IsRead);
+ Assert.False(post.IsExcludeReply);
+ Assert.False(post.FilterHit);
+ Assert.False(post.IsMark);
+
+ Assert.False(post.IsReply);
+ Assert.Null(post.InReplyToStatusId);
+ Assert.Null(post.InReplyToUserId);
+ Assert.Null(post.InReplyToUser);
+
+ Assert.Null(post.RetweetedId);
+ Assert.Null(post.RetweetedBy);
+ Assert.Null(post.RetweetedByUserId);
+
+ Assert.Equal(otherUser.Id, post.UserId);
+ Assert.Equal("tetete", post.ScreenName);
+ Assert.Equal("ててて", post.Nickname);
+ Assert.Equal("https://example.com/profile.png", post.ImageUrl);
+ Assert.False(post.IsProtect);
+ Assert.True(post.IsOwl);
+ Assert.False(post.IsMe);
+ }
+
+ [Fact]
+ public void CreateFromDirectMessageEvent_SenderTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var selfUser = this.CreateUser();
+ var otherUser = this.CreateUser();
+ var eventItem = this.CreateDirectMessage(senderId: selfUser.IdStr, recipientId: otherUser.IdStr);
+ var users = new Dictionary<string, TwitterUser>()
+ {
+ [selfUser.IdStr] = selfUser,
+ [otherUser.IdStr] = otherUser,
+ };
+ var apps = this.CreateApps();
+ var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id);
+
+ Assert.Equal(otherUser.Id, post.UserId);
+ Assert.False(post.IsOwl);
+ Assert.True(post.IsMe);
+ }
+
+ [Fact]
+ public void CreateFromStatus_MediaAltTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var status = this.CreateStatus();
+ status.FullText = "https://t.co/hoge";
+ status.ExtendedEntities = new()
+ {
+ Media = new[]
+ {
+ new TwitterEntityMedia
+ {
+ Indices = new[] { 0, 17 },
+ Url = "https://t.co/hoge",
+ DisplayUrl = "pic.twitter.com/hoge",
+ ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1",
+ AltText = "代替テキスト",
+ },
+ },
+ };
+
+ var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
+
+ var accessibleText = string.Format(Properties.Resources.ImageAltText, "代替テキスト");
+ Assert.Equal(accessibleText, post.AccessibleText);
+ Assert.Equal("<a href=\"https://t.co/hoge\" title=\"代替テキスト\">pic.twitter.com/hoge</a>", post.Text);
+ Assert.Equal("pic.twitter.com/hoge", post.TextFromApi);
+ Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine);
+ }
+
+ [Fact]
+ public void CreateFromStatus_MediaNoAltTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var status = this.CreateStatus();
+ status.FullText = "https://t.co/hoge";
+ status.ExtendedEntities = new()
+ {
+ Media = new[]
+ {
+ new TwitterEntityMedia
+ {
+ Indices = new[] { 0, 17 },
+ Url = "https://t.co/hoge",
+ DisplayUrl = "pic.twitter.com/hoge",
+ ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1",
+ AltText = null,
+ },
+ },
+ };
+
+ var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
+
+ Assert.Equal("pic.twitter.com/hoge", post.AccessibleText);
+ Assert.Equal("<a href=\"https://t.co/hoge\" title=\"https://twitter.com/hoge/status/1234567890/photo/1\">pic.twitter.com/hoge</a>", post.Text);
+ Assert.Equal("pic.twitter.com/hoge", post.TextFromApi);
+ Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine);
+ }
+
+ [Fact]
+ public void CreateFromStatus_QuotedUrlTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var status = this.CreateStatus();
+ status.FullText = "https://t.co/hoge";
+ status.Entities = new()
+ {
+ Urls = new[]
+ {
+ new TwitterEntityUrl
+ {
+ Indices = new[] { 0, 17 },
+ Url = "https://t.co/hoge",
+ DisplayUrl = "twitter.com/hoge/status/1…",
+ ExpandedUrl = "https://twitter.com/hoge/status/1234567890",
+ },
+ },
+ };
+ status.QuotedStatus = new()
+ {
+ Id = 1234567890L,
+ IdStr = "1234567890",
+ User = new()
+ {
+ Id = 1111,
+ IdStr = "1111",
+ ScreenName = "foo",
+ },
+ FullText = "test",
+ };
+
+ var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
+
+ var accessibleText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test");
+ Assert.Equal(accessibleText, post.AccessibleText);
+ Assert.Equal("<a href=\"https://twitter.com/hoge/status/1234567890\" title=\"https://twitter.com/hoge/status/1234567890\">twitter.com/hoge/status/1…</a>", post.Text);
+ Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi);
+ Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine);
+ }
+
+ [Fact]
+ public void CreateFromStatus_QuotedUrlWithPermelinkTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var status = this.CreateStatus();
+ status.FullText = "hoge";
+ status.QuotedStatus = new()
+ {
+ Id = 1234567890L,
+ IdStr = "1234567890",
+ User = new TwitterUser
+ {
+ Id = 1111,
+ IdStr = "1111",
+ ScreenName = "foo",
+ },
+ FullText = "test",
+ };
+ status.QuotedStatusPermalink = new()
+ {
+ Url = "https://t.co/hoge",
+ Display = "twitter.com/hoge/status/1…",
+ Expanded = "https://twitter.com/hoge/status/1234567890",
+ };
+
+ var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
+
+ var accessibleText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test");
+ Assert.Equal(accessibleText, post.AccessibleText);
+ Assert.Equal("hoge <a href=\"https://twitter.com/hoge/status/1234567890\" title=\"https://twitter.com/hoge/status/1234567890\">twitter.com/hoge/status/1…</a>", post.Text);
+ Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextFromApi);
+ Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextSingleLine);
+ }
+
+ [Fact]
+ public void CreateFromStatus_QuotedUrlNoReferenceTest()
+ {
+ var factory = new TwitterPostFactory(this.CreateTabinfo());
+
+ var status = this.CreateStatus();
+ status.FullText = "https://t.co/hoge";
+ status.Entities = new()
+ {
+ Urls = new[]
+ {
+ new TwitterEntityUrl
+ {
+ Indices = new[] { 0, 17 },
+ Url = "https://t.co/hoge",
+ DisplayUrl = "twitter.com/hoge/status/1…",
+ ExpandedUrl = "https://twitter.com/hoge/status/1234567890",
+ },
+ },
+ };
+ status.QuotedStatus = null;
+
+ var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
+
+ var accessibleText = "twitter.com/hoge/status/1…";
+ Assert.Equal(accessibleText, post.AccessibleText);
+ Assert.Equal("<a href=\"https://twitter.com/hoge/status/1234567890\" title=\"https://twitter.com/hoge/status/1234567890\">twitter.com/hoge/status/1…</a>", post.Text);
+ Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi);
+ Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine);
+ }
+
+ [Fact]
+ public void CreateHtmlAnchor_Test()
+ {
+ var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3";
+ var entities = new TwitterEntities
+ {
+ UserMentions = new[]
+ {
+ new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" },
+ },
+ Hashtags = new[]
+ {
+ new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" },
+ },
+ Urls = new[]
+ {
+ new TwitterEntityUrl
+ {
+ Indices = new[] { 31, 54 },
+ Url = "https://t.co/mIJcSoVSK3",
+ DisplayUrl = "apps-of-a-feather.com",
+ ExpandedUrl = "http://apps-of-a-feather.com/",
+ },
+ },
+ };
+
+ var expectedHtml = @"<a class=""mention"" href=""https://twitter.com/twitterapi"">@twitterapi</a>"
+ + @" <a class=""hashtag"" href=""https://twitter.com/search?q=%23BreakingMyTwitter"">#BreakingMyTwitter</a>"
+ + @" <a href=""https://t.co/mIJcSoVSK3"" title=""https://t.co/mIJcSoVSK3"">apps-of-a-feather.com</a>";
+
+ Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null));
+ }
+
+ [Fact]
+ public void CreateHtmlAnchor_NicovideoTest()
+ {
+ var text = "sm9";
+ var entities = new TwitterEntities();
+
+ var expectedHtml = @"<a href=""https://www.nicovideo.jp/watch/sm9"">sm9</a>";
+
+ Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null));
+ }
+
+ [Fact]
+ public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest()
+ {
+ var text = "hoge";
+ var entities = new TwitterEntities();
+ var quotedStatusLink = new TwitterQuotedStatusPermalink
+ {
+ Url = "https://t.co/hoge",
+ Display = "twitter.com/hoge/status/1…",
+ Expanded = "https://twitter.com/hoge/status/1234567890",
+ };
+
+ var expectedHtml = @"hoge"
+ + @" <a href=""https://twitter.com/hoge/status/1234567890"" title=""https://twitter.com/hoge/status/1234567890"">twitter.com/hoge/status/1…</a>";
+
+ Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink));
+ }
+
+ [Fact]
+ public void ParseSource_Test()
+ {
+ var sourceHtml = "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>";
+
+ var expected = ("Twitter Web Client", new Uri("http://twitter.com/"));
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void ParseSource_PlainTextTest()
+ {
+ var sourceHtml = "web";
+
+ var expected = ("web", (Uri?)null);
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void ParseSource_RelativeUriTest()
+ {
+ // 参照: https://twitter.com/kim_upsilon/status/477796052049752064
+ var sourceHtml = "<a href=\"erased_45416\" rel=\"nofollow\">erased_45416</a>";
+
+ var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416"));
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void ParseSource_EmptyTest()
+ {
+ // 参照: https://twitter.com/kim_upsilon/status/595156014032244738
+ var sourceHtml = "";
+
+ var expected = ("", (Uri?)null);
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void ParseSource_NullTest()
+ {
+ string? sourceHtml = null;
+
+ var expected = ("", (Uri?)null);
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void ParseSource_UnescapeTest()
+ {
+ var sourceHtml = "<a href=\"http://example.com/?aaa=123&bbb=456\" rel=\"nofollow\"><<hogehoge>></a>";
+
+ var expected = ("<<hogehoge>>", new Uri("http://example.com/?aaa=123&bbb=456"));
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void ParseSource_UnescapeNoUriTest()
+ {
+ var sourceHtml = "<<hogehoge>>";
+
+ var expected = ("<<hogehoge>>", (Uri?)null);
+ Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
+ }
+
+ [Fact]
+ public void GetQuoteTweetStatusIds_EntityTest()
+ {
+ var entities = new[]
+ {
+ new TwitterEntityUrl
+ {
+ Url = "https://t.co/3HXq0LrbJb",
+ ExpandedUrl = "https://twitter.com/kim_upsilon/status/599261132361072640",
+ },
+ };
+
+ var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink: null);
+ Assert.Equal(new[] { 599261132361072640L }, statusIds);
+ }
+
+ [Fact]
+ public void GetQuoteTweetStatusIds_QuotedStatusLinkTest()
+ {
+ var entities = new TwitterEntities();
+ var quotedStatusLink = new TwitterQuotedStatusPermalink
+ {
+ Url = "https://t.co/3HXq0LrbJb",
+ Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640",
+ };
+
+ var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink);
+ Assert.Equal(new[] { 599261132361072640L }, statusIds);
+ }
+
+ [Fact]
+ public void GetQuoteTweetStatusIds_UrlStringTest()
+ {
+ var urls = new[]
+ {
+ "https://twitter.com/kim_upsilon/status/599261132361072640",
+ };
+
+ var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls);
+ Assert.Equal(new[] { 599261132361072640L }, statusIds);
+ }
+
+ [Fact]
+ public void GetQuoteTweetStatusIds_OverflowTest()
+ {
+ var urls = new[]
+ {
+ // 符号付き 64 ビット整数の範囲を超える値
+ "https://twitter.com/kim_upsilon/status/9999999999999999999",
+ };
+
+ var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls);
+ Assert.Empty(statusIds);
+ }
+ }
+}
}
[Fact]
- public void CreateAccessibleText_MediaAltTest()
- {
- var text = "https://t.co/hoge";
- var entities = new TwitterEntities
- {
- Media = new[]
- {
- new TwitterEntityMedia
- {
- Indices = new[] { 0, 17 },
- Url = "https://t.co/hoge",
- DisplayUrl = "pic.twitter.com/hoge",
- ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1",
- AltText = "代替テキスト",
- },
- },
- };
-
- var expectedText = string.Format(Properties.Resources.ImageAltText, "代替テキスト");
-
- Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus: null, quotedStatusLink: null));
- }
-
- [Fact]
- public void CreateAccessibleText_MediaNoAltTest()
- {
- var text = "https://t.co/hoge";
- var entities = new TwitterEntities
- {
- Media = new[]
- {
- new TwitterEntityMedia
- {
- Indices = new[] { 0, 17 },
- Url = "https://t.co/hoge",
- DisplayUrl = "pic.twitter.com/hoge",
- ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1",
- AltText = null,
- },
- },
- };
-
- var expectedText = "pic.twitter.com/hoge";
-
- Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus: null, quotedStatusLink: null));
- }
-
- [Fact]
- public void CreateAccessibleText_QuotedUrlTest()
- {
- var text = "https://t.co/hoge";
- var entities = new TwitterEntities
- {
- Urls = new[]
- {
- new TwitterEntityUrl
- {
- Indices = new[] { 0, 17 },
- Url = "https://t.co/hoge",
- DisplayUrl = "twitter.com/hoge/status/1…",
- ExpandedUrl = "https://twitter.com/hoge/status/1234567890",
- },
- },
- };
- var quotedStatus = new TwitterStatus
- {
- Id = 1234567890L,
- IdStr = "1234567890",
- User = new TwitterUser
- {
- Id = 1111,
- IdStr = "1111",
- ScreenName = "foo",
- },
- FullText = "test",
- };
-
- var expectedText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test");
-
- Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink: null));
- }
-
- [Fact]
- public void CreateAccessibleText_QuotedUrlWithPermelinkTest()
- {
- var text = "hoge";
- var entities = new TwitterEntities();
- var quotedStatus = new TwitterStatus
- {
- Id = 1234567890L,
- IdStr = "1234567890",
- User = new TwitterUser
- {
- Id = 1111,
- IdStr = "1111",
- ScreenName = "foo",
- },
- FullText = "test",
- };
- var quotedStatusLink = new TwitterQuotedStatusPermalink
- {
- Url = "https://t.co/hoge",
- Display = "twitter.com/hoge/status/1…",
- Expanded = "https://twitter.com/hoge/status/1234567890",
- };
-
- var expectedText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test");
-
- Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink));
- }
-
- [Fact]
- public void CreateAccessibleText_QuotedUrlNoReferenceTest()
- {
- var text = "https://t.co/hoge";
- var entities = new TwitterEntities
- {
- Urls = new[]
- {
- new TwitterEntityUrl
- {
- Indices = new[] { 0, 17 },
- Url = "https://t.co/hoge",
- DisplayUrl = "twitter.com/hoge/status/1…",
- ExpandedUrl = "https://twitter.com/hoge/status/1234567890",
- },
- },
- };
- var quotedStatus = (TwitterStatus?)null;
-
- var expectedText = "twitter.com/hoge/status/1…";
-
- Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink: null));
- }
-
- [Fact]
- public void CreateHtmlAnchor_Test()
- {
- var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3";
- var entities = new TwitterEntities
- {
- UserMentions = new[]
- {
- new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" },
- },
- Hashtags = new[]
- {
- new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" },
- },
- Urls = new[]
- {
- new TwitterEntityUrl
- {
- Indices = new[] { 31, 54 },
- Url = "https://t.co/mIJcSoVSK3",
- DisplayUrl = "apps-of-a-feather.com",
- ExpandedUrl = "http://apps-of-a-feather.com/",
- },
- },
- };
-
- var expectedHtml = @"<a class=""mention"" href=""https://twitter.com/twitterapi"">@twitterapi</a>"
- + @" <a class=""hashtag"" href=""https://twitter.com/search?q=%23BreakingMyTwitter"">#BreakingMyTwitter</a>"
- + @" <a href=""https://t.co/mIJcSoVSK3"" title=""https://t.co/mIJcSoVSK3"">apps-of-a-feather.com</a>";
-
- Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink: null));
- }
-
- [Fact]
- public void CreateHtmlAnchor_NicovideoTest()
- {
- var text = "sm9";
- var entities = new TwitterEntities();
-
- var expectedHtml = @"<a href=""https://www.nicovideo.jp/watch/sm9"">sm9</a>";
-
- Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink: null));
- }
-
- [Fact]
- public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest()
- {
- var text = "hoge";
- var entities = new TwitterEntities();
- var quotedStatusLink = new TwitterQuotedStatusPermalink
- {
- Url = "https://t.co/hoge",
- Display = "twitter.com/hoge/status/1…",
- Expanded = "https://twitter.com/hoge/status/1234567890",
- };
-
- var expectedHtml = @"hoge"
- + @" <a href=""https://t.co/hoge"" title=""https://t.co/hoge"">twitter.com/hoge/status/1…</a>";
-
- Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink));
- }
-
- [Fact]
- public void ParseSource_Test()
- {
- var sourceHtml = "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>";
-
- var expected = ("Twitter Web Client", new Uri("http://twitter.com/"));
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void ParseSource_PlainTextTest()
- {
- var sourceHtml = "web";
-
- var expected = ("web", (Uri?)null);
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void ParseSource_RelativeUriTest()
- {
- // 参照: https://twitter.com/kim_upsilon/status/477796052049752064
- var sourceHtml = "<a href=\"erased_45416\" rel=\"nofollow\">erased_45416</a>";
-
- var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416"));
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void ParseSource_EmptyTest()
- {
- // 参照: https://twitter.com/kim_upsilon/status/595156014032244738
- var sourceHtml = "";
-
- var expected = ("", (Uri?)null);
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void ParseSource_NullTest()
- {
- string? sourceHtml = null;
-
- var expected = ("", (Uri?)null);
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void ParseSource_UnescapeTest()
- {
- var sourceHtml = "<a href=\"http://example.com/?aaa=123&bbb=456\" rel=\"nofollow\"><<hogehoge>></a>";
-
- var expected = ("<<hogehoge>>", new Uri("http://example.com/?aaa=123&bbb=456"));
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void ParseSource_UnescapeNoUriTest()
- {
- var sourceHtml = "<<hogehoge>>";
-
- var expected = ("<<hogehoge>>", (Uri?)null);
- Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
- }
-
- [Fact]
- public void GetQuoteTweetStatusIds_EntityTest()
- {
- var entities = new[]
- {
- new TwitterEntityUrl
- {
- Url = "https://t.co/3HXq0LrbJb",
- ExpandedUrl = "https://twitter.com/kim_upsilon/status/599261132361072640",
- },
- };
-
- var statusIds = Twitter.GetQuoteTweetStatusIds(entities, quotedStatusLink: null);
- Assert.Equal(new[] { 599261132361072640L }, statusIds);
- }
-
- [Fact]
- public void GetQuoteTweetStatusIds_QuotedStatusLinkTest()
- {
- var entities = new TwitterEntities();
- var quotedStatusLink = new TwitterQuotedStatusPermalink
- {
- Url = "https://t.co/3HXq0LrbJb",
- Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640",
- };
-
- var statusIds = Twitter.GetQuoteTweetStatusIds(entities, quotedStatusLink);
- Assert.Equal(new[] { 599261132361072640L }, statusIds);
- }
-
- [Fact]
- public void GetQuoteTweetStatusIds_UrlStringTest()
- {
- var urls = new[]
- {
- "https://twitter.com/kim_upsilon/status/599261132361072640",
- };
-
- var statusIds = Twitter.GetQuoteTweetStatusIds(urls);
- Assert.Equal(new[] { 599261132361072640L }, statusIds);
- }
-
- [Fact]
- public void GetQuoteTweetStatusIds_OverflowTest()
- {
- var urls = new[]
- {
- // 符号付き 64 ビット整数の範囲を超える値
- "https://twitter.com/kim_upsilon/status/9999999999999999999",
- };
-
- var statusIds = Twitter.GetQuoteTweetStatusIds(urls);
- Assert.Empty(statusIds);
- }
-
- [Fact]
public void GetApiResultCount_DefaultTest()
{
var oldInstance = SettingManagerTest.Common;
/// </summary>
public class ExpandedUrlInfo : ICloneable
{
+ public static bool AutoExpand { get; set; } = true;
+
/// <summary>展開前の t.co ドメインの URL</summary>
public string Url { get; }
this.Url = url;
this.expandedUrl = expandedUrl;
- if (deepExpand)
+ if (AutoExpand && deepExpand)
this.ExpandTask = this.DeepExpandAsync();
else
this.ExpandTask = Task.CompletedTask;
// List
private List<ListElement> lists = new();
- private TabInformations()
+ internal TabInformations()
{
}
--- /dev/null
+// OpenTween - Client of Twitter
+// Copyright (c) 2022 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.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+using OpenTween.Api.DataModel;
+
+namespace OpenTween.Models
+{
+ public class TwitterPostFactory
+ {
+ private static readonly Uri SourceUriBase = new("https://twitter.com/");
+
+ private readonly TabInformations tabinfo;
+ private readonly HashSet<string> receivedHashTags = new();
+
+ public TwitterPostFactory(TabInformations tabinfo)
+ => this.tabinfo = tabinfo;
+
+ public string[] GetReceivedHashtags()
+ {
+ lock (this.receivedHashTags)
+ {
+ var hashtags = this.receivedHashTags.ToArray();
+ this.receivedHashTags.Clear();
+ return hashtags;
+ }
+ }
+
+ public PostClass CreateFromStatus(
+ TwitterStatus status,
+ long selfUserId,
+ ISet<long> followerIds,
+ bool favTweet = false
+ )
+ {
+ var post = new PostClass();
+ TwitterEntities entities;
+ string sourceHtml;
+
+ post.StatusId = status.Id;
+ if (status.RetweetedStatus != null)
+ {
+ var retweeted = status.RetweetedStatus;
+
+ post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
+
+ // Id
+ post.RetweetedId = retweeted.Id;
+ // 本文
+ post.TextFromApi = retweeted.FullText;
+ entities = retweeted.MergedEntities;
+ sourceHtml = retweeted.Source;
+ // Reply先
+ post.InReplyToStatusId = retweeted.InReplyToStatusId;
+ post.InReplyToUser = retweeted.InReplyToScreenName;
+ post.InReplyToUserId = status.InReplyToUserId;
+
+ if (favTweet)
+ {
+ post.IsFav = true;
+ }
+ else
+ {
+ // 幻覚fav対策
+ var favTab = this.tabinfo.FavoriteTab;
+ post.IsFav = favTab.Contains(retweeted.Id);
+ }
+
+ if (retweeted.Coordinates != null)
+ post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
+
+ // 以下、ユーザー情報
+ var user = retweeted.User;
+ if (user != null)
+ {
+ post.UserId = user.Id;
+ post.ScreenName = user.ScreenName;
+ post.Nickname = user.Name.Trim();
+ post.ImageUrl = user.ProfileImageUrlHttps;
+ post.IsProtect = user.Protected;
+ }
+ else
+ {
+ post.UserId = 0L;
+ post.ScreenName = "?????";
+ post.Nickname = "Unknown User";
+ }
+
+ // Retweetした人
+ if (status.User != null)
+ {
+ post.RetweetedBy = status.User.ScreenName;
+ post.RetweetedByUserId = status.User.Id;
+ post.IsMe = post.RetweetedByUserId == selfUserId;
+ }
+ else
+ {
+ post.RetweetedBy = "?????";
+ post.RetweetedByUserId = 0L;
+ }
+ }
+ else
+ {
+ post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
+ // 本文
+ post.TextFromApi = status.FullText;
+ entities = status.MergedEntities;
+ sourceHtml = status.Source;
+ post.InReplyToStatusId = status.InReplyToStatusId;
+ post.InReplyToUser = status.InReplyToScreenName;
+ post.InReplyToUserId = status.InReplyToUserId;
+
+ if (favTweet)
+ {
+ post.IsFav = true;
+ }
+ else
+ {
+ // 幻覚fav対策
+ var favTab = this.tabinfo.FavoriteTab;
+ post.IsFav = favTab.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav;
+ }
+
+ if (status.Coordinates != null)
+ post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
+
+ // 以下、ユーザー情報
+ var user = status.User;
+ if (user != null)
+ {
+ post.UserId = user.Id;
+ post.ScreenName = user.ScreenName;
+ post.Nickname = user.Name.Trim();
+ post.ImageUrl = user.ProfileImageUrlHttps;
+ post.IsProtect = user.Protected;
+ post.IsMe = post.UserId == selfUserId;
+ }
+ else
+ {
+ post.UserId = 0L;
+ post.ScreenName = "?????";
+ post.Nickname = "Unknown User";
+ }
+ }
+ // HTMLに整形
+ var textFromApi = post.TextFromApi;
+
+ var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink;
+
+ if (quotedStatusLink != null && entities.Urls != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded))
+ quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある
+
+ post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink);
+ post.TextFromApi = textFromApi;
+ post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink);
+ post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
+ post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+ post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink);
+ post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+ post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
+
+ this.ExtractEntities(entities, post.ReplyToList, post.Media);
+
+ post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink)
+ .Where(x => x != post.StatusId && x != post.RetweetedId)
+ .Distinct().ToArray();
+
+ post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
+ .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
+ .ToArray();
+
+ // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+ if (post.Text == post.TextFromApi)
+ post.Text = post.TextFromApi;
+ if (post.AccessibleText == post.TextFromApi)
+ post.AccessibleText = post.TextFromApi;
+
+ // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+ post.ScreenName = string.Intern(post.ScreenName);
+ post.Nickname = string.Intern(post.Nickname);
+ post.ImageUrl = string.Intern(post.ImageUrl);
+ post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
+
+ // Source整形
+ var (sourceText, sourceUri) = ParseSource(sourceHtml);
+ post.Source = string.Intern(sourceText);
+ post.SourceUri = sourceUri;
+
+ post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == selfUserId);
+ post.IsExcludeReply = false;
+
+ if (post.IsMe)
+ {
+ post.IsOwl = false;
+ }
+ else
+ {
+ if (followerIds.Count > 0)
+ post.IsOwl = !followerIds.Contains(post.UserId);
+ }
+
+ post.IsDm = false;
+ return post;
+ }
+
+ public PostClass CreateFromDirectMessageEvent(
+ TwitterMessageEvent eventItem,
+ IReadOnlyDictionary<string, TwitterUser> users,
+ IReadOnlyDictionary<string, TwitterMessageEventList.App> apps,
+ long selfUserId
+ )
+ {
+ var post = new PostClass();
+ post.StatusId = long.Parse(eventItem.Id);
+
+ var timestamp = long.Parse(eventItem.CreatedTimestamp);
+ post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond);
+ // 本文
+ var textFromApi = eventItem.MessageCreate.MessageData.Text;
+
+ var entities = eventItem.MessageCreate.MessageData.Entities;
+ var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media;
+
+ if (mediaEntity != null)
+ entities.Media = new[] { mediaEntity };
+
+ // HTMLに整形
+ post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null);
+ post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null);
+ post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
+ post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+ post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null);
+ post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+ post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
+ post.IsFav = false;
+
+ this.ExtractEntities(entities, post.ReplyToList, post.Media);
+
+ post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null)
+ .Distinct().ToArray();
+
+ post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
+ .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
+ .ToArray();
+
+ // 以下、ユーザー情報
+ string userId;
+ if (eventItem.MessageCreate.SenderId != selfUserId.ToString(CultureInfo.InvariantCulture))
+ {
+ userId = eventItem.MessageCreate.SenderId;
+ post.IsMe = false;
+ post.IsOwl = true;
+ }
+ else
+ {
+ userId = eventItem.MessageCreate.Target.RecipientId;
+ post.IsMe = true;
+ post.IsOwl = false;
+ }
+
+ if (users.TryGetValue(userId, out var user))
+ {
+ post.UserId = user.Id;
+ post.ScreenName = user.ScreenName;
+ post.Nickname = user.Name.Trim();
+ post.ImageUrl = user.ProfileImageUrlHttps;
+ post.IsProtect = user.Protected;
+ }
+ else
+ {
+ post.UserId = 0L;
+ post.ScreenName = "?????";
+ post.Nickname = "Unknown User";
+ }
+
+ // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+ if (post.Text == post.TextFromApi)
+ post.Text = post.TextFromApi;
+ if (post.AccessibleText == post.TextFromApi)
+ post.AccessibleText = post.TextFromApi;
+
+ // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+ post.ScreenName = string.Intern(post.ScreenName);
+ post.Nickname = string.Intern(post.Nickname);
+ post.ImageUrl = string.Intern(post.ImageUrl);
+
+ var appId = eventItem.MessageCreate.SourceAppId;
+ if (appId != null && apps.TryGetValue(appId, out var app))
+ {
+ post.Source = string.Intern(app.Name);
+
+ try
+ {
+ post.SourceUri = new Uri(SourceUriBase, app.Url);
+ }
+ catch (UriFormatException)
+ {
+ }
+ }
+
+ post.IsReply = false;
+ post.IsExcludeReply = false;
+ post.IsDm = true;
+
+ return post;
+ }
+
+ private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
+ {
+ if (entities?.Urls != null)
+ {
+ foreach (var m in entities.Urls)
+ {
+ if (!MyCommon.IsNullOrEmpty(m.DisplayUrl))
+ text = text.Replace(m.Url, m.DisplayUrl);
+ }
+ }
+
+ if (entities?.Media != null)
+ {
+ foreach (var m in entities.Media)
+ {
+ if (!MyCommon.IsNullOrEmpty(m.DisplayUrl))
+ text = text.Replace(m.Url, m.DisplayUrl);
+ }
+ }
+
+ if (quotedStatusLink != null)
+ text += " " + quotedStatusLink.Display;
+
+ return text;
+ }
+
+ private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> atList, List<MediaInfo> media)
+ {
+ if (entities == null)
+ return;
+
+ if (entities.Hashtags != null)
+ {
+ var hashtags = entities.Hashtags.Select(x => $"#{x.Text}");
+
+ lock (this.receivedHashTags)
+ this.receivedHashTags.UnionWith(hashtags);
+ }
+
+ if (entities.UserMentions != null)
+ {
+ foreach (var ent in entities.UserMentions)
+ atList.Add((ent.Id, ent.ScreenName));
+ }
+
+ if (entities.Media != null)
+ {
+ if (media != null)
+ {
+ foreach (var ent in entities.Media)
+ {
+ if (media.Any(x => x.Url == ent.MediaUrlHttps))
+ continue;
+
+ var videoUrl =
+ ent.VideoInfo != null && ent.Type == "animated_gif" || ent.Type == "video"
+ ? ent.ExpandedUrl
+ : null;
+
+ var mediaInfo = new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl);
+ media.Add(mediaInfo);
+ }
+ }
+ }
+ }
+
+ private static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink)
+ {
+ if (entities == null)
+ return text;
+
+ if (entities.Urls != null)
+ {
+ foreach (var entity in entities.Urls)
+ {
+ if (quotedStatus != null)
+ {
+ var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
+ if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr)
+ {
+ var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
+ text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText));
+ continue;
+ }
+ }
+
+ if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
+ text = text.Replace(entity.Url, entity.DisplayUrl);
+ }
+ }
+
+ if (entities.Media != null)
+ {
+ foreach (var entity in entities.Media)
+ {
+ if (!MyCommon.IsNullOrEmpty(entity.AltText))
+ {
+ text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
+ }
+ else if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
+ {
+ text = text.Replace(entity.Url, entity.DisplayUrl);
+ }
+ }
+ }
+
+ if (quotedStatus != null && quotedStatusLink != null)
+ {
+ var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
+ text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText);
+ }
+
+ return text;
+ }
+
+ internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink? quotedStatusLink)
+ {
+ var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(text));
+
+ // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
+ text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true);
+
+ text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"https://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
+ text = PreProcessUrl(text); // IDN置換
+
+ if (quotedStatusLink != null)
+ {
+ text += string.Format(" <a href=\"{0}\" title=\"{0}\">{1}</a>",
+ WebUtility.HtmlEncode(quotedStatusLink.Expanded),
+ WebUtility.HtmlEncode(quotedStatusLink.Display));
+ }
+
+ return text;
+ }
+
+ private static string PreProcessUrl(string orgData)
+ {
+ int posl1;
+ var posl2 = 0;
+ var href = "<a href=\"";
+
+ while (true)
+ {
+ if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
+ {
+ // IDN展開
+ posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
+ posl1 += href.Length;
+ posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
+ var urlStr = orgData.Substring(posl1, posl2 - posl1);
+
+ if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
+ && !urlStr.StartsWith("https://", StringComparison.Ordinal)
+ && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var replacedUrl = MyCommon.IDNEncode(urlStr);
+ if (replacedUrl == null) continue;
+ if (replacedUrl == urlStr) continue;
+
+ orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
+ posl2 = 0;
+ }
+ else
+ {
+ break;
+ }
+ }
+ return orgData;
+ }
+
+ /// <summary>
+ /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
+ /// </summary>
+ public static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml)
+ {
+ if (MyCommon.IsNullOrEmpty(sourceHtml))
+ return ("", null);
+
+ string sourceText;
+ Uri? sourceUri;
+
+ // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
+
+ var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
+ if (match.Success)
+ {
+ sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
+ try
+ {
+ var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
+ sourceUri = new Uri(SourceUriBase, uriStr);
+ }
+ catch (UriFormatException)
+ {
+ sourceUri = null;
+ }
+ }
+ else
+ {
+ sourceText = WebUtility.HtmlDecode(sourceHtml);
+ sourceUri = null;
+ }
+
+ return (sourceText, sourceUri);
+ }
+
+ /// <summary>
+ /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
+ /// </summary>
+ public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity>? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
+ {
+ entities ??= Enumerable.Empty<TwitterEntity>();
+
+ var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
+
+ if (quotedStatusLink != null)
+ urls = urls.Append(quotedStatusLink.Expanded);
+
+ return GetQuoteTweetStatusIds(urls);
+ }
+
+ public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
+ {
+ foreach (var url in urls)
+ {
+ var match = Twitter.StatusUrlRegex.Match(url);
+ if (match.Success)
+ {
+ if (long.TryParse(match.Groups["StatusId"].Value, out var statusId))
+ yield return statusId;
+ }
+ }
+ }
+ }
+}
<Compile Include="ApplicationInstanceMutex.cs" />
<Compile Include="ApplicationPreconditions.cs" />
<Compile Include="ApplicationSettings.cs" />
+ <Compile Include="Models\TwitterPostFactory.cs" />
<Compile Include="ThemeManager.cs" />
<Compile Include="IconAssetsManager.cs" />
<Compile Include="AsyncTimer.cs" />
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
private ISet<long> followerId = new HashSet<long>();
private long[] noRTId = Array.Empty<long>();
- // プロパティからアクセスされる共通情報
- private readonly List<string> hashList = new();
+ private readonly TwitterPostFactory postFactory;
private string? nextCursorDirectMessage = null;
public Twitter(TwitterApi api)
{
+ this.postFactory = new(TabInformations.GetInstance());
+
this.Api = api;
this.Configuration = TwitterConfiguration.DefaultConfiguration();
this.TextConfiguration = TwitterTextConfiguration.DefaultConfiguration();
this.Api.Initialize(token, tokenSecret, userId, username);
}
- internal static string PreProcessUrl(string orgData)
- {
- int posl1;
- var posl2 = 0;
- var href = "<a href=\"";
-
- while (true)
- {
- if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
- {
- // IDN展開
- posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
- posl1 += href.Length;
- posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
- var urlStr = orgData.Substring(posl1, posl2 - posl1);
-
- if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
- && !urlStr.StartsWith("https://", StringComparison.Ordinal)
- && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
- {
- continue;
- }
-
- var replacedUrl = MyCommon.IDNEncode(urlStr);
- if (replacedUrl == null) continue;
- if (replacedUrl == urlStr) continue;
-
- orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
- posl2 = 0;
- }
- else
- {
- break;
- }
- }
- return orgData;
- }
-
public async Task<PostClass?> PostStatus(PostStatusParams param)
{
this.CheckAccountState();
}
private PostClass CreatePostsFromStatusData(TwitterStatus status)
- => this.CreatePostsFromStatusData(status, false);
+ => this.CreatePostsFromStatusData(status, favTweet: false);
private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
- {
- var post = new PostClass();
- TwitterEntities entities;
- string sourceHtml;
-
- post.StatusId = status.Id;
- if (status.RetweetedStatus != null)
- {
- var retweeted = status.RetweetedStatus;
-
- post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
-
- // Id
- post.RetweetedId = retweeted.Id;
- // 本文
- post.TextFromApi = retweeted.FullText;
- entities = retweeted.MergedEntities;
- sourceHtml = retweeted.Source;
- // Reply先
- post.InReplyToStatusId = retweeted.InReplyToStatusId;
- post.InReplyToUser = retweeted.InReplyToScreenName;
- post.InReplyToUserId = status.InReplyToUserId;
-
- if (favTweet)
- {
- post.IsFav = true;
- }
- else
- {
- // 幻覚fav対策
- var tc = TabInformations.GetInstance().FavoriteTab;
- post.IsFav = tc.Contains(retweeted.Id);
- }
-
- if (retweeted.Coordinates != null)
- post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
-
- // 以下、ユーザー情報
- var user = retweeted.User;
- if (user != null)
- {
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
- }
- else
- {
- post.UserId = 0L;
- post.ScreenName = "?????";
- post.Nickname = "Unknown User";
- }
-
- // Retweetした人
- if (status.User != null)
- {
- post.RetweetedBy = status.User.ScreenName;
- post.RetweetedByUserId = status.User.Id;
- post.IsMe = post.RetweetedByUserId == this.UserId;
- }
- else
- {
- post.RetweetedBy = "?????";
- post.RetweetedByUserId = 0L;
- }
- }
- else
- {
- post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
- // 本文
- post.TextFromApi = status.FullText;
- entities = status.MergedEntities;
- sourceHtml = status.Source;
- post.InReplyToStatusId = status.InReplyToStatusId;
- post.InReplyToUser = status.InReplyToScreenName;
- post.InReplyToUserId = status.InReplyToUserId;
-
- if (favTweet)
- {
- post.IsFav = true;
- }
- else
- {
- // 幻覚fav対策
- var tc = TabInformations.GetInstance().FavoriteTab;
- post.IsFav = tc.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav;
- }
-
- if (status.Coordinates != null)
- post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
-
- // 以下、ユーザー情報
- var user = status.User;
- if (user != null)
- {
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
- post.IsMe = post.UserId == this.UserId;
- }
- else
- {
- post.UserId = 0L;
- post.ScreenName = "?????";
- post.Nickname = "Unknown User";
- }
- }
- // HTMLに整形
- var textFromApi = post.TextFromApi;
-
- var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink;
-
- if (quotedStatusLink != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded))
- quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある
-
- post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink);
- post.TextFromApi = textFromApi;
- post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink);
- post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
- post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
- post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink);
- post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
- post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
-
- this.ExtractEntities(entities, post.ReplyToList, post.Media);
-
- post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink)
- .Where(x => x != post.StatusId && x != post.RetweetedId)
- .Distinct().ToArray();
-
- post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
- .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
- .ToArray();
-
- // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
- if (post.Text == post.TextFromApi)
- post.Text = post.TextFromApi;
- if (post.AccessibleText == post.TextFromApi)
- post.AccessibleText = post.TextFromApi;
-
- // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
- post.ScreenName = string.Intern(post.ScreenName);
- post.Nickname = string.Intern(post.Nickname);
- post.ImageUrl = string.Intern(post.ImageUrl);
- post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
-
- // Source整形
- var (sourceText, sourceUri) = ParseSource(sourceHtml);
- post.Source = string.Intern(sourceText);
- post.SourceUri = sourceUri;
-
- post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == this.UserId);
- post.IsExcludeReply = false;
-
- if (post.IsMe)
- {
- post.IsOwl = false;
- }
- else
- {
- if (this.followerId.Count > 0) post.IsOwl = !this.followerId.Contains(post.UserId);
- }
-
- post.IsDm = false;
- return post;
- }
-
- /// <summary>
- /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
- /// </summary>
- public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity>? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
- {
- entities ??= Enumerable.Empty<TwitterEntity>();
-
- var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
-
- if (quotedStatusLink != null)
- urls = urls.Append(quotedStatusLink.Expanded);
-
- return GetQuoteTweetStatusIds(urls);
- }
-
- public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
- {
- foreach (var url in urls)
- {
- var match = Twitter.StatusUrlRegex.Match(url);
- if (match.Success)
- {
- if (long.TryParse(match.Groups["StatusId"].Value, out var statusId))
- yield return statusId;
- }
- }
- }
+ => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet);
private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read)
{
IReadOnlyDictionary<string, TwitterMessageEventList.App> apps,
bool read)
{
+ var dmTab = TabInformations.GetInstance().DirectMessageTab;
+
foreach (var eventItem in events)
{
- var post = new PostClass();
- post.StatusId = long.Parse(eventItem.Id);
-
- var timestamp = long.Parse(eventItem.CreatedTimestamp);
- post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond);
- // 本文
- var textFromApi = eventItem.MessageCreate.MessageData.Text;
-
- var entities = eventItem.MessageCreate.MessageData.Entities;
- var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media;
-
- if (mediaEntity != null)
- entities.Media = new[] { mediaEntity };
-
- // HTMLに整形
- post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null);
- post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null);
- post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
- post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
- post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null);
- post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
- post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
- post.IsFav = false;
-
- this.ExtractEntities(entities, post.ReplyToList, post.Media);
-
- post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null)
- .Distinct().ToArray();
-
- post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
- .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
- .ToArray();
-
- // 以下、ユーザー情報
- string userId;
- if (eventItem.MessageCreate.SenderId != this.Api.CurrentUserId.ToString(CultureInfo.InvariantCulture))
- {
- userId = eventItem.MessageCreate.SenderId;
- post.IsMe = false;
- post.IsOwl = true;
- }
- else
- {
- userId = eventItem.MessageCreate.Target.RecipientId;
- post.IsMe = true;
- post.IsOwl = false;
- }
-
- if (!users.TryGetValue(userId, out var user))
- continue;
-
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
-
- // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
- if (post.Text == post.TextFromApi)
- post.Text = post.TextFromApi;
- if (post.AccessibleText == post.TextFromApi)
- post.AccessibleText = post.TextFromApi;
-
- // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
- post.ScreenName = string.Intern(post.ScreenName);
- post.Nickname = string.Intern(post.Nickname);
- post.ImageUrl = string.Intern(post.ImageUrl);
-
- var appId = eventItem.MessageCreate.SourceAppId;
- if (appId != null && apps.TryGetValue(appId, out var app))
- {
- post.Source = string.Intern(app.Name);
-
- try
- {
- post.SourceUri = new Uri(SourceUriBase, app.Url);
- }
- catch (UriFormatException)
- {
- }
- }
+ var post = this.postFactory.CreateFromDirectMessageEvent(eventItem, users, apps, this.UserId);
post.IsRead = read;
if (post.IsMe && !read && this.ReadOwnPost)
post.IsRead = true;
- post.IsReply = false;
- post.IsExcludeReply = false;
- post.IsDm = true;
- var dmTab = TabInformations.GetInstance().DirectMessageTab;
dmTab.AddPostQueue(post);
}
}
tab.OldestId = minimumId.Value;
}
- private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
- {
- if (entities != null)
- {
- if (entities.Urls != null)
- {
- foreach (var m in entities.Urls)
- {
- if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
- }
- }
- if (entities.Media != null)
- {
- foreach (var m in entities.Media)
- {
- if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
- }
- }
- }
-
- if (quotedStatusLink != null)
- text += " " + quotedStatusLink.Display;
-
- return text;
- }
-
- internal static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink)
- {
- if (entities == null)
- return text;
-
- if (entities.Urls != null)
- {
- foreach (var entity in entities.Urls)
- {
- if (quotedStatus != null)
- {
- var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
- if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr)
- {
- var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
- text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText));
- continue;
- }
- }
-
- if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
- text = text.Replace(entity.Url, entity.DisplayUrl);
- }
- }
-
- if (entities.Media != null)
- {
- foreach (var entity in entities.Media)
- {
- if (!MyCommon.IsNullOrEmpty(entity.AltText))
- {
- text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
- }
- else if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
- {
- text = text.Replace(entity.Url, entity.DisplayUrl);
- }
- }
- }
-
- if (quotedStatus != null && quotedStatusLink != null)
- {
- var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
- text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText);
- }
-
- return text;
- }
-
/// <summary>
/// フォロワーIDを更新します
/// </summary>
}
}
- private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> atList, List<MediaInfo> media)
- {
- if (entities != null)
- {
- if (entities.Hashtags != null)
- {
- lock (this.lockObj)
- {
- this.hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
- }
- }
- if (entities.UserMentions != null)
- {
- foreach (var ent in entities.UserMentions)
- {
- atList.Add((ent.Id, ent.ScreenName));
- }
- }
- if (entities.Media != null)
- {
- if (media != null)
- {
- foreach (var ent in entities.Media)
- {
- if (!media.Any(x => x.Url == ent.MediaUrlHttps))
- {
- if (ent.VideoInfo != null &&
- ent.Type == "animated_gif" || ent.Type == "video")
- {
- media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
- }
- else
- {
- media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl: null));
- }
- }
- }
- }
- }
- }
- }
-
- internal static string CreateHtmlAnchor(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
- {
- var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(text));
-
- // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
- text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true);
-
- text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"https://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
- text = PreProcessUrl(text); // IDN置換
-
- if (quotedStatusLink != null)
- {
- text += string.Format(" <a href=\"{0}\" title=\"{0}\">{1}</a>",
- WebUtility.HtmlEncode(quotedStatusLink.Url),
- WebUtility.HtmlEncode(quotedStatusLink.Display));
- }
-
- return text;
- }
-
- private static readonly Uri SourceUriBase = new("https://twitter.com/");
-
- /// <summary>
- /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
- /// </summary>
- internal static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml)
- {
- if (MyCommon.IsNullOrEmpty(sourceHtml))
- return ("", null);
-
- string sourceText;
- Uri? sourceUri;
-
- // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
-
- var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
- if (match.Success)
- {
- sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
- try
- {
- var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
- sourceUri = new Uri(SourceUriBase, uriStr);
- }
- catch (UriFormatException)
- {
- sourceUri = null;
- }
- }
- else
- {
- sourceText = WebUtility.HtmlDecode(sourceHtml);
- sourceUri = null;
- }
-
- return (sourceText, sourceUri);
- }
-
public async Task<TwitterApiStatus?> GetInfoApi()
{
if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
}
public string[] GetHashList()
- {
- string[] hashArray;
- lock (this.lockObj)
- {
- hashArray = this.hashList.ToArray();
- this.hashList.Clear();
- }
- return hashArray;
- }
+ => this.postFactory.GetReceivedHashtags();
public string AccessToken
=> ((TwitterApiConnection)this.Api.Connection).AccessToken;