OSDN Git Service

投稿欄にIME経由で絵文字を入力するとエラーが発生する問題を回避
[opentween/open-tween.git] / OpenTween / Twitter.cs
index bca0801..5216259 100644 (file)
@@ -29,6 +29,7 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Http;
 using System.Runtime.CompilerServices;
 using System.Runtime.Serialization;
 using System.Runtime.Serialization.Json;
@@ -46,7 +47,11 @@ 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
 {
@@ -142,7 +147,9 @@ namespace OpenTween
         /// </summary>
         public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
 
+        public TwitterApi Api { get; }
         public TwitterConfiguration Configuration { get; private set; }
+        public TwitterTextConfiguration TextConfiguration { get; private set; }
 
         delegate void GetIconImageDelegate(PostClass post);
         private readonly object LockObj = new object();
@@ -158,20 +165,24 @@ namespace OpenTween
         private List<string> _hashList = new List<string>();
 
         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
-        private long minHomeTimeline = long.MaxValue;
-        private long minMentions = long.MaxValue;
         private long minDirectmessage = long.MaxValue;
         private long minDirectmessageSent = long.MaxValue;
 
-        //private FavoriteQueue favQueue;
+        private long previousStatusId = -1L;
 
-        private HttpTwitter twCon = new HttpTwitter();
+        //private FavoriteQueue favQueue;
 
         //private List<PostClass> _deletemessages = new List<PostClass>();
 
-        public Twitter()
+        public Twitter() : this(new TwitterApi())
+        {
+        }
+
+        public Twitter(TwitterApi api)
         {
+            this.Api = api;
             this.Configuration = TwitterConfiguration.DefaultConfiguration();
+            this.TextConfiguration = TwitterTextConfiguration.DefaultConfiguration();
         }
 
         public TwitterApiAccessLevel AccessLevel
@@ -187,100 +198,31 @@ namespace OpenTween
             MyCommon.TwitterApiInfo.Reset();
         }
 
-        public void Authenticate(string username, string password)
-        {
-            this.ResetApiStatus();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.AuthUserAndPass(username, password, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            _uname = username.ToLowerInvariant();
-            if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
-        }
-
-        public string StartAuthentication()
-        {
-            //OAuth PIN Flow
-            this.ResetApiStatus();
-            try
-            {
-                string pinPageUrl = null;
-                var res = twCon.AuthGetRequestToken(ref pinPageUrl);
-                if (!res)
-                    throw new WebApiException("Err:Failed to access auth server.");
-
-                return pinPageUrl;
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:Failed to access auth server.", ex);
-            }
-        }
-
-        public void Authenticate(string pinCode)
-        {
-            this.ResetApiStatus();
-
-            HttpStatusCode res;
-            try
-            {
-                res = twCon.AuthGetAccessToken(pinCode);
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:Failed to access auth acc server.", ex);
-            }
-
-            this.CheckStatusCode(res, null);
-
-            _uname = Username.ToLowerInvariant();
-            if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
-        }
-
         public void ClearAuthInfo()
         {
             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
             this.ResetApiStatus();
-            twCon.ClearAuthInfo();
         }
 
+        [Obsolete]
         public void VerifyCredentials()
         {
-            HttpStatusCode res;
-            var content = "";
             try
             {
-                res = twCon.VerifyCredentials(ref content);
+                this.VerifyCredentialsAsync().Wait();
             }
-            catch (Exception ex)
+            catch (AggregateException ex) when (ex.InnerException is WebApiException)
             {
-                throw new WebApiException("Err:" + ex.Message, ex);
+                throw new WebApiException(ex.InnerException.Message, ex);
             }
+        }
 
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                var user = TwitterUser.ParseJson(content);
+        public async Task VerifyCredentialsAsync()
+        {
+            var user = await this.Api.AccountVerifyCredentials()
+                .ConfigureAwait(false);
 
-                this.twCon.AuthenticatedUserId = user.Id;
-                this.UpdateUserStats(user);
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
+            this.UpdateUserStats(user);
         }
 
         public void Initialize(string token, string tokenSecret, string username, long userId)
@@ -291,9 +233,9 @@ namespace OpenTween
                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
             }
             this.ResetApiStatus();
-            twCon.Initialize(token, tokenSecret, username, userId);
+            this.Api.Initialize(token, tokenSecret, userId, username);
             _uname = username.ToLowerInvariant();
-            if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
+            if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream();
         }
 
         public string PreProcessUrl(string orgData)
@@ -314,7 +256,9 @@ namespace OpenTween
                     posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
                     urlStr = orgData.Substring(posl1, posl2 - posl1);
 
-                    if (!urlStr.StartsWith("http://") && !urlStr.StartsWith("https://") && !urlStr.StartsWith("ftp://"))
+                    if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
+                        && !urlStr.StartsWith("https://", StringComparison.Ordinal)
+                        && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
                     {
                         continue;
                     }
@@ -334,785 +278,167 @@ namespace OpenTween
             return orgData;
         }
 
-        private string GetPlainText(string orgData)
-        {
-            return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
-        }
-
-        // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
-
-        private string SanitizeHtml(string orgdata)
-        {
-            var retdata = orgdata;
-
-            retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
-                "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
-
-            retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
-
-            return retdata;
-        }
-
-        private string AdjustHtml(string orgData)
-        {
-            var retStr = orgData;
-            //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
-            //while (m.Success)
-            //{
-            //    lock (LockObj)
-            //    {
-            //        _hashList.Add("#" + m.Groups(1).Value);
-            //    }
-            //    m = m.NextMatch;
-            //}
-            retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
-            retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
-            retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
-
-            //半角スペースを置換(Thanks @anis774)
-            var ret = false;
-            do
-            {
-                ret = EscapeSpace(ref retStr);
-            } while (!ret);
-
-            return SanitizeHtml(retStr);
-        }
-
-        private bool EscapeSpace(ref string html)
-        {
-            //半角スペースを置換(Thanks @anis774)
-            var isTag = false;
-            for (int i = 0; i < html.Length; i++)
-            {
-                if (html[i] == '<')
-                {
-                    isTag = true;
-                }
-                if (html[i] == '>')
-                {
-                    isTag = false;
-                }
-
-                if ((!isTag) && (html[i] == ' '))
-                {
-                    html = html.Remove(i, 1);
-                    html = html.Insert(i, "&nbsp;");
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        private struct PostInfo
-        {
-            public string CreatedAt;
-            public string Id;
-            public string Text;
-            public string UserId;
-            public PostInfo(string Created, string IdStr, string txt, string uid)
-            {
-                CreatedAt = Created;
-                Id = IdStr;
-                Text = txt;
-                UserId = uid;
-            }
-            public bool Equals(PostInfo dst)
-            {
-                if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
-                {
-                    return true;
-                }
-                else
-                {
-                    return false;
-                }
-            }
-        }
-
-        static private PostInfo _prev = new PostInfo("", "", "", "");
-        private bool IsPostRestricted(TwitterStatus status)
-        {
-            var _current = new PostInfo("", "", "", "");
-
-            _current.CreatedAt = status.CreatedAt;
-            _current.Id = status.IdStr;
-            if (status.Text == null)
-            {
-                _current.Text = "";
-            }
-            else
-            {
-                _current.Text = status.Text;
-            }
-            _current.UserId = status.User.IdStr;
-
-            if (_current.Equals(_prev))
-            {
-                return true;
-            }
-            _prev.CreatedAt = _current.CreatedAt;
-            _prev.Id = _current.Id;
-            _prev.Text = _current.Text;
-            _prev.UserId = _current.UserId;
-
-            return false;
-        }
-
-        public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
+        public async Task PostStatus(string postStr, long? reply_to, IReadOnlyList<long> mediaIds = null)
         {
             this.CheckAccountState();
 
-            if (mediaIds == null &&
-                Twitter.DMSendTextRegex.IsMatch(postStr))
+            if (Twitter.DMSendTextRegex.IsMatch(postStr))
             {
-                SendDirectMessage(postStr);
+                await this.SendDirectMessage(postStr)
+                    .ConfigureAwait(false);
                 return;
             }
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
-            if (res == HttpStatusCode.NotFound)
-                return;
-
-            this.CheckStatusCode(res, content);
-
-            TwitterStatus status;
-            try
-            {
-                status = TwitterStatus.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-
-            this.UpdateUserStats(status.User);
-
-            if (IsPostRestricted(status))
-            {
-                throw new WebApiException("OK:Delaying?");
-            }
-        }
-
-        public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
-            if (res == HttpStatusCode.NotFound)
-                return;
-
-            this.CheckStatusCode(res, content);
+            var response = await this.Api.StatusesUpdate(postStr, reply_to, mediaIds)
+                .ConfigureAwait(false);
 
-            TwitterStatus status;
-            try
-            {
-                status = TwitterStatus.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
+            var status = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
 
             this.UpdateUserStats(status.User);
 
-            if (IsPostRestricted(status))
-            {
+            if (status.Id == this.previousStatusId)
                 throw new WebApiException("OK:Delaying?");
-            }
-        }
 
-        public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
-        {
-            this.CheckAccountState();
-
-            if (Twitter.DMSendTextRegex.IsMatch(postStr))
-            {
-                SendDirectMessage(postStr);
-                return;
-            }
-
-            var mediaIds = new List<long>();
-
-            foreach (var item in mediaItems)
-            {
-                var mediaId = UploadMedia(item);
-                mediaIds.Add(mediaId);
-            }
-
-            if (mediaIds.Count == 0)
-                throw new WebApiException("Err:Invalid Files!");
-
-            PostStatus(postStr, reply_to, mediaIds);
+            this.previousStatusId = status.Id;
         }
 
-        public long UploadMedia(IMediaItem item)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.UploadMedia(item, ref content);
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
+        public Task<long> UploadMedia(IMediaItem item)
+            => this.UploadMedia(item, SettingManager.Common.AlphaPNGWorkaround);
 
-            TwitterUploadMediaResult status;
-            try
-            {
-                status = TwitterUploadMediaResult.ParseJson(content);
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-
-            return status.MediaId;
-        }
-
-        public void SendDirectMessage(string postStr)
+        public async Task<long> UploadMedia(IMediaItem item, bool alphaPNGWorkaround)
         {
             this.CheckAccountState();
-            this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
-            var mc = Twitter.DMSendTextRegex.Match(postStr);
+            LazyJson<TwitterUploadMediaResult> response;
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
-            }
-            catch(Exception ex)
+            using (var origImage = item.CreateImage())
             {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            TwitterDirectMessage status;
-            try
-            {
-                status = TwitterDirectMessage.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
+                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);
+                }
             }
 
-            this.UpdateUserStats(status.Sender);
-        }
-
-        public void RemoveStatus(long id)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            try
-            {
-                res = twCon.DestroyStatus(id);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
+            var media = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, null);
+            return media.MediaId;
         }
 
-        public void PostRetweet(long id, bool read)
-        {
-            this.CheckAccountState();
-
-            //データ部分の生成
-            var target = id;
-            var post = TabInformations.GetInstance()[id];
-            if (post == null)
-            {
-                throw new WebApiException("Err:Target isn't found.");
-            }
-            if (TabInformations.GetInstance()[id].RetweetedId != null)
-            {
-                target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
-            }
+        /// <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;
+
+            // PNG 画像以外に対しては何もしない
+            if (origImage.RawFormat.Guid != ImageFormat.Png.Guid)
+                return false;
 
-            HttpStatusCode res;
-            var content = "";
-            try
+            using (var bitmap = new Bitmap(origImage))
             {
-                res = twCon.RetweetStatus(target, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
+                // アルファ値が 255 以外のピクセルが含まれていた場合は何もしない
+                foreach (var x in Enumerable.Range(0, bitmap.Width))
+                {
+                    foreach (var y in Enumerable.Range(0, bitmap.Height))
+                    {
+                        if (bitmap.GetPixel(x, y).A != 255)
+                            return false;
+                    }
+                }
 
-            TwitterStatus status;
-            try
-            {
-                status = TwitterStatus.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
+                // 左上の 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);
 
-            //ReTweetしたものをTLに追加
-            post = CreatePostsFromStatusData(status);
-            if (post == null)
-                throw new WebApiException("Invalid Json!", content);
+                // MemoryImage 作成時に画像はコピーされるため、この後 bitmap は破棄しても問題ない
+                newImage = MemoryImage.CopyFromImage(bitmap);
 
-            //二重取得回避
-            lock (LockObj)
-            {
-                if (TabInformations.GetInstance().ContainsKey(post.StatusId))
-                    return;
+                return true;
             }
-            //Retweet判定
-            if (post.RetweetedId == null)
-                throw new WebApiException("Invalid Json!", content);
-            //ユーザー情報
-            post.IsMe = true;
-
-            post.IsRead = read;
-            post.IsOwl = false;
-            if (_readOwnPost) post.IsRead = true;
-            post.IsDm = false;
-
-            TabInformations.GetInstance().AddPost(post);
         }
 
-        public void RemoveDirectMessage(long id, PostClass post)
+        public async Task SendDirectMessage(string postStr)
         {
             this.CheckAccountState();
             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
-            //if (post.IsMe)
-            //    _deletemessages.Add(post)
-            //}
-
-            HttpStatusCode res;
-            try
-            {
-                res = twCon.DestroyDirectMessage(id);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, null);
-        }
-
-        public void PostFollowCommand(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.CreateFriendships(screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
-        }
-
-        public void PostRemoveCommand(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.DestroyFriendships(screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-        }
-
-        public void PostCreateBlock(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.CreateBlock(screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-        }
-
-        public void PostDestroyBlock(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.DestroyBlock(screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-        }
-
-        public void PostReportSpam(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.ReportSpam(screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-        }
-
-        public TwitterFriendship GetFriendshipInfo(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.ShowFriendships(_uname, screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                return TwitterFriendship.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-        }
-
-        public TwitterUser GetUserInfo(string screenName)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.ShowUserInfo(screenName, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                return TwitterUser.ParseJson(content);
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-        }
-
-        public int GetStatus_Retweeted_Count(long StatusId)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.ShowStatuses(StatusId, ref content);
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                var status = TwitterStatus.ParseJson(content);
-                return status.RetweetCount;
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Invalid Json!", content, ex);
-            }
-        }
-
-        public void PostFavAdd(long id)
-        {
-            this.CheckAccountState();
-
-            //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
-
-            //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.CreateFavorites(id, ref content);
-            }
-            catch(Exception ex)
-            {
-                //this.favQueue.Add(id)
-                //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            if (!RestrictFavCheck)
-                return;
-
-            //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
-
-            try
-            {
-                res = twCon.ShowStatuses(id, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            TwitterStatus status;
-            try
-            {
-                status = TwitterStatus.ParseJson(content);
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-            if (status.Favorited != true)
-                throw new WebApiException("NG(Restricted?)");
-        }
-
-        public void PostFavRemove(long id)
-        {
-            this.CheckAccountState();
-
-            //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
+            var mc = Twitter.DMSendTextRegex.Match(postStr);
 
-            //if (this.favQueue.Contains(id))
-            //    this.favQueue.Remove(id)
-            //    return "";
-            //}
+            var response = await this.Api.DirectMessagesNew(mc.Groups["body"].Value, mc.Groups["id"].Value)
+                .ConfigureAwait(false);
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.DestroyFavorites(id, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
+            var dm = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, content);
+            this.UpdateUserStats(dm.Sender);
         }
 
-        public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
+        public async Task PostRetweet(long id, bool read)
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.UpdateProfile(name, url, location, description, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, content, ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                return TwitterUser.ParseJson(content);
-            }
-            catch (SerializationException e)
-            {
-                var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-            catch (Exception e)
-            {
-                var ex = new WebApiException("Err:Invalid Json!", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-        }
+            //データ部分の生成
+            var post = TabInformations.GetInstance()[id];
+            if (post == null)
+                throw new WebApiException("Err:Target isn't found.");
 
-        public void PostUpdateProfileImage(string filename)
-        {
-            this.CheckAccountState();
+            var target = post.RetweetedId ?? id;  //再RTの場合は元発言をRT
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
-            }
-            catch(Exception ex)
+            var response = await this.Api.StatusesRetweet(target)
+                .ConfigureAwait(false);
+
+            var status = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
+
+            //二重取得回避
+            lock (LockObj)
             {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
+                if (TabInformations.GetInstance().ContainsKey(status.Id))
+                    return;
             }
 
-            this.CheckStatusCode(res, content);
+            //Retweet判定
+            if (status.RetweetedStatus == null)
+                throw new WebApiException("Invalid Json!");
+
+            //ReTweetしたものをTLに追加
+            post = CreatePostsFromStatusData(status);
+            
+            //ユーザー情報
+            post.IsMe = true;
+
+            post.IsRead = read;
+            post.IsOwl = false;
+            if (_readOwnPost) post.IsRead = true;
+            post.IsDm = false;
+
+            TabInformations.GetInstance().AddPost(post);
         }
 
         public string Username
-        {
-            get
-            {
-                return twCon.AuthenticatedUsername;
-            }
-        }
+            => this.Api.CurrentScreenName;
 
         public long UserId
-        {
-            get
-            {
-                return twCon.AuthenticatedUserId;
-            }
-        }
-
-        public string Password
-        {
-            get
-            {
-                return twCon.Password;
-            }
-        }
+            => this.Api.CurrentUserId;
 
         private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
         public static MyCommon.ACCOUNT_STATE AccountState
@@ -1129,109 +455,6 @@ namespace OpenTween
 
         public bool RestrictFavCheck { get; set; }
 
-#region "バージョンアップ"
-        public void GetTweenBinary(string strVer)
-        {
-            try
-            {
-                //本体
-                if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
-                                                    Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
-                {
-                    throw new WebApiException("Err:Download failed");
-                }
-                //英語リソース
-                if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
-                {
-                    Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
-                }
-                if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
-                                                    Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
-                {
-                    throw new WebApiException("Err:Download failed");
-                }
-                //その他言語圏のリソース。取得失敗しても継続
-                //UIの言語圏のリソース
-                var curCul = "";
-                if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
-                {
-                    var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
-                    if (idx > -1)
-                    {
-                        curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
-                    }
-                    else
-                    {
-                        curCul = Thread.CurrentThread.CurrentUICulture.Name;
-                    }
-                }
-                else
-                {
-                    curCul = Thread.CurrentThread.CurrentUICulture.Name;
-                }
-                if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
-                {
-                    if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
-                    {
-                        Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
-                    }
-                    if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
-                                                        Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
-                    {
-                        //return "Err:Download failed";
-                    }
-                }
-                //スレッドの言語圏のリソース
-                string curCul2;
-                if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
-                {
-                    var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
-                    if (idx > -1)
-                    {
-                        curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
-                    }
-                    else
-                    {
-                        curCul2 = Thread.CurrentThread.CurrentCulture.Name;
-                    }
-                }
-                else
-                {
-                    curCul2 = Thread.CurrentThread.CurrentCulture.Name;
-                }
-                if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
-                {
-                    if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
-                    {
-                        Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
-                    }
-                    if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
-                                                    Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
-                    {
-                        //return "Err:Download failed";
-                    }
-                }
-
-                //アップデータ
-                if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
-                                                    Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
-                {
-                    throw new WebApiException("Err:Download failed");
-                }
-                //シリアライザDLL
-                if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
-                                                    Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
-                {
-                    throw new WebApiException("Err:Download failed");
-                }
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:Download failed", ex);
-            }
-        }
-#endregion
-
         public bool ReadOwnPost
         {
             get
@@ -1321,187 +544,135 @@ namespace OpenTween
                 return 20;
             }
 
-            if (SettingCommon.Instance.UseAdditionalCount)
+            if (SettingManager.Common.UseAdditionalCount)
             {
                 switch (type)
                 {
                     case MyCommon.WORKERTYPE.Favorites:
-                        if (SettingCommon.Instance.FavoritesCountApi != 0)
-                            return SettingCommon.Instance.FavoritesCountApi;
+                        if (SettingManager.Common.FavoritesCountApi != 0)
+                            return SettingManager.Common.FavoritesCountApi;
                         break;
                     case MyCommon.WORKERTYPE.List:
-                        if (SettingCommon.Instance.ListCountApi != 0)
-                            return SettingCommon.Instance.ListCountApi;
+                        if (SettingManager.Common.ListCountApi != 0)
+                            return SettingManager.Common.ListCountApi;
                         break;
                     case MyCommon.WORKERTYPE.PublicSearch:
-                        if (SettingCommon.Instance.SearchCountApi != 0)
-                            return SettingCommon.Instance.SearchCountApi;
+                        if (SettingManager.Common.SearchCountApi != 0)
+                            return SettingManager.Common.SearchCountApi;
                         break;
                     case MyCommon.WORKERTYPE.UserTimeline:
-                        if (SettingCommon.Instance.UserTimelineCountApi != 0)
-                            return SettingCommon.Instance.UserTimelineCountApi;
+                        if (SettingManager.Common.UserTimelineCountApi != 0)
+                            return SettingManager.Common.UserTimelineCountApi;
                         break;
                 }
-                if (more && SettingCommon.Instance.MoreCountApi != 0)
+                if (more && SettingManager.Common.MoreCountApi != 0)
                 {
-                    return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
+                    return Math.Min(SettingManager.Common.MoreCountApi, GetMaxApiResultCount(type));
                 }
-                if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
+                if (startup && SettingManager.Common.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
                 {
-                    return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
+                    return Math.Min(SettingManager.Common.FirstCountApi, GetMaxApiResultCount(type));
                 }
             }
 
             // 上記に当てはまらない場合の共通処理
-            var count = SettingCommon.Instance.CountApi;
+            var count = SettingManager.Common.CountApi;
 
             if (type == MyCommon.WORKERTYPE.Reply)
-                count = SettingCommon.Instance.CountApiReply;
+                count = SettingManager.Common.CountApiReply;
 
             return Math.Min(count, GetMaxApiResultCount(type));
         }
 
-        public void GetTimelineApi(bool read,
-                                MyCommon.WORKERTYPE gType,
-                                bool more,
-                                bool startup)
+        public async Task GetHomeTimelineApi(bool read, HomeTabModel tab, bool more, bool startup)
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            var content = "";
-            var count = GetApiResultCount(gType, more, startup);
+            var count = GetApiResultCount(MyCommon.WORKERTYPE.Timeline, more, startup);
 
-            try
+            TwitterStatus[] statuses;
+            if (more)
             {
-                if (gType == MyCommon.WORKERTYPE.Timeline)
-                {
-                    if (more)
-                    {
-                        res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
-                    }
-                    else
-                    {
-                        res = twCon.HomeTimeline(count, null, null, ref content);
-                    }
-                }
-                else
-                {
-                    if (more)
-                    {
-                        res = twCon.Mentions(count, this.minMentions, null, ref content);
-                    }
-                    else
-                    {
-                        res = twCon.Mentions(count, null, null, ref content);
-                    }
-                }
+                statuses = await this.Api.StatusesHomeTimeline(count, maxId: tab.OldestId)
+                    .ConfigureAwait(false);
             }
-            catch(Exception ex)
+            else
             {
-                throw new WebApiException("Err:" + ex.Message, ex);
+                statuses = await this.Api.StatusesHomeTimeline(count)
+                    .ConfigureAwait(false);
             }
 
-            this.CheckStatusCode(res, content);
+            var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Timeline, tab, read);
+            if (minimumId != null)
+                tab.OldestId = minimumId.Value;
+        }
+
+        public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool more, bool startup)
+        {
+            this.CheckAccountState();
 
-            var minimumId = CreatePostsFromJson(content, gType, null, read);
+            var count = GetApiResultCount(MyCommon.WORKERTYPE.Reply, more, startup);
 
-            if (minimumId != null)
+            TwitterStatus[] statuses;
+            if (more)
             {
-                if (gType == MyCommon.WORKERTYPE.Timeline)
-                    this.minHomeTimeline = minimumId.Value;
-                else
-                    this.minMentions = minimumId.Value;
+                statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId)
+                    .ConfigureAwait(false);
             }
+            else
+            {
+                statuses = await this.Api.StatusesMentionsTimeline(count)
+                    .ConfigureAwait(false);
+            }
+
+            var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Reply, tab, read);
+            if (minimumId != null)
+                tab.OldestId = minimumId.Value;
         }
 
-        public void GetUserTimelineApi(bool read,
-                                         string userName,
-                                         TabClass tab,
-                                         bool more)
+        public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTabModel tab, bool more)
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            var content = "";
             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
 
-            try
+            TwitterStatus[] statuses;
+            if (string.IsNullOrEmpty(userName))
             {
-                if (string.IsNullOrEmpty(userName))
+                var target = tab.ScreenName;
+                if (string.IsNullOrEmpty(target)) return;
+                userName = target;
+                statuses = await this.Api.StatusesUserTimeline(userName, count)
+                    .ConfigureAwait(false);
+            }
+            else
+            {
+                if (more)
                 {
-                    var target = tab.User;
-                    if (string.IsNullOrEmpty(target)) return;
-                    userName = target;
-                    res = twCon.UserTimeline(null, target, count, null, null, ref content);
+                    statuses = await this.Api.StatusesUserTimeline(userName, count, maxId: tab.OldestId)
+                        .ConfigureAwait(false);
                 }
                 else
                 {
-                    if (more)
-                    {
-                        res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
-                    }
-                    else
-                    {
-                        res = twCon.UserTimeline(null, userName, count, null, null, ref content);
-                    }
+                    statuses = await this.Api.StatusesUserTimeline(userName, count)
+                        .ConfigureAwait(false);
                 }
             }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            if (res == HttpStatusCode.Unauthorized)
-                throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
 
-            this.CheckStatusCode(res, content);
-
-            var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
+            var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
 
             if (minimumId != null)
                 tab.OldestId = minimumId.Value;
         }
 
-        public PostClass GetStatusApi(bool read, long id)
+        public async Task<PostClass> GetStatusApi(bool read, long id)
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.ShowStatuses(id, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-
-            if (res == HttpStatusCode.Forbidden)
-                throw new WebApiException("Err:protected user's tweet", content);
-
-            this.CheckStatusCode(res, content);
-
-            TwitterStatus status;
-            try
-            {
-                status = TwitterStatus.ParseJson(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Invalid Json!", content, ex);
-            }
+            var status = await this.Api.StatusesShow(id)
+                .ConfigureAwait(false);
 
             var item = CreatePostsFromStatusData(status);
-            if (item == null)
-                throw new WebApiException("Err:Can't create post", content);
 
             item.IsRead = read;
             if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
@@ -1509,13 +680,14 @@ namespace OpenTween
             return item;
         }
 
-        public void GetStatusApi(bool read, long id, TabClass tab)
+        public async Task GetStatusApi(bool read, long id, TabModel tab)
         {
-            var post = this.GetStatusApi(read, id);
+            var post = await this.GetStatusApi(read, id)
+                .ConfigureAwait(false);
 
             //非同期アイコン取得&StatusDictionaryに追加
             if (tab != null && tab.IsInnerStorageTabType)
-                tab.AddPostToInnerStorage(post);
+                tab.AddPostQueue(post);
             else
                 TabInformations.GetInstance().AddPost(post);
         }
@@ -1541,7 +713,7 @@ namespace OpenTween
                 //Id
                 post.RetweetedId = retweeted.Id;
                 //本文
-                post.TextFromApi = retweeted.Text;
+                post.TextFromApi = retweeted.FullText;
                 entities = retweeted.MergedEntities;
                 sourceHtml = retweeted.Source;
                 //Reply先
@@ -1565,25 +737,39 @@ namespace OpenTween
 
                 //以下、ユーザー情報
                 var user = retweeted.User;
-
-                if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
-
-                post.UserId = user.Id;
-                post.ScreenName = user.ScreenName;
-                post.Nickname = user.Name.Trim();
-                post.ImageUrl = user.ProfileImageUrlHttps;
-                post.IsProtect = user.Protected;
+                if (user != null)
+                {
+                    post.UserId = user.Id;
+                    post.ScreenName = user.ScreenName;
+                    post.Nickname = user.Name.Trim();
+                    post.ImageUrl = user.ProfileImageUrlHttps;
+                    post.IsProtect = user.Protected;
+                }
+                else
+                {
+                    post.UserId = 0L;
+                    post.ScreenName = "?????";
+                    post.Nickname = "Unknown User";
+                }
 
                 //Retweetした人
-                post.RetweetedBy = status.User.ScreenName;
-                post.RetweetedByUserId = status.User.Id;
-                post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+                if (status.User != null)
+                {
+                    post.RetweetedBy = status.User.ScreenName;
+                    post.RetweetedByUserId = status.User.Id;
+                    post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+                }
+                else
+                {
+                    post.RetweetedBy = "?????";
+                    post.RetweetedByUserId = 0L;
+                }
             }
             else
             {
                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
                 //本文
-                post.TextFromApi = status.Text;
+                post.TextFromApi = status.FullText;
                 entities = status.MergedEntities;
                 sourceHtml = status.Source;
                 post.InReplyToStatusId = status.InReplyToStatusId;
@@ -1606,15 +792,21 @@ namespace OpenTween
 
                 //以下、ユーザー情報
                 var user = status.User;
-
-                if (user == null || user.ScreenName == null) return null;
-
-                post.UserId = user.Id;
-                post.ScreenName = user.ScreenName;
-                post.Nickname = user.Name.Trim();
-                post.ImageUrl = user.ProfileImageUrlHttps;
-                post.IsProtect = user.Protected;
-                post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+                if (user != null)
+                {
+                    post.UserId = user.Id;
+                    post.ScreenName = user.ScreenName;
+                    post.Nickname = user.Name.Trim();
+                    post.ImageUrl = user.ProfileImageUrlHttps;
+                    post.IsProtect = user.Protected;
+                    post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+                }
+                else
+                {
+                    post.UserId = 0L;
+                    post.ScreenName = "?????";
+                    post.Nickname = "Unknown User";
+                }
             }
             //HTMLに整形
             string textFromApi = post.TextFromApi;
@@ -1623,22 +815,36 @@ namespace OpenTween
             post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
             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 = WebUtility.HtmlDecode(post.AccessibleText);
+            post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
 
             post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
                 .Where(x => x != post.StatusId && x != post.RetweetedId)
                 .Distinct().ToArray();
 
             post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
-                .Where(x => x != null)
                 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
                 .ToArray();
 
+            // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+            if (post.Text == post.TextFromApi)
+                post.Text = post.TextFromApi;
+            if (post.AccessibleText == post.TextFromApi)
+                post.AccessibleText = post.TextFromApi;
+
+            // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+            post.ScreenName = string.Intern(post.ScreenName);
+            post.Nickname = string.Intern(post.Nickname);
+            post.ImageUrl = string.Intern(post.ImageUrl);
+            post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
+
             //Source整形
-            var source = ParseSource(sourceHtml);
-            post.Source = source.Item1;
-            post.SourceUri = source.Item2;
+            var (sourceText, sourceUri) = ParseSource(sourceHtml);
+            post.Source = string.Intern(sourceText);
+            post.SourceUri = sourceUri;
 
-            post.IsReply = post.ReplyToList.Contains(_uname);
+            post.IsReply = post.RetweetedId == null && post.ReplyToList.Contains(_uname);
             post.IsExcludeReply = false;
 
             if (post.IsMe)
@@ -1659,8 +865,7 @@ namespace OpenTween
         /// </summary>
         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
         {
-            var urls = entities.OfType<TwitterEntityUrl>().Where(x => x != null)
-                .Select(x => x.ExpandedUrl);
+            var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
 
             return GetQuoteTweetStatusIds(urls);
         }
@@ -1672,65 +877,45 @@ namespace OpenTween
                 var match = Twitter.StatusUrlRegex.Match(url);
                 if (match.Success)
                 {
-                    long statusId;
-                    if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
+                    if (long.TryParse(match.Groups["StatusId"].Value, out var statusId))
                         yield return statusId;
                 }
             }
         }
 
-        private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
+        private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel tab, bool read)
         {
-            TwitterStatus[] items;
-            try
-            {
-                items = TwitterStatus.ParseJsonArray(content);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Invalid Json!", content, ex);
-            }
-
             long? minimumId = null;
 
             foreach (var status in items)
             {
-                PostClass post = null;
-                post = CreatePostsFromStatusData(status);
-                if (post == null) continue;
-
-                if (minimumId == null || minimumId.Value > post.StatusId)
-                    minimumId = post.StatusId;
+                if (minimumId == null || minimumId.Value > status.Id)
+                    minimumId = status.Id;
 
                 //二重取得回避
                 lock (LockObj)
                 {
                     if (tab == null)
                     {
-                        if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
+                        if (TabInformations.GetInstance().ContainsKey(status.Id)) continue;
                     }
                     else
                     {
-                        if (tab.Contains(post.StatusId)) continue;
+                        if (tab.Contains(status.Id)) continue;
                     }
                 }
 
                 //RT禁止ユーザーによるもの
                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
-                    post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
+                    status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue;
+
+                var post = CreatePostsFromStatusData(status);
 
                 post.IsRead = read;
                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
 
-                //非同期アイコン取得&StatusDictionaryに追加
                 if (tab != null && tab.IsInnerStorageTabType)
-                    tab.AddPostToInnerStorage(post);
+                    tab.AddPostQueue(post);
                 else
                     TabInformations.GetInstance().AddPost(post);
             }
@@ -1738,96 +923,43 @@ namespace OpenTween
             return minimumId;
         }
 
-        private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
+        private long? CreatePostsFromSearchJson(TwitterSearchResult items, PublicSearchTabModel tab, bool read, bool more)
         {
-            TwitterSearchResult items;
-            try
-            {
-                items = TwitterSearchResult.ParseJson(content);
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Invalid Json!", content, ex);
-            }
-
             long? minimumId = null;
 
-            foreach (var result in items.Statuses)
+            foreach (var status in items.Statuses)
             {
-                PostClass post = null;
-                post = CreatePostsFromStatusData(result);
-
-                if (post == null)
-                {
-                    // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
-                    try
-                    {
-                        post = this.GetStatusApi(read, result.Id);
-                    }
-                    catch (WebApiException)
-                    {
-                        continue;
-                    }
-                }
-
-                if (minimumId == null || minimumId.Value > post.StatusId)
-                    minimumId = post.StatusId;
+                if (minimumId == null || minimumId.Value > status.Id)
+                    minimumId = status.Id;
 
-                if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
+                if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id;
                 //二重取得回避
                 lock (LockObj)
                 {
-                    if (tab == null)
-                    {
-                        if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
-                    }
-                    else
-                    {
-                        if (tab.Contains(post.StatusId)) continue;
-                    }
+                    if (tab.Contains(status.Id)) continue;
                 }
 
+                var post = CreatePostsFromStatusData(status);
+
                 post.IsRead = read;
                 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
 
-                //非同期アイコン取得&StatusDictionaryに追加
-                if (tab != null && tab.IsInnerStorageTabType)
-                    tab.AddPostToInnerStorage(post);
-                else
-                    TabInformations.GetInstance().AddPost(post);
+                tab.AddPostQueue(post);
             }
 
             return minimumId;
         }
 
-        private void CreateFavoritePostsFromJson(string content, bool read)
+        private long? CreateFavoritePostsFromJson(TwitterStatus[] items, bool read)
         {
-            TwitterStatus[] item;
-            try
-            {
-                item = TwitterStatus.ParseJsonArray(content);
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Invalid Json!", content, ex);
-            }
-
             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+            long? minimumId = null;
 
-            foreach (var status in item)
+            foreach (var status in items)
             {
+                if (minimumId == null || minimumId.Value > status.Id)
+                    minimumId = status.Id;
+
                 //二重取得回避
                 lock (LockObj)
                 {
@@ -1835,42 +967,32 @@ namespace OpenTween
                 }
 
                 var post = CreatePostsFromStatusData(status, true);
-                if (post == null) continue;
 
                 post.IsRead = read;
 
                 TabInformations.GetInstance().AddPost(post);
             }
+
+            return minimumId;
         }
 
-        public void GetListStatus(bool read,
-                                TabClass tab,
-                                bool more,
-                                bool startup)
+        public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, bool startup)
         {
-            HttpStatusCode res;
-            var content = "";
             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
 
-            try
+            TwitterStatus[] statuses;
+            if (more)
             {
-                if (more)
-                {
-                    res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
-                }
-                else
-                {
-                    res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
-                }
+                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Common.IsListsIncludeRts)
+                    .ConfigureAwait(false);
             }
-            catch(Exception ex)
+            else
             {
-                throw new WebApiException("Err:" + ex.Message, ex);
+                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingManager.Common.IsListsIncludeRts)
+                    .ConfigureAwait(false);
             }
 
-            this.CheckStatusCode(res, content);
-
-            var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
+            var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
 
             if (minimumId != null)
                 tab.OldestId = minimumId.Value;
@@ -1896,29 +1018,31 @@ namespace OpenTween
             return nextPost;
         }
 
-        public void GetRelatedResult(bool read, TabClass tab)
+        public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab)
         {
+            var targetPost = tab.TargetPost;
             var relPosts = new Dictionary<Int64, PostClass>();
-            if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
+            if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null)
             {
                 //検索結果対応
-                var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
+                var p = TabInformations.GetInstance()[targetPost.StatusId];
                 if (p != null && p.InReplyToStatusId != null)
                 {
-                    tab.RelationTargetPost = p;
+                    targetPost = p;
                 }
                 else
                 {
-                    p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
-                    tab.RelationTargetPost = p;
+                    p = await this.GetStatusApi(read, targetPost.StatusId)
+                        .ConfigureAwait(false);
+                    targetPost = p;
                 }
             }
-            relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
+            relPosts.Add(targetPost.StatusId, targetPost);
 
             Exception lastException = null;
 
             // in_reply_to_status_id を使用してリプライチェインを辿る
-            var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
+            var nextPost = FindTopOfReplyChain(relPosts, targetPost.StatusId);
             var loopCount = 1;
             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
             {
@@ -1929,7 +1053,8 @@ namespace OpenTween
                 {
                     try
                     {
-                        inReplyToPost = this.GetStatusApi(read, inReplyToId);
+                        inReplyToPost = await this.GetStatusApi(read, inReplyToId)
+                            .ConfigureAwait(false);
                     }
                     catch (WebApiException ex)
                     {
@@ -1944,13 +1069,12 @@ namespace OpenTween
             }
 
             //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
-            var text = tab.RelationTargetPost.Text;
+            var text = targetPost.Text;
             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
             foreach (var _match in ma)
             {
-                Int64 _statusId;
-                if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
+                if (Int64.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
                 {
                     if (relPosts.ContainsKey(_statusId))
                         continue;
@@ -1960,7 +1084,8 @@ namespace OpenTween
                     {
                         try
                         {
-                            p = this.GetStatusApi(read, _statusId);
+                            p = await this.GetStatusApi(read, _statusId)
+                                .ConfigureAwait(false);
                         }
                         catch (WebApiException ex)
                         {
@@ -1981,20 +1106,17 @@ namespace OpenTween
                 else
                     p.IsRead = read;
 
-                tab.AddPostToInnerStorage(p);
+                tab.AddPostQueue(p);
             });
 
             if (lastException != null)
                 throw new WebApiException(lastException.Message, lastException);
         }
 
-        public void GetSearch(bool read,
-                            TabClass tab,
-                            bool more)
+        public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more)
         {
-            HttpStatusCode res;
-            var content = "";
             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
+
             long? maxId = null;
             long? sinceId = null;
             if (more)
@@ -2006,63 +1128,20 @@ namespace OpenTween
                 sinceId = tab.SinceId;
             }
 
-            try
-            {
-                // TODO:一時的に40>100件に 件数変更UI作成の必要あり
-                res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message, ex);
-            }
-            switch (res)
-            {
-                case HttpStatusCode.BadRequest:
-                    throw new WebApiException("Invalid query", content);
-                case HttpStatusCode.NotFound:
-                    throw new WebApiException("Invalid query", content);
-                case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
-                    throw new WebApiException("Search API Limit?", content);
-                case HttpStatusCode.OK:
-                    break;
-                default:
-                    throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
-            }
+            var searchResult = await this.Api.SearchTweets(tab.SearchWords, tab.SearchLang, count, maxId, sinceId)
+                .ConfigureAwait(false);
 
             if (!TabInformations.GetInstance().ContainsTab(tab))
                 return;
 
-            var minimumId =  this.CreatePostsFromSearchJson(content, tab, read, count, more);
+            var minimumId = this.CreatePostsFromSearchJson(searchResult, tab, read, more);
 
             if (minimumId != null)
                 tab.OldestId = minimumId.Value;
         }
 
-        private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
+        private void CreateDirectMessagesFromJson(TwitterDirectMessage[] item, MyCommon.WORKERTYPE gType, bool read)
         {
-            TwitterDirectMessage[] item;
-            try
-            {
-                if (gType == MyCommon.WORKERTYPE.UserStream)
-                {
-                    item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
-                }
-                else
-                {
-                    item = TwitterDirectMessage.ParseJsonArray(content);
-                }
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Invalid Json!", content, ex);
-            }
-
             foreach (var message in item)
             {
                 var post = new PostClass();
@@ -2096,12 +1175,14 @@ namespace OpenTween
                     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>()
-                        .Where(x => x != null)
                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
                         .ToArray();
 
@@ -2109,7 +1190,7 @@ namespace OpenTween
                     TwitterUser user;
                     if (gType == MyCommon.WORKERTYPE.UserStream)
                     {
-                        if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
+                        if (this.Api.CurrentUserId == message.Recipient.Id)
                         {
                             user = message.Sender;
                             post.IsMe = false;
@@ -2143,10 +1224,21 @@ namespace OpenTween
                     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);
                 }
                 catch(Exception ex)
                 {
-                    MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
+                    MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
                     MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
                     continue;
                 }
@@ -2158,77 +1250,70 @@ namespace OpenTween
                 post.IsDm = true;
 
                 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
-                dmTab.AddPostToInnerStorage(post);
+                dmTab.AddPostQueue(post);
             }
         }
 
-        public void GetDirectMessageApi(bool read,
-                                MyCommon.WORKERTYPE gType,
-                                bool more)
+        public async Task GetDirectMessageApi(bool read, MyCommon.WORKERTYPE gType, bool more)
         {
             this.CheckAccountState();
             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
-            HttpStatusCode res;
-            var content = "";
             var count = GetApiResultCount(gType, more, false);
 
-            try
+            TwitterDirectMessage[] messages;
+            if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
             {
-                if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
+                if (more)
                 {
-                    if (more)
-                    {
-                        res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
-                    }
-                    else
-                    {
-                        res = twCon.DirectMessages(count, null, null, ref content);
-                    }
+                    messages = await this.Api.DirectMessagesRecv(count, maxId: this.minDirectmessage)
+                        .ConfigureAwait(false);
                 }
                 else
                 {
-                    if (more)
-                    {
-                        res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
-                    }
-                    else
-                    {
-                        res = twCon.DirectMessagesSent(count, null, null, ref content);
-                    }
+                    messages = await this.Api.DirectMessagesRecv(count)
+                        .ConfigureAwait(false);
                 }
             }
-            catch(Exception ex)
+            else
             {
-                throw new WebApiException("Err:" + ex.Message, ex);
+                if (more)
+                {
+                    messages = await this.Api.DirectMessagesSent(count, maxId: this.minDirectmessageSent)
+                        .ConfigureAwait(false);
+                }
+                else
+                {
+                    messages = await this.Api.DirectMessagesSent(count)
+                        .ConfigureAwait(false);
+                }
             }
 
-            this.CheckStatusCode(res, content);
-
-            CreateDirectMessagesFromJson(content, gType, read);
+            CreateDirectMessagesFromJson(messages, gType, read);
         }
 
-        public void GetFavoritesApi(bool read,
-                            bool more)
+        public async Task GetFavoritesApi(bool read, FavoritesTabModel tab, bool backward)
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            var content = "";
-            var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
+            var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, backward, false);
 
-            try
+            TwitterStatus[] statuses;
+            if (backward)
             {
-                res = twCon.Favorites(count, ref content);
+                statuses = await this.Api.FavoritesList(count, maxId: tab.OldestId)
+                    .ConfigureAwait(false);
             }
-            catch(Exception ex)
+            else
             {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
+                statuses = await this.Api.FavoritesList(count)
+                    .ConfigureAwait(false);
             }
 
-            this.CheckStatusCode(res, content);
+            var minimumId = this.CreateFavoritePostsFromJson(statuses, read);
 
-            CreateFavoritePostsFromJson(content, read);
+            if (minimumId != null)
+                tab.OldestId = minimumId.Value;
         }
 
         private string ReplaceTextFromApi(string text, TwitterEntities entities)
@@ -2253,11 +1338,54 @@ namespace OpenTween
             return text;
         }
 
+        private string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quoteStatus)
+        {
+            if (entities == null)
+                return text;
+
+            if (entities.Urls != null)
+            {
+                foreach (var entity in entities.Urls)
+                {
+                    if (quoteStatus != null)
+                    {
+                        var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
+                        if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quoteStatus.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));
+                        }
+                    }
+                    else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+                    {
+                        text = text.Replace(entity.Url, entity.DisplayUrl);
+                    }
+                }
+            }
+
+            if (entities.Media != null)
+            {
+                foreach (var entity in entities.Media)
+                {
+                    if (!string.IsNullOrEmpty(entity.AltText))
+                    {
+                        text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
+                    }
+                    else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+                    {
+                        text = text.Replace(entity.Url, entity.DisplayUrl);
+                    }
+                }
+            }
+
+            return text;
+        }
+
         /// <summary>
         /// フォロワーIDを更新します
         /// </summary>
         /// <exception cref="WebApiException"/>
-        public void RefreshFollowerIds()
+        public async Task RefreshFollowerIds()
         {
             if (MyCommon._endingFlag) return;
 
@@ -2265,7 +1393,12 @@ namespace OpenTween
             var newFollowerIds = new HashSet<long>();
             do
             {
-                var ret = this.GetFollowerIdsApi(ref cursor);
+                var ret = await this.Api.FollowersIds(cursor)
+                    .ConfigureAwait(false);
+
+                if (ret.Ids == null)
+                    throw new WebApiException("ret.ids == null");
+
                 newFollowerIds.UnionWith(ret.Ids);
                 cursor = ret.NextCursor;
             } while (cursor != 0);
@@ -2284,98 +1417,20 @@ namespace OpenTween
             }
         }
 
-        private TwitterIds GetFollowerIdsApi(ref long cursor)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.FollowerIds(cursor, ref content);
-            }
-            catch(Exception e)
-            {
-                throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                var ret = TwitterIds.ParseJson(content);
-
-                if (ret.Ids == null)
-                {
-                    var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
-                    MyCommon.ExceptionOut(ex);
-                    throw ex;
-                }
-
-                return ret;
-            }
-            catch(SerializationException e)
-            {
-                var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-            catch(Exception e)
-            {
-                var ex = new WebApiException("Err:Invalid Json!", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-        }
-
         /// <summary>
         /// RT 非表示ユーザーを更新します
         /// </summary>
         /// <exception cref="WebApiException"/>
-        public void RefreshNoRetweetIds()
+        public async Task RefreshNoRetweetIds()
         {
             if (MyCommon._endingFlag) return;
 
-            this.noRTId = this.NoRetweetIdsApi();
+            this.noRTId = await this.Api.NoRetweetIds()
+                .ConfigureAwait(false);
 
             this._GetNoRetweetResult = true;
         }
 
-        private long[] NoRetweetIdsApi()
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.NoRetweetIds(ref content);
-            }
-            catch(Exception e)
-            {
-                throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                return MyCommon.CreateDataFromJson<long[]>(content);
-            }
-            catch(SerializationException e)
-            {
-                var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-            catch(Exception e)
-            {
-                var ex = new WebApiException("Err:Invalid Json!", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-        }
-
         public bool GetNoRetweetSuccess
         {
             get
@@ -2388,297 +1443,97 @@ namespace OpenTween
         /// t.co の文字列長などの設定情報を更新します
         /// </summary>
         /// <exception cref="WebApiException"/>
-        public void RefreshConfiguration()
-        {
-            this.Configuration = this.ConfigurationApi();
-        }
-
-        private TwitterConfiguration ConfigurationApi()
+        public async Task RefreshConfiguration()
         {
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.GetConfiguration(ref content);
-            }
-            catch(Exception e)
-            {
-                throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
-            }
-
-            this.CheckStatusCode(res, content);
+            this.Configuration = await this.Api.Configuration()
+                .ConfigureAwait(false);
 
-            try
-            {
-                return TwitterConfiguration.ParseJson(content);
-            }
-            catch(SerializationException e)
-            {
-                var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-            catch(Exception e)
-            {
-                var ex = new WebApiException("Err:Invalid Json!", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
+            // TextConfiguration 相当の JSON を得る API が存在しないため、TransformedURLLength のみ help/configuration.json に合わせて更新する
+            this.TextConfiguration.TransformedURLLength = this.Configuration.ShortUrlLengthHttps;
         }
 
-        public void GetListsApi()
+        public async Task GetListsApi()
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            IEnumerable<ListElement> lists;
-            var content = "";
-
-            try
-            {
-                res = twCon.GetLists(this.Username, ref content);
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                lists = TwitterList.ParseJsonArray(content)
-                    .Select(x => new ListElement(x, this));
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-
-            try
-            {
-                res = twCon.GetListsSubscriptions(this.Username, ref content);
-            }
-            catch (Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                lists = lists.Concat(TwitterList.ParseJsonArray(content)
-                    .Select(x => new ListElement(x, this)));
-            }
-            catch (SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
-
-            TabInformations.GetInstance().SubscribableLists = lists.ToList();
-        }
-
-        public void DeleteList(string list_id)
-        {
-            HttpStatusCode res;
-            var content = "";
-
-            try
-            {
-                res = twCon.DeleteListID(this.Username, list_id, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            this.CheckStatusCode(res, content);
-        }
-
-        public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
-        {
-            HttpStatusCode res;
-            var content = "";
-
-            try
-            {
-                res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
+            var ownedLists = await TwitterLists.GetAllItemsAsync(x =>
+                this.Api.ListsOwnerships(this.Username, cursor: x, count: 1000))
+                    .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, content);
+            var subscribedLists = await TwitterLists.GetAllItemsAsync(x =>
+                this.Api.ListsSubscriptions(this.Username, cursor: x, count: 1000))
+                    .ConfigureAwait(false);
 
-            try
-            {
-                var le = TwitterList.ParseJson(content);
-                return  new ListElement(le, this);
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
+            TabInformations.GetInstance().SubscribableLists = Enumerable.Concat(ownedLists, subscribedLists)
+                .Select(x => new ListElement(x, this))
+                .ToList();
         }
 
-        public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
+        public async Task DeleteList(long listId)
         {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message);
-            }
-
-            this.CheckStatusCode(res, content);
+            await this.Api.ListsDestroy(listId)
+                .IgnoreResponse()
+                .ConfigureAwait(false);
 
-            try
-            {
-                var users = TwitterUsers.ParseJson(content);
-                Array.ForEach<TwitterUser>(
-                    users.Users,
-                    u => lists.Add(new UserInfo(u)));
+            var tabinfo = TabInformations.GetInstance();
 
-                return users.NextCursor;
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
+            tabinfo.SubscribableLists = tabinfo.SubscribableLists
+                .Where(x => x.Id != listId)
+                .ToList();
         }
 
-        public void CreateListApi(string listName, bool isPrivate, string description)
+        public async Task<ListElement> EditList(long listId, string new_name, bool isPrivate, string description)
         {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.CreateLists(listName, isPrivate, description, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
+            var response = await this.Api.ListsUpdate(listId, new_name, description, isPrivate)
+                .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, content);
+            var list = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
 
-            try
-            {
-                var le = TwitterList.ParseJson(content);
-                TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
-            }
-            catch(SerializationException ex)
-            {
-                MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
-                throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-            }
-            catch(Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                throw new WebApiException("Err:Invalid Json!", content, ex);
-            }
+            return new ListElement(list, this);
         }
 
-        public bool ContainsUserAtList(string listId, string user)
+        public async Task<long> GetListMembers(long listId, List<UserInfo> lists, long cursor)
         {
             this.CheckAccountState();
 
-            HttpStatusCode res;
-            var content = "";
-
-            try
-            {
-                res = this.twCon.ShowListMember(listId, user, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
-
-            if (res == HttpStatusCode.NotFound)
-            {
-                return false;
-            }
+            var users = await this.Api.ListsMembers(listId, cursor)
+                .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, content);
+            Array.ForEach(users.Users, u => lists.Add(new UserInfo(u)));
 
-            try
-            {
-                TwitterUser.ParseJson(content);
-                return true;
-            }
-            catch(Exception)
-            {
-                return false;
-            }
+            return users.NextCursor;
         }
 
-        public void AddUserToList(string listId, string user)
+        public async Task CreateListApi(string listName, bool isPrivate, string description)
         {
-            HttpStatusCode res;
-            var content = "";
+            this.CheckAccountState();
 
-            try
-            {
-                res = twCon.CreateListMembers(listId, user, ref content);
-            }
-            catch(Exception ex)
-            {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
-            }
+            var response = await this.Api.ListsCreate(listName, description, isPrivate)
+                .ConfigureAwait(false);
+
+            var list = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, content);
+            TabInformations.GetInstance().SubscribableLists.Add(new ListElement(list, this));
         }
 
-        public void RemoveUserToList(string listId, string user)
+        public async Task<bool> ContainsUserAtList(long listId, string user)
         {
-            HttpStatusCode res;
-            var content = "";
+            this.CheckAccountState();
 
             try
             {
-                res = twCon.DeleteListMembers(listId, user, ref content);
+                await this.Api.ListsMembersShow(listId, user)
+                    .ConfigureAwait(false);
+
+                return true;
             }
-            catch(Exception ex)
+            catch (TwitterApiException ex)
+                when (ex.ErrorResponse.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
             {
-                throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
+                return false;
             }
-
-            this.CheckStatusCode(res, content);
         }
 
         public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
@@ -2716,10 +1571,10 @@ namespace OpenTween
                                     //    .Where(v => v.ContentType == "video/mp4")
                                     //    .OrderByDescending(v => v.Bitrate)
                                     //    .Select(v => v.Url).FirstOrDefault();
-                                    media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
+                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
                                 }
                                 else
-                                    media.Add(new MediaInfo(ent.MediaUrl));
+                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
                             }
                         }
                     }
@@ -2740,10 +1595,10 @@ namespace OpenTween
         /// <summary>
         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
         /// </summary>
-        public static Tuple<string, Uri> ParseSource(string sourceHtml)
+        internal static (string SourceText, Uri SourceUri) ParseSource(string sourceHtml)
         {
             if (string.IsNullOrEmpty(sourceHtml))
-                return Tuple.Create<string, Uri>("", null);
+                return ("", null);
 
             string sourceText;
             Uri sourceUri;
@@ -2770,47 +1625,28 @@ namespace OpenTween
                 sourceUri = null;
             }
 
-            return Tuple.Create(sourceText, sourceUri);
+            return (sourceText, sourceUri);
         }
 
-        public TwitterApiStatus GetInfoApi()
+        public async Task<TwitterApiStatus> GetInfoApi()
         {
             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
 
             if (MyCommon._endingFlag) return null;
 
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.RateLimitStatus(ref content);
-            }
-            catch (Exception)
-            {
-                this.ResetApiStatus();
-                return null;
-            }
+            var limits = await this.Api.ApplicationRateLimitStatus()
+                .ConfigureAwait(false);
 
-            this.CheckStatusCode(res, content);
+            MyCommon.TwitterApiInfo.UpdateFromJson(limits);
 
-            try
-            {
-                MyCommon.TwitterApiInfo.UpdateFromJson(content);
-                return MyCommon.TwitterApiInfo;
-            }
-            catch (Exception ex)
-            {
-                MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
-                MyCommon.TwitterApiInfo.Reset();
-                return null;
-            }
+            return MyCommon.TwitterApiInfo;
         }
 
         /// <summary>
         /// ブロック中のユーザーを更新します
         /// </summary>
         /// <exception cref="WebApiException"/>
-        public void RefreshBlockIds()
+        public async Task RefreshBlockIds()
         {
             if (MyCommon._endingFlag) return;
 
@@ -2818,7 +1654,9 @@ namespace OpenTween
             var newBlockIds = new HashSet<long>();
             do
             {
-                var ret = this.GetBlockIdsApi(cursor);
+                var ret = await this.Api.BlocksIds(cursor)
+                    .ConfigureAwait(false);
+
                 newBlockIds.UnionWith(ret.Ids);
                 cursor = ret.NextCursor;
             } while (cursor != 0);
@@ -2828,41 +1666,6 @@ namespace OpenTween
             TabInformations.GetInstance().BlockIds = newBlockIds;
         }
 
-        public TwitterIds GetBlockIdsApi(long cursor)
-        {
-            this.CheckAccountState();
-
-            HttpStatusCode res;
-            var content = "";
-            try
-            {
-                res = twCon.GetBlockUserIds(ref content, cursor);
-            }
-            catch(Exception e)
-            {
-                throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
-            }
-
-            this.CheckStatusCode(res, content);
-
-            try
-            {
-                return TwitterIds.ParseJson(content);
-            }
-            catch(SerializationException e)
-            {
-                var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-            catch(Exception e)
-            {
-                var ex = new WebApiException("Err:Invalid Json!", content, e);
-                MyCommon.TraceOut(ex);
-                throw ex;
-            }
-        }
-
         /// <summary>
         /// ミュート中のユーザーIDを更新します
         /// </summary>
@@ -2871,39 +1674,12 @@ namespace OpenTween
         {
             if (MyCommon._endingFlag) return;
 
-            var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
+            var ids = await TwitterIds.GetAllItemsAsync(x => this.Api.MutesUsersIds(x))
                 .ConfigureAwait(false);
 
             TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
         }
 
-        public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
-        {
-            var content = "";
-
-            try
-            {
-                var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
-                    .ConfigureAwait(false);
-
-                this.CheckStatusCode(res, content);
-
-                return TwitterIds.ParseJson(content);
-            }
-            catch (WebException ex)
-            {
-                var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
-                MyCommon.TraceOut(ex2);
-                throw ex2;
-            }
-            catch (SerializationException ex)
-            {
-                var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
-                MyCommon.TraceOut(ex2);
-                throw ex2;
-            }
-        }
-
         public string[] GetHashList()
         {
             string[] hashArray;
@@ -2916,20 +1692,10 @@ namespace OpenTween
         }
 
         public string AccessToken
-        {
-            get
-            {
-                return twCon.AccessToken;
-            }
-        }
+            => ((TwitterApiConnection)this.Api.Connection).AccessToken;
 
         public string AccessTokenSecret
-        {
-            get
-            {
-                return twCon.AccessTokenSecret;
-            }
-        }
+            => ((TwitterApiConnection)this.Api.Connection).AccessSecret;
 
         private void CheckAccountState()
         {
@@ -2943,57 +1709,16 @@ namespace OpenTween
                 throw new WebApiException("Auth Err:try to re-authorization.");
         }
 
-        private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
-            [CallerMemberName] string callerMethodName = "")
-        {
-            if (httpStatus == HttpStatusCode.OK)
-            {
-                Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
-                return;
-            }
-
-            if (string.IsNullOrWhiteSpace(responseText))
-            {
-                if (httpStatus == HttpStatusCode.Unauthorized)
-                    Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
-
-                throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
-            }
-
-            try
-            {
-                var errors = TwitterError.ParseJson(responseText).Errors;
-                if (errors == null || !errors.Any())
-                {
-                    throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
-                }
-
-                foreach (var error in errors)
-                {
-                    if (error.Code == TwitterErrorCode.InvalidToken ||
-                        error.Code == TwitterErrorCode.SuspendedAccount)
-                    {
-                        Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
-                    }
-                }
-
-                throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
-            }
-            catch (SerializationException) { }
-
-            throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
-        }
-
         public int GetTextLengthRemain(string postText)
         {
             var matchDm = Twitter.DMSendTextRegex.Match(postText);
             if (matchDm.Success)
-                return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
+                return this.GetTextLengthRemainDM(matchDm.Groups["body"].Value);
 
-            return this.GetTextLengthRemainInternal(postText, isDm: false);
+            return this.GetTextLengthRemainWeighted(postText);
         }
 
-        private int GetTextLengthRemainInternal(string postText, bool isDm)
+        private int GetTextLengthRemainDM(string postText)
         {
             var textLength = 0;
 
@@ -3018,10 +1743,54 @@ namespace OpenTween
                 textLength += shortUrlLength - url.Length;
             }
 
-            if (isDm)
-                return this.Configuration.DmTextCharacterLimit - textLength;
-            else
-                return 140 - textLength;
+            return this.Configuration.DmTextCharacterLimit - textLength;
+        }
+
+        private int GetTextLengthRemainWeighted(string postText)
+        {
+            var config = this.TextConfiguration;
+            var totalWeight = 0;
+
+            var urls = TweetExtractor.ExtractUrlEntities(postText).ToArray();
+
+            var pos = 0;
+            while (pos < postText.Length)
+            {
+                var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == pos);
+                if (urlEntity != null)
+                {
+                    totalWeight += config.TransformedURLLength * config.Scale;
+
+                    var urlLength = urlEntity.Indices[1] - urlEntity.Indices[0];
+                    pos += urlLength;
+
+                    continue;
+                }
+
+                var codepoint = postText.GetCodepointAtSafe(pos);
+                var weight = config.DefaultWeight;
+
+                foreach (var weightRange in config.Ranges)
+                {
+                    if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
+                    {
+                        weight = weightRange.Weight;
+                        break;
+                    }
+                }
+
+                totalWeight += weight;
+
+                var isSurrogatePair = codepoint > 0xffff;
+                if (isSurrogatePair)
+                    pos += 2; // サロゲートペアの場合は2文字分進める
+                else
+                    pos++;
+            }
+
+            var remainWeight = config.MaxWeightedTweetLength * config.Scale - totalWeight;
+
+            return remainWeight / config.Scale;
         }
 
 
@@ -3224,11 +1993,27 @@ namespace OpenTween
 
                 if (isDm)
                 {
-                    CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
+                    try
+                    {
+                        var message = TwitterStreamEventDirectMessage.ParseJson(line).DirectMessage;
+                        this.CreateDirectMessagesFromJson(new[] { message }, MyCommon.WORKERTYPE.UserStream, false);
+                    }
+                    catch (SerializationException ex)
+                    {
+                        throw TwitterApiException.CreateFromException(ex, line);
+                    }
                 }
                 else
                 {
-                    CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
+                    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)
@@ -3236,6 +2021,10 @@ namespace OpenTween
                 MyCommon.TraceOut(ex);
                 return;
             }
+            catch (XmlException)
+            {
+                MyCommon.TraceOut("XmlException (StatusArrived): " + line);
+            }
             catch(NullReferenceException)
             {
                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
@@ -3287,11 +2076,11 @@ namespace OpenTween
             evt.Username = eventData.Source.ScreenName;
             evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
 
-            MyCommon.EVENTTYPE eventType;
-            eventTable.TryGetValue(eventData.Event, out eventType);
+            eventTable.TryGetValue(eventData.Event, out var eventType);
             evt.Eventtype = eventType;
 
-            TwitterStreamEvent<TwitterStatus> tweetEvent;
+            TwitterStreamEvent<TwitterStatusCompat> tweetEvent;
+            TwitterStatus tweet;
 
             switch (eventData.Event)
             {
@@ -3319,11 +2108,12 @@ namespace OpenTween
                     return;
                 case "favorite":
                 case "unfavorite":
-                    tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
-                    evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
-                    evt.Id = tweetEvent.TargetObject.Id;
+                    tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
+                    tweet = tweetEvent.TargetObject.Normalize();
+                    evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
+                    evt.Id = tweet.Id;
 
-                    if (SettingCommon.Instance.IsRemoveSameEvent)
+                    if (SettingManager.Common.IsRemoveSameEvent)
                     {
                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
                             return;
@@ -3331,16 +2121,14 @@ namespace OpenTween
 
                     var tabinfo = TabInformations.GetInstance();
 
-                    PostClass post;
-                    var statusId = tweetEvent.TargetObject.Id;
-                    if (!tabinfo.Posts.TryGetValue(statusId, out post))
+                    var statusId = tweet.Id;
+                    if (!tabinfo.Posts.TryGetValue(statusId, out var post))
                         break;
 
                     if (eventData.Event == "favorite")
                     {
                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
-                        if (!favTab.Contains(post.StatusId))
-                            favTab.AddPostImmediately(post.StatusId, post.IsRead);
+                        favTab.AddPostQueue(post);
 
                         if (tweetEvent.Source.Id == this.UserId)
                         {
@@ -3350,7 +2138,7 @@ namespace OpenTween
                         {
                             post.FavoritedCount++;
 
-                            if (SettingCommon.Instance.FavEventUnread)
+                            if (SettingManager.Common.FavEventUnread)
                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
                         }
                     }
@@ -3369,11 +2157,12 @@ namespace OpenTween
                 case "quoted_tweet":
                     if (evt.IsMe) return;
 
-                    tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
-                    evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
-                    evt.Id = tweetEvent.TargetObject.Id;
+                    tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
+                    tweet = tweetEvent.TargetObject.Normalize();
+                    evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
+                    evt.Id = tweet.Id;
 
-                    if (SettingCommon.Instance.IsRemoveSameEvent)
+                    if (SettingManager.Common.IsRemoveSameEvent)
                     {
                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
                             return;
@@ -3436,40 +2225,32 @@ namespace OpenTween
             this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
         }
 
-        public bool UserStreamEnabled
-        {
-            get
-            {
-                return userStream == null ? false : userStream.Enabled;
-            }
-        }
+        public bool UserStreamActive
+            => this.userStream != null && this.userStream.IsStreamActive;
 
         public void StartUserStream()
         {
-            if (userStream != null)
-            {
-                StopUserStream();
-            }
-            userStream = new TwitterUserstream(twCon);
-            userStream.StatusArrived += userStream_StatusArrived;
-            userStream.Started += userStream_Started;
-            userStream.Stopped += userStream_Stopped;
-            userStream.Start(this.AllAtReply, this.TrackWord);
+            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()
         {
-            userStream?.Dispose();
-            userStream = null;
-            if (!MyCommon._endingFlag)
-            {
-                this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
-            }
+            var oldStream = Interlocked.Exchange(ref this.userStream, null);
+            oldStream?.Dispose();
         }
 
         public void ReconnectUserStream()
         {
-            if (userStream != null)
+            if (this.userStream != null)
             {
                 this.StartUserStream();
             }
@@ -3477,222 +2258,134 @@ namespace OpenTween
 
         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 HttpTwitter twCon;
 
-            private Thread _streamThread;
-            private bool _streamActive;
+            private TwitterApi twitterApi;
 
-            private bool _allAtreplies = false;
-            private string _trackwords = "";
+            private Task streamTask;
+            private CancellationTokenSource streamCts;
 
-            public TwitterUserstream(HttpTwitter twitterConnection)
+            public TwitterUserstream(TwitterApi twitterApi)
             {
-                twCon = (HttpTwitter)twitterConnection.Clone();
+                this.twitterApi = twitterApi;
             }
 
             public void Start(bool allAtReplies, string trackwords)
             {
                 this.AllAtReplies = allAtReplies;
                 this.TrackWords = trackwords;
-                _streamActive = true;
-                if (_streamThread != null && _streamThread.IsAlive) return;
-                _streamThread = new Thread(UserStreamLoop);
-                _streamThread.Name = "UserStreamReceiver";
-                _streamThread.IsBackground = true;
-                _streamThread.Start();
-            }
 
-            public bool Enabled
-            {
-                get
-                {
-                    return _streamActive;
-                }
-            }
+                var cts = new CancellationTokenSource();
 
-            public bool AllAtReplies
-            {
-                get
+                this.streamCts = cts;
+                this.streamTask = Task.Run(async () =>
                 {
-                    return _allAtreplies;
-                }
-                set
-                {
-                    _allAtreplies = value;
-                }
+                    try
+                    {
+                        await this.UserStreamLoop(cts.Token)
+                            .ConfigureAwait(false);
+                    }
+                    catch (OperationCanceledException) { }
+                });
             }
 
-            public string TrackWords
+            public void Stop()
             {
-                get
-                {
-                    return _trackwords;
-                }
-                set
-                {
-                    _trackwords = value;
-                }
+                this.streamCts?.Cancel();
+
+                // streamTask の完了を待たずに IsStreamActive を false にセットする
+                this.IsStreamActive = false;
+                this.Stopped?.Invoke();
             }
 
-            private void UserStreamLoop()
+            private async Task UserStreamLoop(CancellationToken cancellationToken)
             {
-                var sleepSec = 0;
-                do
+                TimeSpan sleep = TimeSpan.Zero;
+                for (;;)
                 {
-                    Stream st = null;
-                    StreamReader sr = null;
-                    try
+                    if (sleep != TimeSpan.Zero)
                     {
-                        if (!MyCommon.IsNetworkAvailable())
-                        {
-                            sleepSec = 30;
-                            continue;
-                        }
+                        await Task.Delay(sleep, cancellationToken)
+                            .ConfigureAwait(false);
+                        sleep = TimeSpan.Zero;
+                    }
 
-                        Started?.Invoke();
+                    if (!MyCommon.IsNetworkAvailable())
+                    {
+                        sleep = TimeSpan.FromSeconds(30);
+                        continue;
+                    }
 
-                        var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
+                    this.IsStreamActive = true;
+                    this.Started?.Invoke();
 
-                        switch (res)
-                        {
-                            case HttpStatusCode.OK:
-                                Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
-                                break;
-                            case HttpStatusCode.Unauthorized:
-                                Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
-                                sleepSec = 120;
-                                continue;
-                        }
+                    try
+                    {
+                        var replies = this.AllAtReplies ? "all" : null;
 
-                        if (st == null)
+                        using (var stream = await this.twitterApi.UserStreams(replies, this.TrackWords)
+                            .ConfigureAwait(false))
+                        using (var reader = new StreamReader(stream))
                         {
-                            sleepSec = 30;
-                            //MyCommon.TraceOut("Stop:stream is null")
-                            continue;
-                        }
+                            while (!reader.EndOfStream)
+                            {
+                                var line = await reader.ReadLineAsync()
+                                    .ConfigureAwait(false);
 
-                        sr = new StreamReader(st);
+                                cancellationToken.ThrowIfCancellationRequested();
 
-                        while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
-                        {
-                            StatusArrived?.Invoke(sr.ReadLine());
-                            //this.LastTime = Now;
+                                this.StatusArrived?.Invoke(line);
+                            }
                         }
 
-                        if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
-                        {
-                            sleepSec = 30;
-                            //MyCommon.TraceOut("Stop:EndOfStream")
-                            continue;
-                        }
-                        break;
-                    }
-                    catch(WebException ex)
-                    {
-                        if (ex.Status == WebExceptionStatus.Timeout)
-                        {
-                            sleepSec = 30;                        //MyCommon.TraceOut("Stop:Timeout")
-                        }
-                        else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
-                        {
-                            //MyCommon.TraceOut("Stop:Connection Limit")
-                            break;
-                        }
-                        else
-                        {
-                            sleepSec = 30;
-                            //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
-                        }
-                    }
-                    catch(ThreadAbortException)
-                    {
-                        break;
-                    }
-                    catch(IOException)
-                    {
-                        sleepSec = 30;
-                        //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
+                        // キャンセルされていないのにストリームが終了した場合
+                        sleep = TimeSpan.FromSeconds(30);
                     }
-                    catch(ArgumentException ex)
+                    catch (TwitterApiException) { sleep = TimeSpan.FromSeconds(30); }
+                    catch (IOException) { sleep = TimeSpan.FromSeconds(30); }
+                    catch (OperationCanceledException)
                     {
-                        //System.ArgumentException: ストリームを読み取れませんでした。
-                        //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
-                        sleepSec = 30;
-                        MyCommon.TraceOut(ex, "Stop:ArgumentException");
+                        if (cancellationToken.IsCancellationRequested)
+                            throw;
+
+                        // cancellationToken によるキャンセルではない(=タイムアウトエラー)
+                        sleep = TimeSpan.FromSeconds(30);
                     }
-                    catch(Exception ex)
+                    catch (Exception ex)
                     {
-                        MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
                         MyCommon.ExceptionOut(ex);
-                        sleepSec = 30;
+                        sleep = TimeSpan.FromSeconds(30);
                     }
                     finally
                     {
-                        if (_streamActive)
-                        {
-                            Stopped?.Invoke();
-                        }
-                        twCon.RequestAbort();
-                        sr?.Close();
-                        if (sleepSec > 0)
-                        {
-                            var ms = 0;
-                            while (_streamActive && ms < sleepSec * 1000)
-                            {
-                                Thread.Sleep(500);
-                                ms += 500;
-                            }
-                        }
-                        sleepSec = 0;
+                        this.IsStreamActive = false;
+                        this.Stopped?.Invoke();
                     }
-                } while (this._streamActive);
-
-                if (_streamActive)
-                {
-                    Stopped?.Invoke();
                 }
-                MyCommon.TraceOut("Stop:EndLoop");
             }
 
-#region "IDisposable Support"
-            private bool disposedValue; // 重複する呼び出しを検出するには
+            private bool disposed = false;
 
-            // IDisposable
-            protected virtual void Dispose(bool disposing)
+            public void Dispose()
             {
-                if (!this.disposedValue)
-                {
-                    if (disposing)
-                    {
-                        _streamActive = false;
-                        if (_streamThread != null && _streamThread.IsAlive)
-                        {
-                            _streamThread.Abort();
-                        }
-                    }
-                }
-                this.disposedValue = true;
-            }
+                if (this.disposed)
+                    return;
 
-            //protected Overrides void Finalize()
-            //{
-            //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
-            //    Dispose(false)
-            //    MyBase.Finalize()
-            //}
+                this.disposed = true;
 
-            // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
-            public void Dispose()
-            {
-                // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
-                Dispose(true);
-                GC.SuppressFinalize(this);
-            }
-#endregion
+                this.Stop();
 
+                this.Started = null;
+                this.Stopped = null;
+                this.StatusArrived = null;
+            }
         }
 #endregion