OSDN Git Service

画像を添付したDMの送信に対応
[opentween/open-tween.git] / OpenTween / Twitter.cs
index 56adcb8..f7a3eed 100644 (file)
@@ -44,13 +44,11 @@ using System.Xml.XPath;
 using System;
 using System.Reflection;
 using System.Collections.Generic;
-using System.Drawing;
 using System.Windows.Forms;
 using OpenTween.Api;
 using OpenTween.Api.DataModel;
 using OpenTween.Connection;
 using OpenTween.Models;
-using System.Drawing.Imaging;
 using OpenTween.Setting;
 
 namespace OpenTween
@@ -136,8 +134,8 @@ namespace OpenTween
         /// attachment_url に指定可能な URL を判定する正規表現
         /// </summary>
         public static readonly Regex AttachmentUrlRegex = new Regex(@"https?://(
-   twitter\.com/[0-9A-Za-z]+/status/[0-9]+
- | mobile\.twitter\.com/[0-9A-Za-z]+/status/[0-9]+
+   twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
+ | mobile\.twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
  | twitter\.com/messages/compose\?recipient_id=[0-9]+(&.+)?
 )$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
 
@@ -160,17 +158,15 @@ namespace OpenTween
         public TwitterConfiguration Configuration { get; private set; }
         public TwitterTextConfiguration TextConfiguration { get; private set; }
 
+        public bool GetFollowersSuccess { get; private set; } = false;
+        public bool GetNoRetweetSuccess { get; private set; } = false;
+
         delegate void GetIconImageDelegate(PostClass post);
         private readonly object LockObj = new object();
         private ISet<long> followerId = new HashSet<long>();
-        private bool _GetFollowerResult = false;
         private long[] noRTId = new long[0];
-        private bool _GetNoRetweetResult = false;
 
         //プロパティからアクセスされる共通情報
-        private string _uname;
-
-        private bool _readOwnPost;
         private List<string> _hashList = new List<string>();
 
         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
@@ -195,17 +191,10 @@ namespace OpenTween
         }
 
         public TwitterApiAccessLevel AccessLevel
-        {
-            get
-            {
-                return MyCommon.TwitterApiInfo.AccessLevel;
-            }
-        }
+            => MyCommon.TwitterApiInfo.AccessLevel;
 
         protected void ResetApiStatus()
-        {
-            MyCommon.TwitterApiInfo.Reset();
-        }
+            => MyCommon.TwitterApiInfo.Reset();
 
         public void ClearAuthInfo()
         {
@@ -243,11 +232,10 @@ namespace OpenTween
             }
             this.ResetApiStatus();
             this.Api.Initialize(token, tokenSecret, userId, username);
-            _uname = username.ToLowerInvariant();
             if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream();
         }
 
-        public string PreProcessUrl(string orgData)
+        internal static string PreProcessUrl(string orgData)
         {
             int posl1;
             var posl2 = 0;
@@ -293,17 +281,15 @@ namespace OpenTween
 
             if (Twitter.DMSendTextRegex.IsMatch(param.Text))
             {
-                await this.SendDirectMessage(param.Text)
+                var mediaId = param.MediaIds != null && param.MediaIds.Any() ? param.MediaIds[0] : (long?)null;
+
+                await this.SendDirectMessage(param.Text, mediaId)
                     .ConfigureAwait(false);
                 return;
             }
 
-            var autoPopulateReplyMetadata = false;
-            if (param.InReplyToStatusId != null && !param.Text.Contains("RT @"))
-                autoPopulateReplyMetadata = true;
-
             var response = await this.Api.StatusesUpdate(param.Text, param.InReplyToStatusId, param.MediaIds,
-                    autoPopulateReplyMetadata, param.ExcludeReplyUserIds, param.AttachmentUrl)
+                    param.AutoPopulateReplyMetadata, param.ExcludeReplyUserIds, param.AttachmentUrl)
                 .ConfigureAwait(false);
 
             var status = await response.LoadJsonAsync()
@@ -317,93 +303,88 @@ namespace OpenTween
             this.previousStatusId = status.Id;
         }
 
-        public Task<long> UploadMedia(IMediaItem item)
-            => this.UploadMedia(item, SettingManager.Common.AlphaPNGWorkaround);
-
-        public async Task<long> UploadMedia(IMediaItem item, bool alphaPNGWorkaround)
+        public async Task<long> UploadMedia(IMediaItem item, string mediaCategory = null)
         {
             this.CheckAccountState();
 
-            LazyJson<TwitterUploadMediaResult> response;
+            string mediaType;
 
-            using (var origImage = item.CreateImage())
+            switch (item.Extension)
             {
-                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);
-                }
+                case ".png":
+                    mediaType = "image/png";
+                    break;
+                case ".jpg":
+                case ".jpeg":
+                    mediaType = "image/jpeg";
+                    break;
+                case ".gif":
+                    mediaType = "image/gif";
+                    break;
+                default:
+                    mediaType = "application/octet-stream";
+                    break;
             }
 
-            var media = await response.LoadJsonAsync()
+            var initResponse = await this.Api.MediaUploadInit(item.Size, mediaType, mediaCategory)
                 .ConfigureAwait(false);
 
-            return media.MediaId;
-        }
+            var initMedia = await initResponse.LoadJsonAsync()
+                .ConfigureAwait(false);
 
-        /// <summary>
-        /// pic.twitter.com アップロード時に JPEG への変換を回避するための加工を行う
-        /// </summary>
-        /// <remarks>
-        /// pic.twitter.com へのアップロード時に、アルファチャンネルを持たない PNG 画像が
-        /// JPEG 形式に変換され画質が低下する問題を回避します。
-        /// PNG 以外の画像や、すでにアルファチャンネルを持つ PNG 画像に対しては何もしません。
-        /// </remarks>
-        /// <returns>加工が行われた場合は true、そうでない場合は false</returns>
-        private bool AddAlphaChannelIfNeeded(Image origImage, out MemoryImage newImage)
-        {
-            newImage = null;
-
-            // PNG 画像以外に対しては何もしない
-            if (origImage.RawFormat.Guid != ImageFormat.Png.Guid)
-                return false;
+            var mediaId = initMedia.MediaId;
+
+            await this.Api.MediaUploadAppend(mediaId, 0, item)
+                .ConfigureAwait(false);
 
-            using (var bitmap = new Bitmap(origImage))
+            var response = await this.Api.MediaUploadFinalize(mediaId)
+                .ConfigureAwait(false);
+
+            var media = await response.LoadJsonAsync()
+                .ConfigureAwait(false);
+
+            while (media.ProcessingInfo is TwitterUploadMediaResult.MediaProcessingInfo processingInfo)
             {
-                // アルファ値が 255 以外のピクセルが含まれていた場合は何もしない
-                foreach (var x in Enumerable.Range(0, bitmap.Width))
+                switch (processingInfo.State)
                 {
-                    foreach (var y in Enumerable.Range(0, bitmap.Height))
-                    {
-                        if (bitmap.GetPixel(x, y).A != 255)
-                            return false;
-                    }
+                    case "pending":
+                        break;
+                    case "in_progress":
+                        break;
+                    case "succeeded":
+                        goto succeeded;
+                    case "failed":
+                        throw new WebApiException($"Err:Upload failed ({processingInfo.Error?.Name})");
+                    default:
+                        throw new WebApiException($"Err:Invalid state ({processingInfo.State})");
                 }
 
-                // 左上の 1px だけアルファ値を 254 にする
-                var pixel = bitmap.GetPixel(0, 0);
-                var newPixel = Color.FromArgb(pixel.A - 1, pixel.R, pixel.G, pixel.B);
-                bitmap.SetPixel(0, 0, newPixel);
-
-                // MemoryImage 作成時に画像はコピーされるため、この後 bitmap は破棄しても問題ない
-                newImage = MemoryImage.CopyFromImage(bitmap);
+                await Task.Delay(TimeSpan.FromSeconds(processingInfo.CheckAfterSecs ?? 5))
+                    .ConfigureAwait(false);
 
-                return true;
+                media = await this.Api.MediaUploadStatus(mediaId)
+                    .ConfigureAwait(false);
             }
+
+            succeeded:
+            return media.MediaId;
         }
 
-        public async Task SendDirectMessage(string postStr)
+        public async Task SendDirectMessage(string postStr, long? mediaId = null)
         {
             this.CheckAccountState();
             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
 
             var mc = Twitter.DMSendTextRegex.Match(postStr);
 
-            var response = await this.Api.DirectMessagesNew(mc.Groups["body"].Value, mc.Groups["id"].Value)
-                .ConfigureAwait(false);
+            var body = mc.Groups["body"].Value;
+            var recipientName = mc.Groups["id"].Value;
 
-            var dm = await response.LoadJsonAsync()
+            var recipient = await this.Api.UsersShow(recipientName)
                 .ConfigureAwait(false);
 
-            this.UpdateUserStats(dm.Sender);
+            await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
+                .ConfigureAwait(false);
         }
 
         public async Task PostRetweet(long id, bool read)
@@ -442,7 +423,7 @@ namespace OpenTween
 
             post.IsRead = read;
             post.IsOwl = false;
-            if (_readOwnPost) post.IsRead = true;
+            if (this.ReadOwnPost) post.IsRead = true;
             post.IsDm = false;
 
             TabInformations.GetInstance().AddPost(post);
@@ -454,32 +435,9 @@ namespace OpenTween
         public long UserId
             => this.Api.CurrentUserId;
 
-        private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
-        public static MyCommon.ACCOUNT_STATE AccountState
-        {
-            get
-            {
-                return _accountState;
-            }
-            set
-            {
-                _accountState = value;
-            }
-        }
-
+        public static MyCommon.ACCOUNT_STATE AccountState { get; set; } = MyCommon.ACCOUNT_STATE.Valid;
         public bool RestrictFavCheck { get; set; }
-
-        public bool ReadOwnPost
-        {
-            get
-            {
-                return _readOwnPost;
-            }
-            set
-            {
-                _readOwnPost = value;
-            }
-        }
+        public bool ReadOwnPost { get; set; }
 
         public int FollowersCount { get; private set; }
         public int FriendsCount { get; private set; }
@@ -501,25 +459,19 @@ namespace OpenTween
         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
-        {
-            return count >= 20 && count <= GetMaxApiResultCount(type);
-        }
+            => count >= 20 && count <= GetMaxApiResultCount(type);
 
         /// <summary>
         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyMoreApiResultCount(int count)
-        {
-            return count >= 20 && count <= 200;
-        }
+            => count >= 20 && count <= 200;
 
         /// <summary>
         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyFirstApiResultCount(int count)
-        {
-            return count >= 20 && count <= 200;
-        }
+            => count >= 20 && count <= 200;
 
         /// <summary>
         /// WORKERTYPEに応じた取得可能な最大件数を取得する
@@ -689,7 +641,7 @@ namespace OpenTween
             var item = CreatePostsFromStatusData(status);
 
             item.IsRead = read;
-            if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
+            if (item.IsMe && !read && this.ReadOwnPost) item.IsRead = true;
 
             return item;
         }
@@ -707,9 +659,7 @@ namespace OpenTween
         }
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status)
-        {
-            return CreatePostsFromStatusData(status, false);
-        }
+            => this.CreatePostsFromStatusData(status, false);
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
         {
@@ -771,7 +721,7 @@ namespace OpenTween
                 {
                     post.RetweetedBy = status.User.ScreenName;
                     post.RetweetedByUserId = status.User.Id;
-                    post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+                    post.IsMe = post.RetweetedByUserId == this.UserId;
                 }
                 else
                 {
@@ -813,7 +763,7 @@ namespace OpenTween
                     post.Nickname = user.Name.Trim();
                     post.ImageUrl = user.ProfileImageUrlHttps;
                     post.IsProtect = user.Protected;
-                    post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+                    post.IsMe = post.UserId == this.UserId;
                 }
                 else
                 {
@@ -824,16 +774,24 @@ namespace OpenTween
             }
             //HTMLに整形
             string textFromApi = post.TextFromApi;
-            post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
+
+            var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink;
+
+            if (quotedStatusLink != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded))
+                quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある
+
+            post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink);
             post.TextFromApi = textFromApi;
-            post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
+            post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink);
             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
-            post.AccessibleText = this.CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus);
+            post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink);
             post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
             post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
 
-            post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
+            this.ExtractEntities(entities, post.ReplyToList, post.Media);
+
+            post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink)
                 .Where(x => x != post.StatusId && x != post.RetweetedId)
                 .Distinct().ToArray();
 
@@ -877,10 +835,13 @@ namespace OpenTween
         /// <summary>
         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
         /// </summary>
-        public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
+        public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities, TwitterQuotedStatusPermalink quotedStatusLink)
         {
             var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
 
+            if (quotedStatusLink != null)
+                urls = urls.Concat(new[] { quotedStatusLink.Expanded });
+
             return GetQuoteTweetStatusIds(urls);
         }
 
@@ -926,7 +887,7 @@ namespace OpenTween
                 var post = CreatePostsFromStatusData(status);
 
                 post.IsRead = read;
-                if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
+                if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
 
                 if (tab != null && tab.IsInnerStorageTabType)
                     tab.AddPostQueue(post);
@@ -956,7 +917,7 @@ namespace OpenTween
                 var post = CreatePostsFromStatusData(status);
 
                 post.IsRead = read;
-                if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
+                if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true;
 
                 tab.AddPostQueue(post);
             }
@@ -1115,7 +1076,7 @@ namespace OpenTween
 
             relPosts.Values.ToList().ForEach(p =>
             {
-                if (p.IsMe && !read && this._readOwnPost)
+                if (p.IsMe && !read && this.ReadOwnPost)
                     p.IsRead = true;
                 else
                     p.IsRead = read;
@@ -1185,16 +1146,19 @@ namespace OpenTween
                     //本文
                     var textFromApi = message.Text;
                     //HTMLに整形
-                    post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
-                    post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
+                    post.Text = CreateHtmlAnchor(textFromApi, message.Entities, quotedStatusLink: null);
+                    post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities, quotedStatusLink: null);
                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
-                    post.AccessibleText = this.CreateAccessibleText(textFromApi, message.Entities, quoteStatus: null);
+                    post.AccessibleText = CreateAccessibleText(textFromApi, message.Entities, quotedStatus: null, quotedStatusLink: null);
                     post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
                     post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
                     post.IsFav = false;
 
-                    post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
+                    this.ExtractEntities(message.Entities, post.ReplyToList, post.Media);
+
+                    post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities, quotedStatusLink: null)
+                        .Distinct().ToArray();
 
                     post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
@@ -1258,7 +1222,7 @@ namespace OpenTween
                 }
 
                 post.IsRead = read;
-                if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
+                if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
                 post.IsReply = false;
                 post.IsExcludeReply = false;
                 post.IsDm = true;
@@ -1330,7 +1294,7 @@ namespace OpenTween
                 tab.OldestId = minimumId.Value;
         }
 
-        private string ReplaceTextFromApi(string text, TwitterEntities entities)
+        private string ReplaceTextFromApi(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
         {
             if (entities != null)
             {
@@ -1349,10 +1313,14 @@ namespace OpenTween
                     }
                 }
             }
+
+            if (quotedStatusLink != null)
+                text += " " + quotedStatusLink.Display;
+
             return text;
         }
 
-        private string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quoteStatus)
+        internal static string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quotedStatus, TwitterQuotedStatusPermalink quotedStatusLink)
         {
             if (entities == null)
                 return text;
@@ -1361,19 +1329,19 @@ namespace OpenTween
             {
                 foreach (var entity in entities.Urls)
                 {
-                    if (quoteStatus != null)
+                    if (quotedStatus != null)
                     {
                         var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
-                        if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quoteStatus.IdStr)
+                        if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr)
                         {
-                            var quoteText = this.CreateAccessibleText(quoteStatus.FullText, quoteStatus.MergedEntities, quoteStatus: null);
-                            text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quoteStatus.User.ScreenName, quoteText));
+                            var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
+                            text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText));
+                            continue;
                         }
                     }
-                    else if (!string.IsNullOrEmpty(entity.DisplayUrl))
-                    {
+
+                    if (!string.IsNullOrEmpty(entity.DisplayUrl))
                         text = text.Replace(entity.Url, entity.DisplayUrl);
-                    }
                 }
             }
 
@@ -1392,6 +1360,12 @@ namespace OpenTween
                 }
             }
 
+            if (quotedStatusLink != null)
+            {
+                var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
+                text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText);
+            }
+
             return text;
         }
 
@@ -1420,15 +1394,7 @@ namespace OpenTween
             this.followerId = newFollowerIds;
             TabInformations.GetInstance().RefreshOwl(this.followerId);
 
-            this._GetFollowerResult = true;
-        }
-
-        public bool GetFollowersSuccess
-        {
-            get
-            {
-                return _GetFollowerResult;
-            }
+            this.GetFollowersSuccess = true;
         }
 
         /// <summary>
@@ -1442,15 +1408,7 @@ namespace OpenTween
             this.noRTId = await this.Api.NoRetweetIds()
                 .ConfigureAwait(false);
 
-            this._GetNoRetweetResult = true;
-        }
-
-        public bool GetNoRetweetSuccess
-        {
-            get
-            {
-                return _GetNoRetweetResult;
-            }
+            this.GetNoRetweetSuccess = true;
         }
 
         /// <summary>
@@ -1550,7 +1508,7 @@ namespace OpenTween
             }
         }
 
-        public string CreateHtmlAnchor(string text, List<Tuple<long, string>> AtList, TwitterEntities entities, List<MediaInfo> media)
+        private void ExtractEntities(TwitterEntities entities, List<Tuple<long, string>> AtList, List<MediaInfo> media)
         {
             if (entities != null)
             {
@@ -1574,7 +1532,7 @@ namespace OpenTween
                     {
                         foreach (var ent in entities.Media)
                         {
-                            if (!media.Any(x => x.Url == ent.MediaUrl))
+                            if (!media.Any(x => x.Url == ent.MediaUrlHttps))
                             {
                                 if (ent.VideoInfo != null &&
                                     ent.Type == "animated_gif" || ent.Type == "video")
@@ -1583,22 +1541,32 @@ namespace OpenTween
                                     //    .Where(v => v.ContentType == "video/mp4")
                                     //    .OrderByDescending(v => v.Bitrate)
                                     //    .Select(v => v.Url).FirstOrDefault();
-                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
+                                    media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
                                 }
                                 else
-                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
+                                    media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl: null));
                             }
                         }
                     }
                 }
             }
+        }
 
+        internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
+        {
             // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
             text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
 
             text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
             text = PreProcessUrl(text); //IDN置換
 
+            if (quotedStatusLink != null)
+            {
+                text += string.Format(" <a href=\"{0}\" title=\"{0}\">{1}</a>",
+                    WebUtility.HtmlEncode(quotedStatusLink.Url),
+                    WebUtility.HtmlEncode(quotedStatusLink.Display));
+            }
+
             return text;
         }
 
@@ -1807,43 +1775,21 @@ namespace OpenTween
 
 
 #region "UserStream"
-        private string trackWord_ = "";
-        public string TrackWord
-        {
-            get
-            {
-                return trackWord_;
-            }
-            set
-            {
-                trackWord_ = value;
-            }
-        }
-        private bool allAtReply_ = false;
-        public bool AllAtReply
-        {
-            get
-            {
-                return allAtReply_;
-            }
-            set
-            {
-                allAtReply_ = value;
-            }
-        }
+        public string TrackWord { get; set; } = "";
+        public bool AllAtReply { get; set; } = false;
 
         public event EventHandler NewPostFromStream;
         public event EventHandler UserStreamStarted;
         public event EventHandler UserStreamStopped;
         public event EventHandler<PostDeletedEventArgs> PostDeleted;
         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
-        private DateTime _lastUserstreamDataReceived;
+        private DateTimeUtc _lastUserstreamDataReceived;
         private TwitterUserstream userStream;
 
         public class FormattedEvent
         {
             public MyCommon.EVENTTYPE Eventtype { get; set; }
-            public DateTime CreatedAt { get; set; }
+            public DateTimeUtc CreatedAt { get; set; }
             public string Event { get; set; }
             public string Username { get; set; }
             public string Target { get; set; }
@@ -1851,18 +1797,7 @@ namespace OpenTween
             public bool IsMe { get; set; }
         }
 
-        public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
-        public List<FormattedEvent> StoredEvent
-        {
-            get
-            {
-                return storedEvent_;
-            }
-            set
-            {
-                storedEvent_ = value;
-            }
-        }
+        public List<FormattedEvent> StoredEvent { get; } = new List<FormattedEvent>();
 
         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
         {
@@ -1887,16 +1822,11 @@ namespace OpenTween
         };
 
         public bool IsUserstreamDataReceived
-        {
-            get
-            {
-                return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
-            }
-        }
+            => (DateTimeUtc.Now - this._lastUserstreamDataReceived).TotalSeconds < 31;
 
         private void userStream_StatusArrived(string line)
         {
-            this._lastUserstreamDataReceived = DateTime.Now;
+            this._lastUserstreamDataReceived = DateTimeUtc.Now;
             if (string.IsNullOrEmpty(line)) return;
 
             if (line.First() != '{' || line.Last() != '}')
@@ -2082,14 +2012,14 @@ namespace OpenTween
                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
             }
 
-            var evt = new FormattedEvent();
-            evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
-            evt.Event = eventData.Event;
-            evt.Username = eventData.Source.ScreenName;
-            evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
-
-            eventTable.TryGetValue(eventData.Event, out var eventType);
-            evt.Eventtype = eventType;
+            var evt = new FormattedEvent
+            {
+                CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt),
+                Event = eventData.Event,
+                Username = eventData.Source.ScreenName,
+                IsMe = eventData.Source.Id == this.UserId,
+                Eventtype = eventTable.TryGetValue(eventData.Event, out var eventType) ? eventType : MyCommon.EVENTTYPE.None,
+            };
 
             TwitterStreamEvent<TwitterStatusCompat> tweetEvent;
             TwitterStatus tweet;
@@ -2102,7 +2032,7 @@ namespace OpenTween
                 case "user_suspend":
                     return;
                 case "follow":
-                    if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
+                    if (eventData.Target.Id == this.UserId)
                     {
                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
                     }
@@ -2228,14 +2158,10 @@ namespace OpenTween
         }
 
         private void userStream_Started()
-        {
-            this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
-        }
+            => this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
 
         private void userStream_Stopped()
-        {
-            this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
-        }
+            => this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
 
         public bool UserStreamActive
             => this.userStream != null && this.userStream.IsStreamActive;
@@ -2285,9 +2211,7 @@ namespace OpenTween
             private CancellationTokenSource streamCts;
 
             public TwitterUserstream(TwitterApi twitterApi)
-            {
-                this.twitterApi = twitterApi;
-            }
+                => this.twitterApi = twitterApi;
 
             public void Start(bool allAtReplies, string trackwords)
             {
@@ -2439,9 +2363,7 @@ namespace OpenTween
         public long StatusId { get; }
 
         public PostDeletedEventArgs(long statusId)
-        {
-            this.StatusId = statusId;
-        }
+            => this.StatusId = statusId;
     }
 
     public class UserStreamEventReceivedEventArgs : EventArgs
@@ -2449,8 +2371,6 @@ namespace OpenTween
         public Twitter.FormattedEvent EventData { get; }
 
         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
-        {
-            this.EventData = eventData;
-        }
+            => this.EventData = eventData;
     }
 }