OSDN Git Service

usingの順序を揃える (SA1208, SA1210)
[opentween/open-tween.git] / OpenTween / Twitter.cs
index 02a713f..f2c31cd 100644 (file)
 // 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.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Reflection;
 using System.Runtime.CompilerServices;
-using System.Runtime.Serialization;
-using System.Runtime.Serialization.Json;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Web;
-using System.Xml;
-using System.Xml.Linq;
-using System.Xml.XPath;
-using System;
-using System.Reflection;
-using System.Collections.Generic;
-using System.Drawing;
 using System.Windows.Forms;
 using OpenTween.Api;
 using OpenTween.Api.DataModel;
 using OpenTween.Connection;
 using OpenTween.Models;
-using System.Drawing.Imaging;
 using OpenTween.Setting;
 
 namespace OpenTween
@@ -76,17 +71,16 @@ namespace OpenTween
         //   implied. See the License for the specific language governing
         //   permissions and limitations under the License.
 
-        //Hashtag用正規表現
+        // 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 + "]";
-        private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
-        private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
+        private const string HASHTAG_ALPHA = "[A-Za-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
+        private const string HASHTAG_ALPHANUMERIC = "[A-Za-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
+        private const string HASHTAG_TERMINATOR = "[^A-Za-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
         public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
-        //URL正規表現
+        // URL正規表現
         private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
         public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
         private const string url_invalid_domain_chars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e";
@@ -135,21 +129,25 @@ namespace OpenTween
         /// <summary>
         /// attachment_url に指定可能な URL を判定する正規表現
         /// </summary>
-        public static readonly Regex AttachmentUrlRegex = new Regex(@"https?://(
+        public static readonly Regex AttachmentUrlRegex = new Regex(
+            @"https?://(
    twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
  | mobile\.twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
  | twitter\.com/messages/compose\?recipient_id=[0-9]+(&.+)?
-)$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+)$",
+            RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
 
         /// <summary>
         /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
         /// </summary>
-        public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
+        public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(
+            @"https?://(?:[^.]+\.)?(?:
   favstar\.fm/users/[a-zA-Z0-9_]+/status/       # Favstar
 | favstar\.fm/t/                                # Favstar (short)
 | aclog\.koba789\.com/i/                        # aclog
 | frtrt\.net/solo_status\.php\?status=          # RtRT
-)(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+)(?<StatusId>[0-9]+)",
+            RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
 
         /// <summary>
         /// DM送信かどうかを判定する正規表現
@@ -160,33 +158,21 @@ namespace OpenTween
         public TwitterConfiguration Configuration { get; private set; }
         public TwitterTextConfiguration TextConfiguration { get; private set; }
 
+        public bool GetFollowersSuccess { get; private set; } = false;
+        public bool GetNoRetweetSuccess { get; private set; } = false;
+
         delegate void GetIconImageDelegate(PostClass post);
         private readonly object LockObj = new object();
         private ISet<long> followerId = new HashSet<long>();
-        private bool _GetFollowerResult = false;
-        private long[] noRTId = new long[0];
-        private bool _GetNoRetweetResult = false;
+        private long[] noRTId = Array.Empty<long>();
 
-        //プロパティからアクセスされる共通情報
-        private string _uname;
+        // プロパティからアクセスされる共通情報
+        private readonly List<string> _hashList = new List<string>();
 
-        private bool _readOwnPost;
-        private 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())
-        {
-        }
-
         public Twitter(TwitterApi api)
         {
             this.Api = api;
@@ -195,17 +181,10 @@ namespace OpenTween
         }
 
         public TwitterApiAccessLevel AccessLevel
-        {
-            get
-            {
-                return MyCommon.TwitterApiInfo.AccessLevel;
-            }
-        }
+            => MyCommon.TwitterApiInfo.AccessLevel;
 
         protected void ResetApiStatus()
-        {
-            MyCommon.TwitterApiInfo.Reset();
-        }
+            => MyCommon.TwitterApiInfo.Reset();
 
         public void ClearAuthInfo()
         {
@@ -213,7 +192,6 @@ namespace OpenTween
             this.ResetApiStatus();
         }
 
-        [Obsolete]
         public void VerifyCredentials()
         {
             try
@@ -236,34 +214,30 @@ namespace OpenTween
 
         public void Initialize(string token, string tokenSecret, string username, long userId)
         {
-            //OAuth認証
-            if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
+            // OAuth認証
+            if (MyCommon.IsNullOrEmpty(token) || MyCommon.IsNullOrEmpty(tokenSecret) || MyCommon.IsNullOrEmpty(username))
             {
                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
             }
             this.ResetApiStatus();
             this.Api.Initialize(token, tokenSecret, userId, username);
-            _uname = username.ToLowerInvariant();
-            if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream();
         }
 
-        public string PreProcessUrl(string orgData)
+        internal static string PreProcessUrl(string orgData)
         {
             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)
@@ -287,19 +261,27 @@ namespace OpenTween
             return orgData;
         }
 
-        public async Task PostStatus(PostStatusParams param)
+        public async Task<PostClass?> PostStatus(PostStatusParams param)
         {
             this.CheckAccountState();
 
             if (Twitter.DMSendTextRegex.IsMatch(param.Text))
             {
-                await this.SendDirectMessage(param.Text)
+                var mediaId = param.MediaIds != null && param.MediaIds.Any() ? param.MediaIds[0] : (long?)null;
+
+                await this.SendDirectMessage(param.Text, mediaId)
                     .ConfigureAwait(false);
-                return;
+                return null;
             }
 
-            var response = await this.Api.StatusesUpdate(param.Text, param.InReplyToStatusId, param.MediaIds,
-                    param.AutoPopulateReplyMetadata, param.ExcludeReplyUserIds, param.AttachmentUrl)
+            var response = await this.Api.StatusesUpdate(
+                    param.Text,
+                    param.InReplyToStatusId,
+                    param.MediaIds,
+                    param.AutoPopulateReplyMetadata,
+                    param.ExcludeReplyUserIds,
+                    param.AttachmentUrl
+                )
                 .ConfigureAwait(false);
 
             var status = await response.LoadJsonAsync()
@@ -311,107 +293,103 @@ namespace OpenTween
                 throw new WebApiException("OK:Delaying?");
 
             this.previousStatusId = status.Id;
-        }
 
-        public Task<long> UploadMedia(IMediaItem item)
-            => this.UploadMedia(item, SettingManager.Common.AlphaPNGWorkaround);
+            // 投稿したものを返す
+            var post = this.CreatePostsFromStatusData(status);
+            if (this.ReadOwnPost) post.IsRead = true;
+            return post;
+        }
 
-        public async Task<long> UploadMedia(IMediaItem item, bool alphaPNGWorkaround)
+        public async Task<long> UploadMedia(IMediaItem item, string? mediaCategory = null)
         {
             this.CheckAccountState();
 
-            LazyJson<TwitterUploadMediaResult> response;
-
-            using (var origImage = item.CreateImage())
+            var mediaType = item.Extension switch
             {
-                if (alphaPNGWorkaround && this.AddAlphaChannelIfNeeded(origImage.Image, out var newImage))
-                {
-                    using (var newMediaItem = new MemoryImageMediaItem(newImage))
-                    {
-                        response = await this.Api.MediaUpload(newMediaItem)
-                            .ConfigureAwait(false);
-                    }
-                }
-                else
-                {
-                    response = await this.Api.MediaUpload(item)
-                        .ConfigureAwait(false);
-                }
-            }
+                ".png" => "image/png",
+                ".jpg" => "image/jpeg",
+                ".jpeg" => "image/jpeg",
+                ".gif" => "image/gif",
+                _ => "application/octet-stream",
+            };
 
-            var media = await response.LoadJsonAsync()
+            var initResponse = await this.Api.MediaUploadInit(item.Size, mediaType, mediaCategory)
                 .ConfigureAwait(false);
 
-            return media.MediaId;
-        }
+            var initMedia = await initResponse.LoadJsonAsync()
+                .ConfigureAwait(false);
 
-        /// <summary>
-        /// pic.twitter.com アップロード時に JPEG への変換を回避するための加工を行う
-        /// </summary>
-        /// <remarks>
-        /// pic.twitter.com へのアップロード時に、アルファチャンネルを持たない PNG 画像が
-        /// JPEG 形式に変換され画質が低下する問題を回避します。
-        /// PNG 以外の画像や、すでにアルファチャンネルを持つ PNG 画像に対しては何もしません。
-        /// </remarks>
-        /// <returns>加工が行われた場合は true、そうでない場合は false</returns>
-        private bool AddAlphaChannelIfNeeded(Image origImage, out MemoryImage newImage)
-        {
-            newImage = null;
+            var mediaId = initMedia.MediaId;
 
-            // PNG 画像以外に対しては何もしない
-            if (origImage.RawFormat.Guid != ImageFormat.Png.Guid)
-                return false;
+            await this.Api.MediaUploadAppend(mediaId, 0, item)
+                .ConfigureAwait(false);
+
+            var response = await this.Api.MediaUploadFinalize(mediaId)
+                .ConfigureAwait(false);
 
-            using (var bitmap = new Bitmap(origImage))
+            var media = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
+
+            while (media.ProcessingInfo is TwitterUploadMediaResult.MediaProcessingInfo processingInfo)
             {
-                // アルファ値が 255 以外のピクセルが含まれていた場合は何もしない
-                foreach (var x in Enumerable.Range(0, bitmap.Width))
+                switch (processingInfo.State)
                 {
-                    foreach (var y in Enumerable.Range(0, bitmap.Height))
-                    {
-                        if (bitmap.GetPixel(x, y).A != 255)
-                            return false;
-                    }
+                    case "pending":
+                        break;
+                    case "in_progress":
+                        break;
+                    case "succeeded":
+                        goto succeeded;
+                    case "failed":
+                        throw new WebApiException($"Err:Upload failed ({processingInfo.Error?.Name})");
+                    default:
+                        throw new WebApiException($"Err:Invalid state ({processingInfo.State})");
                 }
 
-                // 左上の 1px だけアルファ値を 254 にする
-                var pixel = bitmap.GetPixel(0, 0);
-                var newPixel = Color.FromArgb(pixel.A - 1, pixel.R, pixel.G, pixel.B);
-                bitmap.SetPixel(0, 0, newPixel);
-
-                // MemoryImage 作成時に画像はコピーされるため、この後 bitmap は破棄しても問題ない
-                newImage = MemoryImage.CopyFromImage(bitmap);
+                await Task.Delay(TimeSpan.FromSeconds(processingInfo.CheckAfterSecs ?? 5))
+                    .ConfigureAwait(false);
 
-                return true;
+                media = await this.Api.MediaUploadStatus(mediaId)
+                    .ConfigureAwait(false);
             }
+
+            succeeded:
+            return media.MediaId;
         }
 
-        public async Task SendDirectMessage(string postStr)
+        public async Task SendDirectMessage(string postStr, long? mediaId = null)
         {
             this.CheckAccountState();
             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
             var mc = Twitter.DMSendTextRegex.Match(postStr);
 
-            var response = await this.Api.DirectMessagesNew(mc.Groups["body"].Value, mc.Groups["id"].Value)
+            var body = mc.Groups["body"].Value;
+            var recipientName = mc.Groups["id"].Value;
+
+            var recipient = await this.Api.UsersShow(recipientName)
+                .ConfigureAwait(false);
+
+            var response = await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
                 .ConfigureAwait(false);
 
-            var dm = await response.LoadJsonAsync()
+            var messageEventSingle = await response.LoadJsonAsync()
                 .ConfigureAwait(false);
 
-            this.UpdateUserStats(dm.Sender);
+            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();
 
-            //データ部分の生成
+            // データ部分の生成
             var post = TabInformations.GetInstance()[id];
             if (post == null)
                 throw new WebApiException("Err:Target isn't found.");
 
-            var target = post.RetweetedId ?? id;  //再RTの場合は元発言をRT
+            var target = post.RetweetedId ?? id;  // 再RTの場合は元発言をRT
 
             var response = await this.Api.StatusesRetweet(target)
                 .ConfigureAwait(false);
@@ -419,29 +397,29 @@ namespace OpenTween
             var status = await response.LoadJsonAsync()
                 .ConfigureAwait(false);
 
-            //二重取得回避
-            lock (LockObj)
+            // 二重取得回避
+            lock (this.LockObj)
             {
                 if (TabInformations.GetInstance().ContainsKey(status.Id))
-                    return;
+                    return null;
             }
 
-            //Retweet判定
+            // Retweet判定
             if (status.RetweetedStatus == null)
                 throw new WebApiException("Invalid Json!");
 
-            //ReTweetしたものをTLに追加
-            post = CreatePostsFromStatusData(status);
-            
-            //ユーザー情報
+            // Retweetしたものを返す
+            post = this.CreatePostsFromStatusData(status);
+
+            // ユーザー情報
             post.IsMe = true;
 
             post.IsRead = read;
             post.IsOwl = false;
-            if (_readOwnPost) post.IsRead = true;
+            if (this.ReadOwnPost) post.IsRead = true;
             post.IsDm = false;
 
-            TabInformations.GetInstance().AddPost(post);
+            return post;
         }
 
         public string Username
@@ -450,32 +428,9 @@ namespace OpenTween
         public long UserId
             => this.Api.CurrentUserId;
 
-        private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
-        public static MyCommon.ACCOUNT_STATE AccountState
-        {
-            get
-            {
-                return _accountState;
-            }
-            set
-            {
-                _accountState = value;
-            }
-        }
-
+        public static MyCommon.ACCOUNT_STATE AccountState { get; set; } = MyCommon.ACCOUNT_STATE.Valid;
         public bool RestrictFavCheck { get; set; }
-
-        public bool ReadOwnPost
-        {
-            get
-            {
-                return _readOwnPost;
-            }
-            set
-            {
-                _readOwnPost = value;
-            }
-        }
+        public bool ReadOwnPost { get; set; }
 
         public int FollowersCount { get; private set; }
         public int FriendsCount { get; private set; }
@@ -489,33 +444,27 @@ 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>
         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
-        {
-            return count >= 20 && count <= GetMaxApiResultCount(type);
-        }
+            => count >= 20 && count <= GetMaxApiResultCount(type);
 
         /// <summary>
         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyMoreApiResultCount(int count)
-        {
-            return count >= 20 && count <= 200;
-        }
+            => count >= 20 && count <= 200;
 
         /// <summary>
         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyFirstApiResultCount(int count)
-        {
-            return count >= 20 && count <= 200;
-        }
+            => count >= 20 && count <= 200;
 
         /// <summary>
         /// WORKERTYPEに応じた取得可能な最大件数を取得する
@@ -524,23 +473,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>
@@ -548,12 +490,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)
@@ -647,10 +583,10 @@ namespace OpenTween
             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
 
             TwitterStatus[] statuses;
-            if (string.IsNullOrEmpty(userName))
+            if (MyCommon.IsNullOrEmpty(userName))
             {
                 var target = tab.ScreenName;
-                if (string.IsNullOrEmpty(target)) return;
+                if (MyCommon.IsNullOrEmpty(target)) return;
                 userName = target;
                 statuses = await this.Api.StatusesUserTimeline(userName, count)
                     .ConfigureAwait(false);
@@ -669,7 +605,7 @@ namespace OpenTween
                 }
             }
 
-            var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
+            var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
 
             if (minimumId != null)
                 tab.OldestId = minimumId.Value;
@@ -682,10 +618,10 @@ namespace OpenTween
             var status = await this.Api.StatusesShow(id)
                 .ConfigureAwait(false);
 
-            var item = CreatePostsFromStatusData(status);
+            var item = this.CreatePostsFromStatusData(status);
 
             item.IsRead = read;
-            if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
+            if (item.IsMe && !read && this.ReadOwnPost) item.IsRead = true;
 
             return item;
         }
@@ -695,7 +631,7 @@ namespace OpenTween
             var post = await this.GetStatusApi(read, id)
                 .ConfigureAwait(false);
 
-            //非同期アイコン取得&StatusDictionaryに追加
+            // 非同期アイコン取得&StatusDictionaryに追加
             if (tab != null && tab.IsInnerStorageTabType)
                 tab.AddPostQueue(post);
             else
@@ -703,9 +639,7 @@ namespace OpenTween
         }
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status)
-        {
-            return CreatePostsFromStatusData(status, false);
-        }
+            => this.CreatePostsFromStatusData(status, false);
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
         {
@@ -720,13 +654,13 @@ namespace OpenTween
 
                 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
 
-                //Id
+                // Id
                 post.RetweetedId = retweeted.Id;
-                //本文
+                // 本文
                 post.TextFromApi = retweeted.FullText;
                 entities = retweeted.MergedEntities;
                 sourceHtml = retweeted.Source;
-                //Reply先
+                // Reply先
                 post.InReplyToStatusId = retweeted.InReplyToStatusId;
                 post.InReplyToUser = retweeted.InReplyToScreenName;
                 post.InReplyToUserId = status.InReplyToUserId;
@@ -737,15 +671,15 @@ namespace OpenTween
                 }
                 else
                 {
-                    //幻覚fav対策
-                    var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+                    // 幻覚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)
                 {
@@ -762,12 +696,12 @@ namespace OpenTween
                     post.Nickname = "Unknown User";
                 }
 
-                //Retweetした人
+                // Retweetした人
                 if (status.User != null)
                 {
                     post.RetweetedBy = status.User.ScreenName;
                     post.RetweetedByUserId = status.User.Id;
-                    post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+                    post.IsMe = post.RetweetedByUserId == this.UserId;
                 }
                 else
                 {
@@ -778,7 +712,7 @@ namespace OpenTween
             else
             {
                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
-                //本文
+                // 本文
                 post.TextFromApi = status.FullText;
                 entities = status.MergedEntities;
                 sourceHtml = status.Source;
@@ -792,15 +726,15 @@ namespace OpenTween
                 }
                 else
                 {
-                    //幻覚fav対策
-                    var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
-                    post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
+                    // 幻覚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)
                 {
@@ -809,7 +743,7 @@ namespace OpenTween
                     post.Nickname = user.Name.Trim();
                     post.ImageUrl = user.ProfileImageUrlHttps;
                     post.IsProtect = user.Protected;
-                    post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+                    post.IsMe = post.UserId == this.UserId;
                 }
                 else
                 {
@@ -818,18 +752,26 @@ namespace OpenTween
                     post.Nickname = "Unknown User";
                 }
             }
-            //HTMLに整形
-            string textFromApi = post.TextFromApi;
-            post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
+            // 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);
+            post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink);
             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
-            post.AccessibleText = this.CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus);
+            post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink);
             post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
             post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
 
-            post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
+            this.ExtractEntities(entities, post.ReplyToList, post.Media);
+
+            post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink)
                 .Where(x => x != post.StatusId && x != post.RetweetedId)
                 .Distinct().ToArray();
 
@@ -849,12 +791,12 @@ namespace OpenTween
             post.ImageUrl = string.Intern(post.ImageUrl);
             post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
 
-            //Source整形
+            // Source整形
             var (sourceText, sourceUri) = ParseSource(sourceHtml);
             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)
@@ -863,7 +805,7 @@ namespace OpenTween
             }
             else
             {
-                if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
+                if (this.followerId.Count > 0) post.IsOwl = !this.followerId.Contains(post.UserId);
             }
 
             post.IsDm = false;
@@ -873,10 +815,15 @@ namespace OpenTween
         /// <summary>
         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
         /// </summary>
-        public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
+        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);
         }
 
@@ -893,7 +840,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;
 
@@ -902,8 +849,8 @@ namespace OpenTween
                 if (minimumId == null || minimumId.Value > status.Id)
                     minimumId = status.Id;
 
-                //二重取得回避
-                lock (LockObj)
+                // 二重取得回避
+                lock (this.LockObj)
                 {
                     if (tab == null)
                     {
@@ -915,14 +862,14 @@ namespace OpenTween
                     }
                 }
 
-                //RT禁止ユーザーによるもの
+                // RT禁止ユーザーによるもの
                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
                     status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue;
 
-                var post = CreatePostsFromStatusData(status);
+                var post = this.CreatePostsFromStatusData(status);
 
                 post.IsRead = read;
-                if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
+                if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
 
                 if (tab != null && tab.IsInnerStorageTabType)
                     tab.AddPostQueue(post);
@@ -943,16 +890,16 @@ namespace OpenTween
                     minimumId = status.Id;
 
                 if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id;
-                //二重取得回避
-                lock (LockObj)
+                // 二重取得回避
+                lock (this.LockObj)
                 {
                     if (tab.Contains(status.Id)) continue;
                 }
 
-                var post = CreatePostsFromStatusData(status);
+                var post = this.CreatePostsFromStatusData(status);
 
                 post.IsRead = read;
-                if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
+                if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true;
 
                 tab.AddPostQueue(post);
             }
@@ -962,7 +909,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)
@@ -970,13 +917,13 @@ namespace OpenTween
                 if (minimumId == null || minimumId.Value > status.Id)
                     minimumId = status.Id;
 
-                //二重取得回避
-                lock (LockObj)
+                // 二重取得回避
+                lock (this.LockObj)
                 {
                     if (favTab.Contains(status.Id)) continue;
                 }
 
-                var post = CreatePostsFromStatusData(status, true);
+                var post = this.CreatePostsFromStatusData(status, true);
 
                 post.IsRead = read;
 
@@ -1002,7 +949,7 @@ namespace OpenTween
                     .ConfigureAwait(false);
             }
 
-            var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
+            var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
 
             if (minimumId != null)
                 tab.OldestId = minimumId.Value;
@@ -1012,7 +959,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));
@@ -1031,10 +978,10 @@ 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)
             {
-                //検索結果対応
+                // 検索結果対応
                 var p = TabInformations.GetInstance()[targetPost.StatusId];
                 if (p != null && p.InReplyToStatusId != null)
                 {
@@ -1049,7 +996,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);
@@ -1078,13 +1025,13 @@ namespace OpenTween
                 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
             }
 
-            //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
+            // MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
             var text = targetPost.Text;
             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
                 .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;
@@ -1111,7 +1058,7 @@ namespace OpenTween
 
             relPosts.Values.ToList().ForEach(p =>
             {
-                if (p.IsMe && !read && this._readOwnPost)
+                if (p.IsMe && !read && this.ReadOwnPost)
                     p.IsRead = true;
                 else
                     p.IsRead = read;
@@ -1150,156 +1097,163 @@ namespace OpenTween
                 tab.OldestId = minimumId.Value;
         }
 
-        private void CreateDirectMessagesFromJson(TwitterDirectMessage[] item, MyCommon.WORKERTYPE gType, bool read)
+        public async Task GetDirectMessageEvents(bool read, bool backward)
         {
-            foreach (var message in item)
-            {
-                var post = new PostClass();
-                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;
-                        }
-                    }
+            this.CheckAccountState();
+            this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
-                    //二重取得回避
-                    lock (LockObj)
-                    {
-                        if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
-                    }
-                    //sender_id
-                    //recipient_id
-                    post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
-                    //本文
-                    var textFromApi = message.Text;
-                    //HTMLに整形
-                    post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
-                    post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
-                    post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
-                    post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
-                    post.AccessibleText = this.CreateAccessibleText(textFromApi, message.Entities, quoteStatus: null);
-                    post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
-                    post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
-                    post.IsFav = false;
-
-                    post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
-
-                    post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
-                        .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
-                        .ToArray();
-
-                    //以下、ユーザー情報
-                    TwitterUser user;
-                    if (gType == MyCommon.WORKERTYPE.UserStream)
-                    {
-                        if (this.Api.CurrentUserId == message.Recipient.Id)
-                        {
-                            user = message.Sender;
-                            post.IsMe = false;
-                            post.IsOwl = true;
-                        }
-                        else
-                        {
-                            user = message.Recipient;
-                            post.IsMe = true;
-                            post.IsOwl = false;
-                        }
-                    }
-                    else
-                    {
-                        if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
-                        {
-                            user = message.Sender;
-                            post.IsMe = false;
-                            post.IsOwl = true;
-                        }
-                        else
-                        {
-                            user = message.Recipient;
-                            post.IsMe = true;
-                            post.IsOwl = false;
-                        }
-                    }
+            var count = 50;
 
-                    post.UserId = user.Id;
-                    post.ScreenName = user.ScreenName;
-                    post.Nickname = user.Name.Trim();
-                    post.ImageUrl = user.ProfileImageUrlHttps;
-                    post.IsProtect = user.Protected;
+            TwitterMessageEventList eventList;
+            if (backward)
+            {
+                eventList = await this.Api.DirectMessagesEventsList(count, this.nextCursorDirectMessage)
+                    .ConfigureAwait(false);
+            }
+            else
+            {
+                eventList = await this.Api.DirectMessagesEventsList(count)
+                    .ConfigureAwait(false);
+            }
 
-                    // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
-                    if (post.Text == post.TextFromApi)
-                        post.Text = post.TextFromApi;
-                    if (post.AccessibleText == post.TextFromApi)
-                        post.AccessibleText = post.TextFromApi;
+            this.nextCursorDirectMessage = eventList.NextCursor;
 
-                    // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
-                    post.ScreenName = string.Intern(post.ScreenName);
-                    post.Nickname = string.Intern(post.Nickname);
-                    post.ImageUrl = string.Intern(post.ImageUrl);
-                }
-                catch(Exception ex)
-                {
-                    MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
-                    MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
-                    continue;
-                }
+            await this.CreateDirectMessagesEventFromJson(eventList, read)
+                .ConfigureAwait(false);
+        }
 
-                post.IsRead = read;
-                if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
-                post.IsReply = false;
-                post.IsExcludeReply = false;
-                post.IsDm = true;
+        private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventSingle eventSingle, bool read)
+        {
+            var eventList = new TwitterMessageEventList
+            {
+                Apps = new Dictionary<string, TwitterMessageEventList.App>(),
+                Events = new[] { eventSingle.Event },
+            };
 
-                var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
-                dmTab.AddPostQueue(post);
-            }
+            await this.CreateDirectMessagesEventFromJson(eventList, read)
+                .ConfigureAwait(false);
         }
 
-        public async Task GetDirectMessageApi(bool read, MyCommon.WORKERTYPE gType, bool more)
+        private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventList eventList, bool read)
         {
-            this.CheckAccountState();
-            this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
+            var events = eventList.Events
+                .Where(x => x.Type == "message_create")
+                .ToArray();
 
-            var count = GetApiResultCount(gType, more, false);
+            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>();
 
-            TwitterDirectMessage[] messages;
-            if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
+            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)
             {
-                if (more)
+                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.DirectMessagesRecv(count, maxId: this.minDirectmessage)
-                        .ConfigureAwait(false);
+                    userId = eventItem.MessageCreate.SenderId;
+                    post.IsMe = false;
+                    post.IsOwl = true;
                 }
                 else
                 {
-                    messages = await this.Api.DirectMessagesRecv(count)
-                        .ConfigureAwait(false);
-                }
-            }
-            else
-            {
-                if (more)
-                {
-                    messages = await this.Api.DirectMessagesSent(count, maxId: this.minDirectmessageSent)
-                        .ConfigureAwait(false);
+                    userId = eventItem.MessageCreate.Target.RecipientId;
+                    post.IsMe = true;
+                    post.IsOwl = false;
                 }
-                else
+
+                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))
                 {
-                    messages = await this.Api.DirectMessagesSent(count)
-                        .ConfigureAwait(false);
+                    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)
@@ -1326,7 +1280,7 @@ namespace OpenTween
                 tab.OldestId = minimumId.Value;
         }
 
-        private string ReplaceTextFromApi(string text, TwitterEntities entities)
+        private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
         {
             if (entities != null)
             {
@@ -1334,21 +1288,25 @@ namespace OpenTween
                 {
                     foreach (var m in entities.Urls)
                     {
-                        if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
+                        if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
                     }
                 }
                 if (entities.Media != null)
                 {
                     foreach (var m in entities.Media)
                     {
-                        if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
+                        if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
                     }
                 }
             }
+
+            if (quotedStatusLink != null)
+                text += " " + quotedStatusLink.Display;
+
             return text;
         }
 
-        private string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quoteStatus)
+        internal static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink)
         {
             if (entities == null)
                 return text;
@@ -1357,19 +1315,19 @@ namespace OpenTween
             {
                 foreach (var entity in entities.Urls)
                 {
-                    if (quoteStatus != null)
+                    if (quotedStatus != null)
                     {
                         var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
-                        if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quoteStatus.IdStr)
+                        if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr)
                         {
-                            var quoteText = this.CreateAccessibleText(quoteStatus.FullText, quoteStatus.MergedEntities, quoteStatus: null);
-                            text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quoteStatus.User.ScreenName, quoteText));
+                            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;
                         }
                     }
-                    else if (!string.IsNullOrEmpty(entity.DisplayUrl))
-                    {
+
+                    if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
                         text = text.Replace(entity.Url, entity.DisplayUrl);
-                    }
                 }
             }
 
@@ -1377,17 +1335,23 @@ namespace OpenTween
             {
                 foreach (var entity in entities.Media)
                 {
-                    if (!string.IsNullOrEmpty(entity.AltText))
+                    if (!MyCommon.IsNullOrEmpty(entity.AltText))
                     {
                         text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
                     }
-                    else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+                    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;
         }
 
@@ -1400,7 +1364,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)
@@ -1409,22 +1373,14 @@ 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._GetFollowerResult = true;
-        }
-
-        public bool GetFollowersSuccess
-        {
-            get
-            {
-                return _GetFollowerResult;
-            }
+            this.GetFollowersSuccess = true;
         }
 
         /// <summary>
@@ -1438,15 +1394,7 @@ namespace OpenTween
             this.noRTId = await this.Api.NoRetweetIds()
                 .ConfigureAwait(false);
 
-            this._GetNoRetweetResult = true;
-        }
-
-        public bool GetNoRetweetSuccess
-        {
-            get
-            {
-                return _GetNoRetweetResult;
-            }
+            this.GetNoRetweetSuccess = true;
         }
 
         /// <summary>
@@ -1540,13 +1488,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;
             }
         }
 
-        public string CreateHtmlAnchor(string text, List<Tuple<long, string>> AtList, TwitterEntities entities, List<MediaInfo> media)
+        private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> AtList, List<MediaInfo> media)
         {
             if (entities != null)
             {
@@ -1561,7 +1509,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)
@@ -1570,30 +1518,38 @@ namespace OpenTween
                     {
                         foreach (var ent in entities.Media)
                         {
-                            if (!media.Any(x => x.Url == ent.MediaUrl))
+                            if (!media.Any(x => x.Url == ent.MediaUrlHttps))
                             {
                                 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.MediaUrl, ent.AltText, ent.ExpandedUrl));
+                                    media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
                                 }
                                 else
-                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
+                                    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, 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 = PreProcessUrl(text); //IDN置換
+            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;
         }
@@ -1603,13 +1559,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))
+            if (MyCommon.IsNullOrEmpty(sourceHtml))
                 return ("", null);
 
             string sourceText;
-            Uri sourceUri;
+            Uri? sourceUri;
 
             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
 
@@ -1636,7 +1592,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;
 
@@ -1659,19 +1615,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>
@@ -1685,16 +1642,16 @@ 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()
         {
             string[] hashArray;
-            lock (LockObj)
+            lock (this.LockObj)
             {
-                hashArray = _hashList.ToArray();
-                _hashList.Clear();
+                hashArray = this._hashList.ToArray();
+                this._hashList.Clear();
             }
             return hashArray;
         }
@@ -1759,41 +1716,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;
@@ -1801,652 +1763,25 @@ namespace OpenTween
             return remainWeight / config.Scale;
         }
 
+        public bool IsDisposed { get; private set; } = false;
 
-#region "UserStream"
-        private string trackWord_ = "";
-        public string TrackWord
+        protected virtual void Dispose(bool disposing)
         {
-            get
-            {
-                return trackWord_;
-            }
-            set
+            if (this.IsDisposed)
+                return;
+
+            if (disposing)
             {
-                trackWord_ = value;
+                this.Api.Dispose();
             }
+
+            this.IsDisposed = true;
         }
-        private bool allAtReply_ = false;
-        public bool AllAtReply
-        {
-            get
-            {
-                return allAtReply_;
-            }
-            set
-            {
-                allAtReply_ = value;
-            }
-        }
-
-        public event EventHandler NewPostFromStream;
-        public event EventHandler UserStreamStarted;
-        public event EventHandler UserStreamStopped;
-        public event EventHandler<PostDeletedEventArgs> PostDeleted;
-        public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
-        private DateTime _lastUserstreamDataReceived;
-        private TwitterUserstream userStream;
-
-        public class FormattedEvent
-        {
-            public MyCommon.EVENTTYPE Eventtype { get; set; }
-            public DateTime CreatedAt { get; set; }
-            public string Event { get; set; }
-            public string Username { get; set; }
-            public string Target { get; set; }
-            public Int64 Id { get; set; }
-            public bool IsMe { get; set; }
-        }
-
-        public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
-        public List<FormattedEvent> StoredEvent
-        {
-            get
-            {
-                return storedEvent_;
-            }
-            set
-            {
-                storedEvent_ = value;
-            }
-        }
-
-        private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
-        {
-            ["favorite"] = MyCommon.EVENTTYPE.Favorite,
-            ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
-            ["follow"] = MyCommon.EVENTTYPE.Follow,
-            ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
-            ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
-            ["block"] = MyCommon.EVENTTYPE.Block,
-            ["unblock"] = MyCommon.EVENTTYPE.Unblock,
-            ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
-            ["deleted"] = MyCommon.EVENTTYPE.Deleted,
-            ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
-            ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
-            ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
-            ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
-            ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
-            ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
-            ["mute"] = MyCommon.EVENTTYPE.Mute,
-            ["unmute"] = MyCommon.EVENTTYPE.Unmute,
-            ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
-        };
-
-        public bool IsUserstreamDataReceived
-        {
-            get
-            {
-                return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
-            }
-        }
-
-        private void userStream_StatusArrived(string line)
-        {
-            this._lastUserstreamDataReceived = DateTime.Now;
-            if (string.IsNullOrEmpty(line)) return;
-
-            if (line.First() != '{' || line.Last() != '}')
-            {
-                MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
-                return;
-            }
-
-            var isDm = false;
-
-            try
-            {
-                using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
-                {
-                    var xElm = XElement.Load(jsonReader);
-                    if (xElm.Element("friends") != null)
-                    {
-                        Debug.WriteLine("friends");
-                        return;
-                    }
-                    else if (xElm.Element("delete") != null)
-                    {
-                        Debug.WriteLine("delete");
-                        Int64 id;
-                        XElement idElm;
-                        if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
-                        {
-                            id = 0;
-                            long.TryParse(idElm.Value, out id);
-
-                            this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
-                        }
-                        else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
-                        {
-                            id = 0;
-                            long.TryParse(idElm.Value, out id);
-
-                            this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
-                        }
-                        else
-                        {
-                            MyCommon.TraceOut("delete:" + line);
-                            return;
-                        }
-                        for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
-                        {
-                            var sEvt = this.StoredEvent[i];
-                            if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
-                            {
-                                this.StoredEvent.RemoveAt(i);
-                            }
-                        }
-                        return;
-                    }
-                    else if (xElm.Element("limit") != null)
-                    {
-                        Debug.WriteLine(line);
-                        return;
-                    }
-                    else if (xElm.Element("event") != null)
-                    {
-                        Debug.WriteLine("event: " + xElm.Element("event").Value);
-                        CreateEventFromJson(line);
-                        return;
-                    }
-                    else if (xElm.Element("direct_message") != null)
-                    {
-                        Debug.WriteLine("direct_message");
-                        isDm = true;
-                    }
-                    else if (xElm.Element("retweeted_status") != null)
-                    {
-                        var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
-                        var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
-
-                        // 自分に関係しないリツイートの場合は無視する
-                        var selfUserId = this.UserId.ToString();
-                        if (sourceUserId == selfUserId || targetUserId == selfUserId)
-                        {
-                            // 公式 RT をイベントとしても扱う
-                            var evt = CreateEventFromRetweet(xElm);
-                            if (evt != null)
-                            {
-                                this.StoredEvent.Insert(0, evt);
-
-                                this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
-                            }
-                        }
-
-                        // 従来通り公式 RT の表示も行うため return しない
-                    }
-                    else if (xElm.Element("scrub_geo") != null)
-                    {
-                        try
-                        {
-                            TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
-                                                                        long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
-                        }
-                        catch(Exception)
-                        {
-                            MyCommon.TraceOut("scrub_geo:" + line);
-                        }
-                        return;
-                    }
-                }
-
-                if (isDm)
-                {
-                    try
-                    {
-                        var message = TwitterStreamEventDirectMessage.ParseJson(line).DirectMessage;
-                        this.CreateDirectMessagesFromJson(new[] { message }, MyCommon.WORKERTYPE.UserStream, false);
-                    }
-                    catch (SerializationException ex)
-                    {
-                        throw TwitterApiException.CreateFromException(ex, line);
-                    }
-                }
-                else
-                {
-                    try
-                    {
-                        var status = TwitterStatusCompat.ParseJson(line);
-                        this.CreatePostsFromJson(new[] { status.Normalize() }, MyCommon.WORKERTYPE.UserStream, null, false);
-                    }
-                    catch (SerializationException ex)
-                    {
-                        throw TwitterApiException.CreateFromException(ex, line);
-                    }
-                }
-            }
-            catch (WebApiException ex)
-            {
-                MyCommon.TraceOut(ex);
-                return;
-            }
-            catch (XmlException)
-            {
-                MyCommon.TraceOut("XmlException (StatusArrived): " + line);
-            }
-            catch(NullReferenceException)
-            {
-                MyCommon.TraceOut("NullRef StatusArrived: " + line);
-            }
-
-            this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
-        }
-
-        /// <summary>
-        /// UserStreamsから受信した公式RTをイベントに変換します
-        /// </summary>
-        private FormattedEvent CreateEventFromRetweet(XElement xElm)
-        {
-            return new FormattedEvent
-            {
-                Eventtype = MyCommon.EVENTTYPE.Retweet,
-                Event = "retweet",
-                CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
-                IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
-                Username = xElm.XPathSelectElement("/user/screen_name").Value,
-                Target = string.Format("@{0}:{1}", new[]
-                {
-                    xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
-                    WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
-                }),
-                Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
-            };
-        }
-
-        private void CreateEventFromJson(string content)
-        {
-            TwitterStreamEvent eventData = null;
-            try
-            {
-                eventData = TwitterStreamEvent.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
-            }
-
-            var evt = new FormattedEvent();
-            evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
-            evt.Event = eventData.Event;
-            evt.Username = eventData.Source.ScreenName;
-            evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
-
-            eventTable.TryGetValue(eventData.Event, out var eventType);
-            evt.Eventtype = eventType;
-
-            TwitterStreamEvent<TwitterStatusCompat> tweetEvent;
-            TwitterStatus tweet;
-
-            switch (eventData.Event)
-            {
-                case "access_revoked":
-                case "access_unrevoked":
-                case "user_delete":
-                case "user_suspend":
-                    return;
-                case "follow":
-                    if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
-                    {
-                        if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
-                    }
-                    else
-                    {
-                        return;    //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
-                    }
-                    evt.Target = "";
-                    break;
-                case "unfollow":
-                    evt.Target = "@" + eventData.Target.ScreenName;
-                    break;
-                case "favorited_retweet":
-                case "retweeted_retweet":
-                    return;
-                case "favorite":
-                case "unfavorite":
-                    tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
-                    tweet = tweetEvent.TargetObject.Normalize();
-                    evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
-                    evt.Id = tweet.Id;
-
-                    if (SettingManager.Common.IsRemoveSameEvent)
-                    {
-                        if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
-                            return;
-                    }
-
-                    var tabinfo = TabInformations.GetInstance();
-
-                    var statusId = tweet.Id;
-                    if (!tabinfo.Posts.TryGetValue(statusId, out var post))
-                        break;
-
-                    if (eventData.Event == "favorite")
-                    {
-                        var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
-                        favTab.AddPostQueue(post);
-
-                        if (tweetEvent.Source.Id == this.UserId)
-                        {
-                            post.IsFav = true;
-                        }
-                        else if (tweetEvent.Target.Id == this.UserId)
-                        {
-                            post.FavoritedCount++;
-
-                            if (SettingManager.Common.FavEventUnread)
-                                tabinfo.SetReadAllTab(post.StatusId, read: false);
-                        }
-                    }
-                    else // unfavorite
-                    {
-                        if (tweetEvent.Source.Id == this.UserId)
-                        {
-                            post.IsFav = false;
-                        }
-                        else if (tweetEvent.Target.Id == this.UserId)
-                        {
-                            post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
-                        }
-                    }
-                    break;
-                case "quoted_tweet":
-                    if (evt.IsMe) return;
-
-                    tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
-                    tweet = tweetEvent.TargetObject.Normalize();
-                    evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
-                    evt.Id = tweet.Id;
 
-                    if (SettingManager.Common.IsRemoveSameEvent)
-                    {
-                        if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
-                            return;
-                    }
-                    break;
-                case "list_member_added":
-                case "list_member_removed":
-                case "list_created":
-                case "list_destroyed":
-                case "list_updated":
-                case "list_user_subscribed":
-                case "list_user_unsubscribed":
-                    var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
-                    evt.Target = listEvent.TargetObject.FullName;
-                    break;
-                case "block":
-                    if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
-                    evt.Target = "";
-                    break;
-                case "unblock":
-                    if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
-                    evt.Target = "";
-                    break;
-                case "user_update":
-                    evt.Target = "";
-                    break;
-                
-                // Mute / Unmute
-                case "mute":
-                    evt.Target = "@" + eventData.Target.ScreenName;
-                    if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
-                    {
-                        TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
-                    }
-                    break;
-                case "unmute":
-                    evt.Target = "@" + eventData.Target.ScreenName;
-                    if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
-                    {
-                        TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
-                    }
-                    break;
-
-                default:
-                    MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
-                    break;
-            }
-            this.StoredEvent.Insert(0, evt);
-
-            this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
-        }
-
-        private void userStream_Started()
-        {
-            this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
-        }
-
-        private void userStream_Stopped()
-        {
-            this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
-        }
-
-        public bool UserStreamActive
-            => this.userStream != null && this.userStream.IsStreamActive;
-
-        public void StartUserStream()
-        {
-            var newStream = new TwitterUserstream(this.Api);
-
-            newStream.StatusArrived += userStream_StatusArrived;
-            newStream.Started += userStream_Started;
-            newStream.Stopped += userStream_Stopped;
-
-            newStream.Start(this.AllAtReply, this.TrackWord);
-
-            var oldStream = Interlocked.Exchange(ref this.userStream, newStream);
-            oldStream?.Dispose();
-        }
-
-        public void StopUserStream()
-        {
-            var oldStream = Interlocked.Exchange(ref this.userStream, null);
-            oldStream?.Dispose();
-        }
-
-        public void ReconnectUserStream()
-        {
-            if (this.userStream != null)
-            {
-                this.StartUserStream();
-            }
-        }
-
-        private class TwitterUserstream : IDisposable
-        {
-            public bool AllAtReplies { get; private set; }
-            public string TrackWords { get; private set; }
-
-            public bool IsStreamActive { get; private set; }
-
-            public event Action<string> StatusArrived;
-            public event Action Stopped;
-            public event Action Started;
-
-            private TwitterApi twitterApi;
-
-            private Task streamTask;
-            private CancellationTokenSource streamCts;
-
-            public TwitterUserstream(TwitterApi twitterApi)
-            {
-                this.twitterApi = twitterApi;
-            }
-
-            public void Start(bool allAtReplies, string trackwords)
-            {
-                this.AllAtReplies = allAtReplies;
-                this.TrackWords = trackwords;
-
-                var cts = new CancellationTokenSource();
-
-                this.streamCts = cts;
-                this.streamTask = Task.Run(async () =>
-                {
-                    try
-                    {
-                        await this.UserStreamLoop(cts.Token)
-                            .ConfigureAwait(false);
-                    }
-                    catch (OperationCanceledException) { }
-                });
-            }
-
-            public void Stop()
-            {
-                this.streamCts?.Cancel();
-
-                // streamTask の完了を待たずに IsStreamActive を false にセットする
-                this.IsStreamActive = false;
-                this.Stopped?.Invoke();
-            }
-
-            private async Task UserStreamLoop(CancellationToken cancellationToken)
-            {
-                TimeSpan sleep = TimeSpan.Zero;
-                for (;;)
-                {
-                    if (sleep != TimeSpan.Zero)
-                    {
-                        await Task.Delay(sleep, cancellationToken)
-                            .ConfigureAwait(false);
-                        sleep = TimeSpan.Zero;
-                    }
-
-                    if (!MyCommon.IsNetworkAvailable())
-                    {
-                        sleep = TimeSpan.FromSeconds(30);
-                        continue;
-                    }
-
-                    this.IsStreamActive = true;
-                    this.Started?.Invoke();
-
-                    try
-                    {
-                        var replies = this.AllAtReplies ? "all" : null;
-
-                        using (var stream = await this.twitterApi.UserStreams(replies, this.TrackWords)
-                            .ConfigureAwait(false))
-                        using (var reader = new StreamReader(stream))
-                        {
-                            while (!reader.EndOfStream)
-                            {
-                                var line = await reader.ReadLineAsync()
-                                    .ConfigureAwait(false);
-
-                                cancellationToken.ThrowIfCancellationRequested();
-
-                                this.StatusArrived?.Invoke(line);
-                            }
-                        }
-
-                        // キャンセルされていないのにストリームが終了した場合
-                        sleep = TimeSpan.FromSeconds(30);
-                    }
-                    catch (TwitterApiException) { sleep = TimeSpan.FromSeconds(30); }
-                    catch (IOException) { sleep = TimeSpan.FromSeconds(30); }
-                    catch (OperationCanceledException)
-                    {
-                        if (cancellationToken.IsCancellationRequested)
-                            throw;
-
-                        // cancellationToken によるキャンセルではない(=タイムアウトエラー)
-                        sleep = TimeSpan.FromSeconds(30);
-                    }
-                    catch (Exception ex)
-                    {
-                        MyCommon.ExceptionOut(ex);
-                        sleep = TimeSpan.FromSeconds(30);
-                    }
-                    finally
-                    {
-                        this.IsStreamActive = false;
-                        this.Stopped?.Invoke();
-                    }
-                }
-            }
-
-            private bool disposed = false;
-
-            public void Dispose()
-            {
-                if (this.disposed)
-                    return;
-
-                this.disposed = true;
-
-                this.Stop();
-
-                this.Started = null;
-                this.Stopped = null;
-                this.StatusArrived = null;
-            }
-        }
-#endregion
-
-#region "IDisposable Support"
-        private bool disposedValue; // 重複する呼び出しを検出するには
-
-        // IDisposable
-        protected virtual void Dispose(bool disposing)
-        {
-            if (!this.disposedValue)
-            {
-                if (disposing)
-                {
-                    this.StopUserStream();
-                }
-            }
-            this.disposedValue = true;
-        }
-
-        //protected Overrides void Finalize()
-        //{
-        //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
-        //    Dispose(false)
-        //    MyBase.Finalize()
-        //}
-
-        // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
         public void Dispose()
         {
-            // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
-            Dispose(true);
+            this.Dispose(true);
             GC.SuppressFinalize(this);
         }
-#endregion
-    }
-
-    public class PostDeletedEventArgs : EventArgs
-    {
-        public long StatusId { get; }
-
-        public PostDeletedEventArgs(long statusId)
-        {
-            this.StatusId = statusId;
-        }
-    }
-
-    public class UserStreamEventReceivedEventArgs : EventArgs
-    {
-        public Twitter.FormattedEvent EventData { get; }
-
-        public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
-        {
-            this.EventData = eventData;
-        }
     }
 }