OSDN Git Service

コメントアウトされた不要なコードを削除
[opentween/open-tween.git] / OpenTween / Twitter.cs
index 30e4a04..e184c27 100644 (file)
@@ -25,6 +25,8 @@
 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
 // Boston, MA 02110-1301, USA.
 
+#nullable enable
+
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
@@ -44,6 +46,7 @@ using OpenTween.Api.DataModel;
 using OpenTween.Connection;
 using OpenTween.Models;
 using OpenTween.Setting;
+using System.Globalization;
 
 namespace OpenTween
 {
@@ -71,7 +74,6 @@ namespace OpenTween
         //Hashtag用正規表現
         private const string LATIN_ACCENTS = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff";
         private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
-        //private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u3096\u3400-\u4DBF\u4E00-\u9FFF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F\u2F800-\u2FA1F";
         private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
         private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
         private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
@@ -158,21 +160,15 @@ namespace OpenTween
         delegate void GetIconImageDelegate(PostClass post);
         private readonly object LockObj = new object();
         private ISet<long> followerId = new HashSet<long>();
-        private long[] noRTId = new long[0];
+        private long[] noRTId = Array.Empty<long>();
 
         //プロパティからアクセスされる共通情報
-        private List<string> _hashList = new List<string>();
+        private readonly List<string> _hashList = new List<string>();
 
-        //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
-        private long minDirectmessage = long.MaxValue;
-        private long minDirectmessageSent = long.MaxValue;
+        private string? nextCursorDirectMessage = null;
 
         private long previousStatusId = -1L;
 
-        //private FavoriteQueue favQueue;
-
-        //private List<PostClass> _deletemessages = new List<PostClass>();
-
         public Twitter() : this(new TwitterApi())
         {
         }
@@ -233,19 +229,17 @@ namespace OpenTween
         {
             int posl1;
             var posl2 = 0;
-            //var IDNConveter = new IdnMapping();
             var href = "<a href=\"";
 
             while (true)
             {
                 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
                 {
-                    var urlStr = "";
                     // IDN展開
                     posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
                     posl1 += href.Length;
                     posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
-                    urlStr = orgData.Substring(posl1, posl2 - posl1);
+                    var urlStr = orgData.Substring(posl1, posl2 - posl1);
 
                     if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
                         && !urlStr.StartsWith("https://", StringComparison.Ordinal)
@@ -269,7 +263,7 @@ namespace OpenTween
             return orgData;
         }
 
-        public async Task PostStatus(PostStatusParams param)
+        public async Task<PostClass?> PostStatus(PostStatusParams param)
         {
             this.CheckAccountState();
 
@@ -279,7 +273,7 @@ namespace OpenTween
 
                 await this.SendDirectMessage(param.Text, mediaId)
                     .ConfigureAwait(false);
-                return;
+                return null;
             }
 
             var response = await this.Api.StatusesUpdate(param.Text, param.InReplyToStatusId, param.MediaIds,
@@ -295,30 +289,25 @@ namespace OpenTween
                 throw new WebApiException("OK:Delaying?");
 
             this.previousStatusId = status.Id;
+
+            //投稿したものを返す
+            var post = CreatePostsFromStatusData(status);
+            if (this.ReadOwnPost) post.IsRead = true;
+            return post;
         }
 
-        public async Task<long> UploadMedia(IMediaItem item, string mediaCategory = null)
+        public async Task<long> UploadMedia(IMediaItem item, string? mediaCategory = null)
         {
             this.CheckAccountState();
 
-            string mediaType;
-
-            switch (item.Extension)
+            var mediaType = item.Extension switch
             {
-                case ".png":
-                    mediaType = "image/png";
-                    break;
-                case ".jpg":
-                case ".jpeg":
-                    mediaType = "image/jpeg";
-                    break;
-                case ".gif":
-                    mediaType = "image/gif";
-                    break;
-                default:
-                    mediaType = "application/octet-stream";
-                    break;
-            }
+                ".png" => "image/png",
+                ".jpg" => "image/jpeg",
+                ".jpeg" => "image/jpeg",
+                ".gif" => "image/gif",
+                _ => "application/octet-stream",
+            };
 
             var initResponse = await this.Api.MediaUploadInit(item.Size, mediaType, mediaCategory)
                 .ConfigureAwait(false);
@@ -377,11 +366,17 @@ namespace OpenTween
             var recipient = await this.Api.UsersShow(recipientName)
                 .ConfigureAwait(false);
 
-            await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
+            var response = await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
+                .ConfigureAwait(false);
+
+            var messageEventSingle = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
+
+            await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true)
                 .ConfigureAwait(false);
         }
 
-        public async Task PostRetweet(long id, bool read)
+        public async Task<PostClass?> PostRetweet(long id, bool read)
         {
             this.CheckAccountState();
 
@@ -402,16 +397,16 @@ namespace OpenTween
             lock (LockObj)
             {
                 if (TabInformations.GetInstance().ContainsKey(status.Id))
-                    return;
+                    return null;
             }
 
             //Retweet判定
             if (status.RetweetedStatus == null)
                 throw new WebApiException("Invalid Json!");
 
-            //ReTweetしたものをTLに追加
+            //Retweetしたものを返す
             post = CreatePostsFromStatusData(status);
-            
+
             //ユーザー情報
             post.IsMe = true;
 
@@ -420,7 +415,7 @@ namespace OpenTween
             if (this.ReadOwnPost) post.IsRead = true;
             post.IsDm = false;
 
-            TabInformations.GetInstance().AddPost(post);
+            return post;
         }
 
         public string Username
@@ -445,8 +440,8 @@ namespace OpenTween
             this.FollowersCount = self.FollowersCount;
             this.FriendsCount = self.FriendsCount;
             this.StatusesCount = self.StatusesCount;
-            this.Location = self.Location;
-            this.Bio = self.Description;
+            this.Location = self.Location ?? "";
+            this.Bio = self.Description ?? "";
         }
 
         /// <summary>
@@ -474,23 +469,16 @@ namespace OpenTween
         {
             // 参照: REST APIs - 各endpointのcountパラメータ
             // https://dev.twitter.com/rest/public
-            switch (type)
-            {
-                case MyCommon.WORKERTYPE.Timeline:
-                case MyCommon.WORKERTYPE.Reply:
-                case MyCommon.WORKERTYPE.UserTimeline:
-                case MyCommon.WORKERTYPE.Favorites:
-                case MyCommon.WORKERTYPE.DirectMessegeRcv:
-                case MyCommon.WORKERTYPE.DirectMessegeSnt:
-                case MyCommon.WORKERTYPE.List:  // 不明
-                    return 200;
-
-                case MyCommon.WORKERTYPE.PublicSearch:
-                    return 100;
-
-                default:
-                    throw new InvalidOperationException("Invalid type: " + type);
-            }
+            return type switch
+            {
+                MyCommon.WORKERTYPE.Timeline => 200,
+                MyCommon.WORKERTYPE.Reply => 200,
+                MyCommon.WORKERTYPE.UserTimeline => 200,
+                MyCommon.WORKERTYPE.Favorites => 200,
+                MyCommon.WORKERTYPE.List => 200, // 不明
+                MyCommon.WORKERTYPE.PublicSearch => 100,
+                _ => throw new InvalidOperationException("Invalid type: " + type),
+            };
         }
 
         /// <summary>
@@ -498,12 +486,6 @@ namespace OpenTween
         /// </summary>
         public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
         {
-            if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
-                type == MyCommon.WORKERTYPE.DirectMessegeSnt)
-            {
-                return 20;
-            }
-
             if (SettingManager.Common.UseAdditionalCount)
             {
                 switch (type)
@@ -686,7 +668,7 @@ namespace OpenTween
                 else
                 {
                     //幻覚fav対策
-                    var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+                    var tc = TabInformations.GetInstance().FavoriteTab;
                     post.IsFav = tc.Contains(retweeted.Id);
                 }
 
@@ -741,8 +723,8 @@ namespace OpenTween
                 else
                 {
                     //幻覚fav対策
-                    var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
-                    post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
+                    var tc = TabInformations.GetInstance().FavoriteTab;
+                    post.IsFav = tc.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav;
                 }
 
                 if (status.Coordinates != null)
@@ -767,7 +749,7 @@ namespace OpenTween
                 }
             }
             //HTMLに整形
-            string textFromApi = post.TextFromApi;
+            var textFromApi = post.TextFromApi;
 
             var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink;
 
@@ -810,7 +792,7 @@ namespace OpenTween
             post.Source = string.Intern(sourceText);
             post.SourceUri = sourceUri;
 
-            post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.Item1 == this.UserId);
+            post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == this.UserId);
             post.IsExcludeReply = false;
 
             if (post.IsMe)
@@ -829,12 +811,14 @@ namespace OpenTween
         /// <summary>
         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
         /// </summary>
-        public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities, TwitterQuotedStatusPermalink quotedStatusLink)
+        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.Concat(new[] { quotedStatusLink.Expanded });
+                urls = urls.Append(quotedStatusLink.Expanded);
 
             return GetQuoteTweetStatusIds(urls);
         }
@@ -852,7 +836,7 @@ namespace OpenTween
             }
         }
 
-        private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel tab, bool read)
+        private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read)
         {
             long? minimumId = null;
 
@@ -921,7 +905,7 @@ namespace OpenTween
 
         private long? CreateFavoritePostsFromJson(TwitterStatus[] items, bool read)
         {
-            var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+            var favTab = TabInformations.GetInstance().FavoriteTab;
             long? minimumId = null;
 
             foreach (var status in items)
@@ -971,7 +955,7 @@ namespace OpenTween
         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
         /// </summary>
         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
-        internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
+        internal static PostClass FindTopOfReplyChain(IDictionary<long, PostClass> posts, long startStatusId)
         {
             if (!posts.ContainsKey(startStatusId))
                 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
@@ -990,7 +974,7 @@ namespace OpenTween
         public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab)
         {
             var targetPost = tab.TargetPost;
-            var relPosts = new Dictionary<Int64, PostClass>();
+            var relPosts = new Dictionary<long, PostClass>();
             if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null)
             {
                 //検索結果対応
@@ -1008,7 +992,7 @@ namespace OpenTween
             }
             relPosts.Add(targetPost.StatusId, targetPost);
 
-            Exception lastException = null;
+            Exception? lastException = null;
 
             // in_reply_to_status_id を使用してリプライチェインを辿る
             var nextPost = FindTopOfReplyChain(relPosts, targetPost.StatusId);
@@ -1043,7 +1027,7 @@ namespace OpenTween
                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
             foreach (var _match in ma)
             {
-                if (Int64.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
+                if (long.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
                 {
                     if (relPosts.ContainsKey(_statusId))
                         continue;
@@ -1117,22 +1101,11 @@ namespace OpenTween
                 try
                 {
                     post.StatusId = message.Id;
-                    if (gType != MyCommon.WORKERTYPE.UserStream)
-                    {
-                        if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
-                        {
-                            if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
-                        }
-                        else
-                        {
-                            if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
-                        }
-                    }
 
                     //二重取得回避
                     lock (LockObj)
                     {
-                        if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
+                        if (TabInformations.GetInstance().DirectMessageTab.Contains(post.StatusId)) continue;
                     }
                     //sender_id
                     //recipient_id
@@ -1159,10 +1132,10 @@ namespace OpenTween
                         .ToArray();
 
                     //以下、ユーザー情報
-                    TwitterUser user;
+                    TwitterUser? user;
                     if (gType == MyCommon.WORKERTYPE.UserStream)
                     {
-                        if (this.Api.CurrentUserId == message.Recipient.Id)
+                        if (this.Api.CurrentUserId == message.Recipient?.Id)
                         {
                             user = message.Sender;
                             post.IsMe = false;
@@ -1191,11 +1164,21 @@ namespace OpenTween
                         }
                     }
 
-                    post.UserId = user.Id;
-                    post.ScreenName = user.ScreenName;
-                    post.Nickname = user.Name.Trim();
-                    post.ImageUrl = user.ProfileImageUrlHttps;
-                    post.IsProtect = user.Protected;
+                    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";
+                    }
 
                     // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
                     if (post.Text == post.TextFromApi)
@@ -1221,47 +1204,165 @@ namespace OpenTween
                 post.IsExcludeReply = false;
                 post.IsDm = true;
 
-                var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
+                var dmTab = TabInformations.GetInstance().DirectMessageTab;
                 dmTab.AddPostQueue(post);
             }
         }
 
-        public async Task GetDirectMessageApi(bool read, MyCommon.WORKERTYPE gType, bool more)
+        public async Task GetDirectMessageEvents(bool read, bool backward)
         {
             this.CheckAccountState();
             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
-            var count = GetApiResultCount(gType, more, false);
+            var count = 50;
 
-            TwitterDirectMessage[] messages;
-            if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
+            TwitterMessageEventList eventList;
+            if (backward)
             {
-                if (more)
-                {
-                    messages = await this.Api.DirectMessagesRecv(count, maxId: this.minDirectmessage)
-                        .ConfigureAwait(false);
-                }
-                else
-                {
-                    messages = await this.Api.DirectMessagesRecv(count)
-                        .ConfigureAwait(false);
-                }
+                eventList = await this.Api.DirectMessagesEventsList(count, this.nextCursorDirectMessage)
+                    .ConfigureAwait(false);
             }
             else
             {
-                if (more)
+                eventList = await this.Api.DirectMessagesEventsList(count)
+                    .ConfigureAwait(false);
+            }
+
+            this.nextCursorDirectMessage = eventList.NextCursor;
+
+            await this.CreateDirectMessagesEventFromJson(eventList, read)
+                .ConfigureAwait(false);
+        }
+
+        private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventSingle eventSingle, bool read)
+        {
+            var eventList = new TwitterMessageEventList
+            {
+                Apps = new Dictionary<string, TwitterMessageEventList.App>(),
+                Events = new[] { eventSingle.Event },
+            };
+
+            await this.CreateDirectMessagesEventFromJson(eventList, read)
+                .ConfigureAwait(false);
+        }
+
+        private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventList eventList, bool read)
+        {
+            var events = eventList.Events
+                .Where(x => x.Type == "message_create")
+                .ToArray();
+
+            if (events.Length == 0)
+                return;
+
+            var userIds = Enumerable.Concat(
+                events.Select(x => x.MessageCreate.SenderId),
+                events.Select(x => x.MessageCreate.Target.RecipientId)
+            ).Distinct().ToArray();
+
+            var users = (await this.Api.UsersLookup(userIds).ConfigureAwait(false))
+                .ToDictionary(x => x.IdStr);
+
+            var apps = eventList.Apps ?? new Dictionary<string, TwitterMessageEventList.App>();
+
+            this.CreateDirectMessagesEventFromJson(events, users, apps, read);
+        }
+
+        private void CreateDirectMessagesEventFromJson(IEnumerable<TwitterMessageEvent> events, IReadOnlyDictionary<string, TwitterUser> users,
+            IReadOnlyDictionary<string, TwitterMessageEventList.App> apps, bool read)
+        {
+            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))
                 {
-                    messages = await this.Api.DirectMessagesSent(count, maxId: this.minDirectmessageSent)
-                        .ConfigureAwait(false);
+                    userId = eventItem.MessageCreate.SenderId;
+                    post.IsMe = false;
+                    post.IsOwl = true;
                 }
                 else
                 {
-                    messages = await this.Api.DirectMessagesSent(count)
-                        .ConfigureAwait(false);
+                    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) { }
                 }
-            }
 
-            CreateDirectMessagesFromJson(messages, gType, read);
+                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);
+            }
         }
 
         public async Task GetFavoritesApi(bool read, FavoritesTabModel tab, bool backward)
@@ -1288,7 +1389,7 @@ namespace OpenTween
                 tab.OldestId = minimumId.Value;
         }
 
-        private string ReplaceTextFromApi(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
+        private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
         {
             if (entities != null)
             {
@@ -1314,7 +1415,7 @@ namespace OpenTween
             return text;
         }
 
-        internal static string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quotedStatus, TwitterQuotedStatusPermalink quotedStatusLink)
+        internal static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink)
         {
             if (entities == null)
                 return text;
@@ -1354,7 +1455,7 @@ namespace OpenTween
                 }
             }
 
-            if (quotedStatusLink != null)
+            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);
@@ -1372,7 +1473,7 @@ namespace OpenTween
             if (MyCommon._endingFlag) return;
 
             var cursor = -1L;
-            var newFollowerIds = new HashSet<long>();
+            var newFollowerIds = Enumerable.Empty<long>();
             do
             {
                 var ret = await this.Api.FollowersIds(cursor)
@@ -1381,11 +1482,11 @@ namespace OpenTween
                 if (ret.Ids == null)
                     throw new WebApiException("ret.ids == null");
 
-                newFollowerIds.UnionWith(ret.Ids);
+                newFollowerIds = newFollowerIds.Concat(ret.Ids);
                 cursor = ret.NextCursor;
             } while (cursor != 0);
 
-            this.followerId = newFollowerIds;
+            this.followerId = newFollowerIds.ToHashSet();
             TabInformations.GetInstance().RefreshOwl(this.followerId);
 
             this.GetFollowersSuccess = true;
@@ -1496,13 +1597,13 @@ namespace OpenTween
                 return true;
             }
             catch (TwitterApiException ex)
-                when (ex.ErrorResponse.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
+                when (ex.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
             {
                 return false;
             }
         }
 
-        private void ExtractEntities(TwitterEntities entities, List<Tuple<long, string>> AtList, List<MediaInfo> media)
+        private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> AtList, List<MediaInfo> media)
         {
             if (entities != null)
             {
@@ -1517,7 +1618,7 @@ namespace OpenTween
                 {
                     foreach (var ent in entities.UserMentions)
                     {
-                        AtList.Add(Tuple.Create(ent.Id, ent.ScreenName));
+                        AtList.Add((ent.Id, ent.ScreenName));
                     }
                 }
                 if (entities.Media != null)
@@ -1531,10 +1632,6 @@ namespace OpenTween
                                 if (ent.VideoInfo != null &&
                                     ent.Type == "animated_gif" || ent.Type == "video")
                                 {
-                                    //var videoUrl = ent.VideoInfo.Variants
-                                    //    .Where(v => v.ContentType == "video/mp4")
-                                    //    .OrderByDescending(v => v.Bitrate)
-                                    //    .Select(v => v.Url).FirstOrDefault();
                                     media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
                                 }
                                 else
@@ -1546,12 +1643,14 @@ namespace OpenTween
             }
         }
 
-        internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
+        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, entities, keepTco: true);
+            text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true);
 
-            text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
+            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)
@@ -1569,13 +1668,13 @@ namespace OpenTween
         /// <summary>
         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
         /// </summary>
-        internal static (string SourceText, Uri SourceUri) ParseSource(string sourceHtml)
+        internal static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml)
         {
             if (string.IsNullOrEmpty(sourceHtml))
                 return ("", null);
 
             string sourceText;
-            Uri sourceUri;
+            Uri? sourceUri;
 
             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
 
@@ -1602,7 +1701,7 @@ namespace OpenTween
             return (sourceText, sourceUri);
         }
 
-        public async Task<TwitterApiStatus> GetInfoApi()
+        public async Task<TwitterApiStatus?> GetInfoApi()
         {
             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
 
@@ -1625,19 +1724,20 @@ namespace OpenTween
             if (MyCommon._endingFlag) return;
 
             var cursor = -1L;
-            var newBlockIds = new HashSet<long>();
+            var newBlockIds = Enumerable.Empty<long>();
             do
             {
                 var ret = await this.Api.BlocksIds(cursor)
                     .ConfigureAwait(false);
 
-                newBlockIds.UnionWith(ret.Ids);
+                newBlockIds = newBlockIds.Concat(ret.Ids);
                 cursor = ret.NextCursor;
             } while (cursor != 0);
 
-            newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
+            var blockIdsSet = newBlockIds.ToHashSet();
+            blockIdsSet.Remove(this.UserId); // 元のソースにあったので一応残しておく
 
-            TabInformations.GetInstance().BlockIds = newBlockIds;
+            TabInformations.GetInstance().BlockIds = blockIdsSet;
         }
 
         /// <summary>
@@ -1651,7 +1751,7 @@ namespace OpenTween
             var ids = await TwitterIds.GetAllItemsAsync(x => this.Api.MutesUsersIds(x))
                 .ConfigureAwait(false);
 
-            TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
+            TabInformations.GetInstance().MuteUserIds = ids.ToHashSet();
         }
 
         public string[] GetHashList()
@@ -1725,41 +1825,46 @@ namespace OpenTween
             var config = this.TextConfiguration;
             var totalWeight = 0;
 
+            int GetWeightFromCodepoint(int codepoint)
+            {
+                foreach (var weightRange in config.Ranges)
+                {
+                    if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
+                        return weightRange.Weight;
+                }
+
+                return config.DefaultWeight;
+            }
+
             var urls = TweetExtractor.ExtractUrlEntities(postText).ToArray();
+            var emojis = config.EmojiParsingEnabled
+                ? TweetExtractor.ExtractEmojiEntities(postText).ToArray()
+                : Array.Empty<TwitterEntityEmoji>();
 
-            var pos = 0;
-            while (pos < postText.Length)
+            var codepoints = postText.ToCodepoints().ToArray();
+            var index = 0;
+            while (index < codepoints.Length)
             {
-                var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == pos);
+                var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == index);
                 if (urlEntity != null)
                 {
                     totalWeight += config.TransformedURLLength * config.Scale;
-
-                    var urlLength = urlEntity.Indices[1] - urlEntity.Indices[0];
-                    pos += urlLength;
-
+                    index = urlEntity.Indices[1];
                     continue;
                 }
 
-                var codepoint = postText.GetCodepointAtSafe(pos);
-                var weight = config.DefaultWeight;
-
-                foreach (var weightRange in config.Ranges)
+                var emojiEntity = emojis.FirstOrDefault(x => x.Indices[0] == index);
+                if (emojiEntity != null)
                 {
-                    if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
-                    {
-                        weight = weightRange.Weight;
-                        break;
-                    }
+                    totalWeight += GetWeightFromCodepoint(codepoints[index]);
+                    index = emojiEntity.Indices[1];
+                    continue;
                 }
 
-                totalWeight += weight;
+                var codepoint = codepoints[index];
+                totalWeight += GetWeightFromCodepoint(codepoint);
 
-                var isSurrogatePair = codepoint > 0xffff;
-                if (isSurrogatePair)
-                    pos += 2; // サロゲートペアの場合は2文字分進める
-                else
-                    pos++;
+                index++;
             }
 
             var remainWeight = config.MaxWeightedTweetLength * config.Scale - totalWeight;
@@ -1778,16 +1883,16 @@ namespace OpenTween
         public event EventHandler<PostDeletedEventArgs> PostDeleted;
         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
         private DateTimeUtc _lastUserstreamDataReceived;
-        private StreamAutoConnector userStreamConnector;
+        private StreamAutoConnector? userStreamConnector;
 
         public class FormattedEvent
         {
             public MyCommon.EVENTTYPE Eventtype { get; set; }
             public DateTimeUtc CreatedAt { get; set; }
-            public string Event { get; set; }
-            public string Username { get; set; }
-            public string Target { get; set; }
-            public Int64 Id { get; set; }
+            public string Event { get; set; } = "";
+            public string Username { get; set; } = "";
+            public string Target { get; set; } = "";
+            public long Id { get; set; }
             public bool IsMe { get; set; }
         }
 
@@ -1888,6 +1993,9 @@ namespace OpenTween
         /// </summary>
         private FormattedEvent CreateEventFromRetweet(TwitterStatus status)
         {
+            if (status.RetweetedStatus == null)
+                throw new InvalidOperationException();
+
             return new FormattedEvent
             {
                 Eventtype = MyCommon.EVENTTYPE.Retweet,
@@ -1965,7 +2073,7 @@ namespace OpenTween
 
                     if (eventData.Event == "favorite")
                     {
-                        var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
+                        var favTab = tabinfo.FavoriteTab;
                         favTab.AddPostQueue(post);
 
                         if (tweetEvent.Source.Id == this.UserId)
@@ -2099,11 +2207,11 @@ namespace OpenTween
             public bool IsStreamActive { get; private set; }
             public bool IsDisposed { get; private set; }
 
-            public event Action<ITwitterStreamMessage> MessageReceived;
-            public event Action Stopped;
-            public event Action Started;
+            public event Action<ITwitterStreamMessage>? MessageReceived;
+            public event Action? Stopped;
+            public event Action? Started;
 
-            private Task streamTask;
+            private Task? streamTask;
             private CancellationTokenSource streamCts = new CancellationTokenSource();
 
             public StreamAutoConnector(TwitterStreamObservable streamObservable)
@@ -2136,7 +2244,7 @@ namespace OpenTween
 
             private async Task StreamLoop(CancellationToken cancellationToken)
             {
-                TimeSpan sleep = TimeSpan.Zero;
+                var sleep = TimeSpan.Zero;
                 for (; ; )
                 {
                     if (sleep != TimeSpan.Zero)
@@ -2224,13 +2332,6 @@ namespace OpenTween
             this.disposedValue = true;
         }
 
-        //protected Overrides void Finalize()
-        //{
-        //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
-        //    Dispose(false)
-        //    MyBase.Finalize()
-        //}
-
         // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
         public void Dispose()
         {