OSDN Git Service

ITwitterCredentialとアクセス手段ごとの具象クラスを追加
[opentween/open-tween.git] / OpenTween / Twitter.cs
index d5aa041..48be5ba 100644 (file)
@@ -43,6 +43,7 @@ using System.Threading.Tasks;
 using System.Windows.Forms;
 using OpenTween.Api;
 using OpenTween.Api.DataModel;
+using OpenTween.Api.GraphQL;
 using OpenTween.Api.TwitterV2;
 using OpenTween.Connection;
 using OpenTween.Models;
@@ -173,9 +174,7 @@ namespace OpenTween
 
         private readonly TwitterPostFactory postFactory;
 
-        private string? nextCursorDirectMessage = null;
-
-        private long previousStatusId = -1L;
+        private string? previousStatusId = null;
 
         public Twitter(TwitterApi api)
         {
@@ -229,6 +228,16 @@ namespace OpenTween
             this.Api.Initialize(token, tokenSecret, userId, username);
         }
 
+        public void Initialize(ITwitterCredential credential, string username, long userId)
+        {
+            // OAuth認証
+            if (credential is TwitterCredentialNone)
+                Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
+
+            this.ResetApiStatus();
+            this.Api.Initialize(credential, userId, username);
+        }
+
         public async Task<PostClass?> PostStatus(PostStatusParams param)
         {
             this.CheckAccountState();
@@ -242,25 +251,44 @@ namespace OpenTween
                 return null;
             }
 
-            var response = await this.Api.StatusesUpdate(
-                    param.Text,
-                    param.InReplyToStatusId,
-                    param.MediaIds,
-                    param.AutoPopulateReplyMetadata,
-                    param.ExcludeReplyUserIds,
-                    param.AttachmentUrl
-                )
-                .ConfigureAwait(false);
+            TwitterStatus status;
 
-            var status = await response.LoadJsonAsync()
-                .ConfigureAwait(false);
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new CreateTweetRequest
+                {
+                    TweetText = param.Text,
+                    InReplyToTweetId = param.InReplyToStatusId?.ToTwitterStatusId(),
+                    ExcludeReplyUserIds = param.ExcludeReplyUserIds.Select(x => x.ToString()).ToArray(),
+                    MediaIds = param.MediaIds.Select(x => x.ToString()).ToArray(),
+                    AttachmentUrl = param.AttachmentUrl,
+                };
+
+                status = await request.Send(this.Api.Connection)
+                    .ConfigureAwait(false);
+            }
+            else
+            {
+                var response = await this.Api.StatusesUpdate(
+                        param.Text,
+                        param.InReplyToStatusId?.ToTwitterStatusId(),
+                        param.MediaIds,
+                        param.AutoPopulateReplyMetadata,
+                        param.ExcludeReplyUserIds,
+                        param.AttachmentUrl
+                    )
+                    .ConfigureAwait(false);
+
+                status = await response.LoadJsonAsync()
+                    .ConfigureAwait(false);
+            }
 
             this.UpdateUserStats(status.User);
 
-            if (status.Id == this.previousStatusId)
+            if (status.IdStr == this.previousStatusId)
                 throw new WebApiException("OK:Delaying?");
 
-            this.previousStatusId = status.Id;
+            this.previousStatusId = status.IdStr;
 
             // 投稿したものを返す
             var post = this.CreatePostsFromStatusData(status);
@@ -268,6 +296,23 @@ namespace OpenTween
             return post;
         }
 
+        public async Task DeleteTweet(TwitterStatusId tweetId)
+        {
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new DeleteTweetRequest
+                {
+                    TweetId = tweetId,
+                };
+                await request.Send(this.Api.Connection);
+            }
+            else
+            {
+                await this.Api.StatusesDestroy(tweetId)
+                    .IgnoreResponse();
+            }
+        }
+
         public async Task<long> UploadMedia(IMediaItem item, string? mediaCategory = null)
         {
             this.CheckAccountState();
@@ -335,7 +380,7 @@ namespace OpenTween
             var body = mc.Groups["body"].Value;
             var recipientName = mc.Groups["id"].Value;
 
-            var recipient = await this.Api.UsersShow(recipientName)
+            var recipient = await this.GetUserInfo(recipientName)
                 .ConfigureAwait(false);
 
             var response = await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
@@ -348,7 +393,7 @@ namespace OpenTween
                 .ConfigureAwait(false);
         }
 
-        public async Task<PostClass?> PostRetweet(long id, bool read)
+        public async Task<PostClass?> PostRetweet(PostId id, bool read)
         {
             this.CheckAccountState();
 
@@ -359,7 +404,17 @@ namespace OpenTween
 
             var target = post.RetweetedId ?? id;  // 再RTの場合は元発言をRT
 
-            var response = await this.Api.StatusesRetweet(target)
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new CreateRetweetRequest
+                {
+                    TweetId = target.ToTwitterStatusId(),
+                };
+                await request.Send(this.Api.Connection).ConfigureAwait(false);
+                return null;
+            }
+
+            var response = await this.Api.StatusesRetweet(target.ToTwitterStatusId())
                 .ConfigureAwait(false);
 
             var status = await response.LoadJsonAsync()
@@ -368,7 +423,8 @@ namespace OpenTween
             // 二重取得回避
             lock (this.lockObj)
             {
-                if (TabInformations.GetInstance().ContainsKey(status.Id))
+                var statusId = new TwitterStatusId(status.IdStr);
+                if (TabInformations.GetInstance().ContainsKey(statusId))
                     return null;
             }
 
@@ -377,17 +433,54 @@ namespace OpenTween
                 throw new WebApiException("Invalid Json!");
 
             // Retweetしたものを返す
-            post = this.CreatePostsFromStatusData(status);
+            return this.CreatePostsFromStatusData(status) with
+            {
+                IsMe = true,
+                IsRead = this.ReadOwnPost ? true : read,
+                IsOwl = false,
+            };
+        }
 
-            // ユーザー情報
-            post.IsMe = true;
+        public async Task DeleteRetweet(PostClass post)
+        {
+            if (post.RetweetedId == null)
+                throw new ArgumentException("post is not retweeted status", nameof(post));
 
-            post.IsRead = read;
-            post.IsOwl = false;
-            if (this.ReadOwnPost) post.IsRead = true;
-            post.IsDm = false;
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new DeleteRetweetRequest
+                {
+                    SourceTweetId = post.RetweetedId.ToTwitterStatusId(),
+                };
+                await request.Send(this.Api.Connection).ConfigureAwait(false);
+            }
+            else
+            {
+                await this.Api.StatusesDestroy(post.StatusId.ToTwitterStatusId())
+                    .IgnoreResponse();
+            }
+        }
 
-            return post;
+        public async Task<TwitterUser> GetUserInfo(string screenName)
+        {
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new UserByScreenNameRequest
+                {
+                    ScreenName = screenName,
+                };
+                var response = await request.Send(this.Api.Connection)
+                    .ConfigureAwait(false);
+
+                return response.ToTwitterUser();
+            }
+            else
+            {
+                var user = await this.Api.UsersShow(screenName)
+                    .ConfigureAwait(false);
+
+                return user;
+            }
         }
 
         public string Username
@@ -516,7 +609,7 @@ namespace OpenTween
                 var request = new GetTimelineRequest(this.UserId)
                 {
                     MaxResults = count,
-                    UntilId = more ? tab.OldestId.ToString() : null,
+                    UntilId = more ? tab.OldestId as TwitterStatusId : null,
                 };
 
                 var response = await request.Send(this.Api.Connection)
@@ -532,15 +625,15 @@ namespace OpenTween
             }
             else
             {
-                var maxId = more ? tab.OldestId : (long?)null;
+                var maxId = more ? tab.OldestId : null;
 
-                statuses = await this.Api.StatusesHomeTimeline(count, maxId)
+                statuses = await this.Api.StatusesHomeTimeline(count, maxId as TwitterStatusId)
                     .ConfigureAwait(false);
             }
 
             var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Timeline, tab, read);
             if (minimumId != null)
-                tab.OldestId = minimumId.Value;
+                tab.OldestId = minimumId;
         }
 
         public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool more, bool startup)
@@ -552,7 +645,7 @@ namespace OpenTween
             TwitterStatus[] statuses;
             if (more)
             {
-                statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId)
+                statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId as TwitterStatusId)
                     .ConfigureAwait(false);
             }
             else
@@ -563,34 +656,57 @@ namespace OpenTween
 
             var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Reply, tab, read);
             if (minimumId != null)
-                tab.OldestId = minimumId.Value;
+                tab.OldestId = minimumId;
         }
 
-        public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTabModel tab, bool more)
+        public async Task GetUserTimelineApi(bool read, UserTimelineTabModel tab, bool more)
         {
             this.CheckAccountState();
 
             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
 
             TwitterStatus[] statuses;
-            if (MyCommon.IsNullOrEmpty(userName))
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
             {
-                var target = tab.ScreenName;
-                if (MyCommon.IsNullOrEmpty(target)) return;
-                userName = target;
-                statuses = await this.Api.StatusesUserTimeline(userName, count)
+                var userId = tab.UserId;
+                if (MyCommon.IsNullOrEmpty(userId))
+                {
+                    var user = await this.GetUserInfo(tab.ScreenName)
+                        .ConfigureAwait(false);
+
+                    userId = user.IdStr;
+                    tab.UserId = user.IdStr;
+                }
+
+                var request = new UserTweetsAndRepliesRequest(userId)
+                {
+                    Count = count,
+                    Cursor = more ? tab.CursorBottom : tab.CursorTop,
+                };
+                var response = await request.Send(this.Api.Connection)
                     .ConfigureAwait(false);
+
+                statuses = response.Tweets
+                    .Where(x => !x.IsTombstone)
+                    .Select(x => x.ToTwitterStatus())
+                    .Where(x => x.User.IdStr == userId) // リプライツリーに含まれる他ユーザーのツイートを除外
+                    .ToArray();
+
+                tab.CursorBottom = response.CursorBottom;
+
+                if (!more)
+                    tab.CursorTop = response.CursorTop;
             }
             else
             {
                 if (more)
                 {
-                    statuses = await this.Api.StatusesUserTimeline(userName, count, maxId: tab.OldestId)
+                    statuses = await this.Api.StatusesUserTimeline(tab.ScreenName, count, maxId: tab.OldestId as TwitterStatusId)
                         .ConfigureAwait(false);
                 }
                 else
                 {
-                    statuses = await this.Api.StatusesUserTimeline(userName, count)
+                    statuses = await this.Api.StatusesUserTimeline(tab.ScreenName, count)
                         .ConfigureAwait(false);
                 }
             }
@@ -598,15 +714,30 @@ namespace OpenTween
             var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
 
             if (minimumId != null)
-                tab.OldestId = minimumId.Value;
+                tab.OldestId = minimumId;
         }
 
-        public async Task<PostClass> GetStatusApi(bool read, long id)
+        public async Task<PostClass> GetStatusApi(bool read, TwitterStatusId id)
         {
             this.CheckAccountState();
 
-            var status = await this.Api.StatusesShow(id)
-                .ConfigureAwait(false);
+            TwitterStatus status;
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new TweetDetailRequest
+                {
+                    FocalTweetId = id,
+                };
+                var tweets = await request.Send(this.Api.Connection).ConfigureAwait(false);
+                status = tweets.Select(x => x.ToTwitterStatus())
+                    .Where(x => x.IdStr == id.Id)
+                    .FirstOrDefault() ?? throw new WebApiException("Empty result set");
+            }
+            else
+            {
+                status = await this.Api.StatusesShow(id)
+                    .ConfigureAwait(false);
+            }
 
             var item = this.CreatePostsFromStatusData(status);
 
@@ -616,7 +747,7 @@ namespace OpenTween
             return item;
         }
 
-        public async Task GetStatusApi(bool read, long id, TabModel tab)
+        public async Task GetStatusApi(bool read, TwitterStatusId id, TabModel tab)
         {
             var post = await this.GetStatusApi(read, id)
                 .ConfigureAwait(false);
@@ -634,33 +765,39 @@ namespace OpenTween
         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
             => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet);
 
-        private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read)
+        private PostId? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read)
         {
-            long? minimumId = null;
+            PostId? minimumId = null;
 
-            foreach (var status in items)
+            var posts = items.Select(x => this.CreatePostsFromStatusData(x)).ToArray();
+
+            TwitterPostFactory.AdjustSortKeyForPromotedPost(posts);
+
+            foreach (var post in posts)
             {
-                if (minimumId == null || minimumId.Value > status.Id)
-                    minimumId = status.Id;
+                if (!post.IsPromoted)
+                {
+                    if (minimumId == null || minimumId > post.StatusId)
+                        minimumId = post.StatusId;
+                }
 
                 // 二重取得回避
                 lock (this.lockObj)
                 {
+                    var id = post.StatusId;
                     if (tab == null)
                     {
-                        if (TabInformations.GetInstance().ContainsKey(status.Id)) continue;
+                        if (TabInformations.GetInstance().ContainsKey(id)) continue;
                     }
                     else
                     {
-                        if (tab.Contains(status.Id)) continue;
+                        if (tab.Contains(id)) continue;
                     }
                 }
 
                 // RT禁止ユーザーによるもの
                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
-                    status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue;
-
-                var post = this.CreatePostsFromStatusData(status);
+                    post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
 
                 post.IsRead = read;
                 if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
@@ -674,24 +811,32 @@ namespace OpenTween
             return minimumId;
         }
 
-        private long? CreatePostsFromSearchJson(TwitterSearchResult items, PublicSearchTabModel tab, bool read, bool more)
+        private PostId? CreatePostsFromSearchJson(TwitterStatus[] statuses, PublicSearchTabModel tab, bool read, bool more)
         {
-            long? minimumId = null;
+            PostId? minimumId = null;
+
+            var posts = statuses.Select(x => this.CreatePostsFromStatusData(x)).ToArray();
 
-            foreach (var status in items.Statuses)
+            TwitterPostFactory.AdjustSortKeyForPromotedPost(posts);
+
+            foreach (var post in posts)
             {
-                if (minimumId == null || minimumId.Value > status.Id)
-                    minimumId = status.Id;
+                if (!post.IsPromoted)
+                {
+                    if (minimumId == null || minimumId > post.StatusId)
+                        minimumId = post.StatusId;
+
+                    if (!more && (tab.SinceId == null || post.StatusId > tab.SinceId))
+                        tab.SinceId = post.StatusId;
+                }
 
-                if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id;
                 // 二重取得回避
                 lock (this.lockObj)
                 {
-                    if (tab.Contains(status.Id)) continue;
+                    if (tab.Contains(post.StatusId))
+                        continue;
                 }
 
-                var post = this.CreatePostsFromStatusData(status);
-
                 post.IsRead = read;
                 if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true;
 
@@ -714,7 +859,8 @@ namespace OpenTween
                 // 二重取得回避
                 lock (this.lockObj)
                 {
-                    if (favTab.Contains(status.Id)) continue;
+                    if (favTab.Contains(new TwitterStatusId(status.IdStr)))
+                        continue;
                 }
 
                 var post = this.CreatePostsFromStatusData(status, true);
@@ -732,9 +878,32 @@ namespace OpenTween
             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
 
             TwitterStatus[] statuses;
-            if (more)
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
+            {
+                var request = new ListLatestTweetsTimelineRequest(tab.ListInfo.Id.ToString())
+                {
+                    Count = count,
+                    Cursor = more ? tab.CursorBottom : tab.CursorTop,
+                };
+                var response = await request.Send(this.Api.Connection)
+                    .ConfigureAwait(false);
+
+                var convertedStatuses = response.Tweets
+                    .Where(x => !x.IsTombstone)
+                    .Select(x => x.ToTwitterStatus());
+
+                if (!SettingManager.Instance.Common.IsListsIncludeRts)
+                    convertedStatuses = convertedStatuses.Where(x => x.RetweetedStatus == null);
+
+                statuses = convertedStatuses.ToArray();
+                tab.CursorBottom = response.CursorBottom;
+
+                if (!more)
+                    tab.CursorTop = response.CursorTop;
+            }
+            else if (more)
             {
-                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Instance.Common.IsListsIncludeRts)
+                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId as TwitterStatusId, includeRTs: SettingManager.Instance.Common.IsListsIncludeRts)
                     .ConfigureAwait(false);
             }
             else
@@ -746,24 +915,24 @@ namespace OpenTween
             var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
 
             if (minimumId != null)
-                tab.OldestId = minimumId.Value;
+                tab.OldestId = minimumId;
         }
 
         /// <summary>
         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
         /// </summary>
         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
-        internal static PostClass FindTopOfReplyChain(IDictionary<long, PostClass> posts, long startStatusId)
+        internal static PostClass FindTopOfReplyChain(IDictionary<PostId, PostClass> posts, PostId startStatusId)
         {
             if (!posts.ContainsKey(startStatusId))
-                throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
+                throw new ArgumentException("startStatusId (" + startStatusId.Id + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
 
             var nextPost = posts[startStatusId];
             while (nextPost.InReplyToStatusId != null)
             {
-                if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
+                if (!posts.ContainsKey(nextPost.InReplyToStatusId))
                     break;
-                nextPost = posts[nextPost.InReplyToStatusId.Value];
+                nextPost = posts[nextPost.InReplyToStatusId];
             }
 
             return nextPost;
@@ -775,14 +944,16 @@ namespace OpenTween
 
             if (targetPost.RetweetedId != null)
             {
-                var originalPost = targetPost.Clone();
-                originalPost.StatusId = targetPost.RetweetedId.Value;
-                originalPost.RetweetedId = null;
-                originalPost.RetweetedBy = null;
+                var originalPost = targetPost with
+                {
+                    StatusId = targetPost.RetweetedId,
+                    RetweetedId = null,
+                    RetweetedBy = null,
+                };
                 targetPost = originalPost;
             }
 
-            var relPosts = new Dictionary<long, PostClass>();
+            var relPosts = new Dictionary<PostId, PostClass>();
             if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null)
             {
                 // 検索結果対応
@@ -793,7 +964,7 @@ namespace OpenTween
                 }
                 else
                 {
-                    p = await this.GetStatusApi(read, targetPost.StatusId)
+                    p = await this.GetStatusApi(read, targetPost.StatusId.ToTwitterStatusId())
                         .ConfigureAwait(false);
                     targetPost = p;
                 }
@@ -807,14 +978,14 @@ namespace OpenTween
             var loopCount = 1;
             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
             {
-                var inReplyToId = nextPost.InReplyToStatusId.Value;
+                var inReplyToId = nextPost.InReplyToStatusId;
 
                 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
                 if (inReplyToPost == null)
                 {
                     try
                     {
-                        inReplyToPost = await this.GetStatusApi(read, inReplyToId)
+                        inReplyToPost = await this.GetStatusApi(read, inReplyToId.ToTwitterStatusId())
                             .ConfigureAwait(false);
                     }
                     catch (WebApiException ex)
@@ -835,11 +1006,9 @@ namespace OpenTween
                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
             foreach (var match in ma)
             {
-                if (long.TryParse(match.Groups["StatusId"].Value, out var statusId))
+                var statusId = new TwitterStatusId(match.Groups["StatusId"].Value);
+                if (!relPosts.ContainsKey(statusId))
                 {
-                    if (relPosts.ContainsKey(statusId))
-                        continue;
-
                     var p = TabInformations.GetInstance()[statusId];
                     if (p == null)
                     {
@@ -872,7 +1041,7 @@ namespace OpenTween
                         continue;
 
                     // リプライチェーンが繋がらないツイートは除外
-                    if (post.InReplyToStatusId == null || !relPosts.ContainsKey(post.InReplyToStatusId.Value))
+                    if (post.InReplyToStatusId == null || !relPosts.ContainsKey(post.InReplyToStatusId))
                         continue;
 
                     relPosts.Add(post.StatusId, post);
@@ -885,7 +1054,7 @@ namespace OpenTween
 
             relPosts.Values.ToList().ForEach(p =>
             {
-                var post = p.Clone();
+                var post = p with { };
                 if (post.IsMe && !read && this.ReadOwnPost)
                     post.IsRead = true;
                 else
@@ -901,7 +1070,7 @@ namespace OpenTween
         private async Task<PostClass[]> GetConversationPosts(PostClass firstPost, PostClass targetPost)
         {
             var conversationId = firstPost.StatusId;
-            var query = $"conversation_id:{conversationId}";
+            var query = $"conversation_id:{conversationId.Id}";
 
             if (targetPost.InReplyToUser != null && targetPost.InReplyToUser != targetPost.ScreenName)
                 query += $" (from:{targetPost.ScreenName} to:{targetPost.InReplyToUser}) OR (from:{targetPost.InReplyToUser} to:{targetPost.ScreenName})";
@@ -918,30 +1087,56 @@ namespace OpenTween
         {
             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
 
-            long? maxId = null;
-            long? sinceId = null;
-            if (more)
+            TwitterStatus[] statuses;
+            if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie)
             {
-                maxId = tab.OldestId - 1;
+                var request = new SearchTimelineRequest(tab.SearchWords)
+                {
+                    Count = count,
+                    Cursor = more ? tab.CursorBottom : tab.CursorTop,
+                };
+                var response = await request.Send(this.Api.Connection)
+                    .ConfigureAwait(false);
+
+                statuses = response.Tweets
+                    .Where(x => !x.IsTombstone)
+                    .Select(x => x.ToTwitterStatus())
+                    .ToArray();
+
+                tab.CursorBottom = response.CursorBottom;
+
+                if (!more)
+                    tab.CursorTop = response.CursorTop;
             }
             else
             {
-                sinceId = tab.SinceId;
-            }
+                TwitterStatusId? maxId = null;
+                TwitterStatusId? sinceId = null;
+                if (more)
+                {
+                    maxId = tab.OldestId as TwitterStatusId;
+                }
+                else
+                {
+                    sinceId = tab.SinceId as TwitterStatusId;
+                }
 
-            var searchResult = await this.Api.SearchTweets(tab.SearchWords, tab.SearchLang, count, maxId, sinceId)
-                .ConfigureAwait(false);
+                var searchResult = await this.Api.SearchTweets(tab.SearchWords, tab.SearchLang, count, maxId, sinceId)
+                    .ConfigureAwait(false);
+
+                statuses = searchResult.Statuses;
+            }
 
             if (!TabInformations.GetInstance().ContainsTab(tab))
                 return;
 
-            var minimumId = this.CreatePostsFromSearchJson(searchResult, tab, read, more);
+            var minimumId = this.CreatePostsFromSearchJson(statuses, tab, read, more);
 
             if (minimumId != null)
-                tab.OldestId = minimumId.Value;
+                tab.OldestId = minimumId;
         }
 
-        public async Task GetDirectMessageEvents(bool read, bool backward)
+        public async Task GetDirectMessageEvents(bool read, DirectMessagesTabModel dmTab, bool backward)
         {
             this.CheckAccountState();
             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
@@ -951,7 +1146,7 @@ namespace OpenTween
             TwitterMessageEventList eventList;
             if (backward)
             {
-                eventList = await this.Api.DirectMessagesEventsList(count, this.nextCursorDirectMessage)
+                eventList = await this.Api.DirectMessagesEventsList(count, dmTab.NextCursor)
                     .ConfigureAwait(false);
             }
             else
@@ -960,7 +1155,7 @@ namespace OpenTween
                     .ConfigureAwait(false);
             }
 
-            this.nextCursorDirectMessage = eventList.NextCursor;
+            dmTab.NextCursor = eventList.NextCursor;
 
             await this.CreateDirectMessagesEventFromJson(eventList, read)
                 .ConfigureAwait(false);
@@ -1241,12 +1436,6 @@ namespace OpenTween
         public string[] GetHashList()
             => this.postFactory.GetReceivedHashtags();
 
-        public string AccessToken
-            => ((TwitterApiConnection)this.Api.Connection).AccessToken;
-
-        public string AccessTokenSecret
-            => ((TwitterApiConnection)this.Api.Connection).AccessSecret;
-
         private void CheckAccountState()
         {
             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)