OSDN Git Service

/direct_messages/events/list.json によるDMの取得に対応
authorKimura Youichi <kim.upsilon@bucyou.net>
Fri, 17 Aug 2018 03:42:37 +0000 (12:42 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Fri, 17 Aug 2018 03:42:37 +0000 (12:42 +0900)
OpenTween.Tests/Api/DataModel/TwitterMessageEventTest.cs [new file with mode: 0644]
OpenTween.Tests/Api/TwitterApiTest.cs
OpenTween/Api/DataModel/TwitterMessageEvent.cs [new file with mode: 0644]
OpenTween/Api/TwitterApi.cs
OpenTween/Models/DirectMessagesTabModel.cs
OpenTween/OpenTween.csproj
OpenTween/Resources/ChangeLog.txt
OpenTween/Twitter.cs

diff --git a/OpenTween.Tests/Api/DataModel/TwitterMessageEventTest.cs b/OpenTween.Tests/Api/DataModel/TwitterMessageEventTest.cs
new file mode 100644 (file)
index 0000000..872c96e
--- /dev/null
@@ -0,0 +1,52 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+using System.Linq;
+using Xunit;
+
+namespace OpenTween.Api.DataModel
+{
+    public class TwitterMessageEventListTest
+    {
+        [Fact]
+        public void Deserialize_AppsTest()
+        {
+            var json = @"{
+  ""events"": [],
+  ""apps"": {
+    ""258901"": {
+      ""id"": ""258901"",
+      ""name"": ""Twitter for Android"",
+      ""url"": ""http://twitter.com/download/android""
+    }
+  }
+}";
+            var result = MyCommon.CreateDataFromJson<TwitterMessageEventList>(json);
+            Assert.Single(result.Apps);
+
+            var (key, app) = result.Apps.Single();
+            Assert.Equal("258901", key);
+            Assert.Equal("258901", app.Id);
+            Assert.Equal("Twitter for Android", app.Name);
+            Assert.Equal("http://twitter.com/download/android", app.Url);
+        }
+    }
+}
index 3f1fcf9..0cf0610 100644 (file)
@@ -747,6 +747,32 @@ namespace OpenTween.Api
         }
 
         [Fact]
+        public async Task DirectMessagesEventsList_Test()
+        {
+            using (var twitterApi = new TwitterApi())
+            {
+                var mock = new Mock<IApiConnection>();
+                mock.Setup(x =>
+                    x.GetAsync<TwitterMessageEventList>(
+                        new Uri("direct_messages/events/list.json", UriKind.Relative),
+                        new Dictionary<string, string> {
+                            { "count", "50" },
+                            { "cursor", "12345abcdefg" },
+                        },
+                        "/direct_messages/events/list")
+                )
+                .ReturnsAsync(new TwitterMessageEventList());
+
+                twitterApi.apiConnection = mock.Object;
+
+                await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg")
+                    .ConfigureAwait(false);
+
+                mock.VerifyAll();
+            }
+        }
+
+        [Fact]
         public async Task DirectMessagesEventsNew_Test()
         {
             using (var twitterApi = new TwitterApi())
@@ -815,6 +841,34 @@ namespace OpenTween.Api
         }
 
         [Fact]
+        public async Task UsersLookup_Test()
+        {
+            using (var twitterApi = new TwitterApi())
+            {
+                var mock = new Mock<IApiConnection>();
+                mock.Setup(x =>
+                    x.GetAsync<TwitterUser[]>(
+                        new Uri("users/lookup.json", UriKind.Relative),
+                        new Dictionary<string, string> {
+                            { "user_id", "11111,22222" },
+                            { "include_entities", "true" },
+                            { "include_ext_alt_text", "true" },
+                            { "tweet_mode", "extended" },
+                        },
+                        "/users/lookup")
+                )
+                .ReturnsAsync(new TwitterUser[0]);
+
+                twitterApi.apiConnection = mock.Object;
+
+                await twitterApi.UsersLookup(userIds: new[] { "11111", "22222" })
+                    .ConfigureAwait(false);
+
+                mock.VerifyAll();
+            }
+        }
+
+        [Fact]
         public async Task UsersReportSpam_Test()
         {
             using (var twitterApi = new TwitterApi())
diff --git a/OpenTween/Api/DataModel/TwitterMessageEvent.cs b/OpenTween/Api/DataModel/TwitterMessageEvent.cs
new file mode 100644 (file)
index 0000000..e1b97e0
--- /dev/null
@@ -0,0 +1,115 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2018 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace OpenTween.Api.DataModel
+{
+    [DataContract]
+    public class TwitterMessageEventList
+    {
+        [DataMember(Name = "apps")]
+        public Dictionary<string, App> Apps { get; set; }
+
+        [DataContract]
+        public class App
+        {
+            [DataMember(Name = "id")]
+            public string Id { get; set; }
+
+            [DataMember(Name = "name")]
+            public string Name { get; set; }
+
+            [DataMember(Name = "url")]
+            public string Url { get; set; }
+        }
+
+        [DataMember(Name = "events")]
+        public TwitterMessageEvent[] Events { get; set; }
+
+        [DataMember(Name = "next_cursor", IsRequired = false)]
+        public string NextCursor { get; set; }
+    }
+
+    [DataContract]
+    public class TwitterMessageEvent
+    {
+        [DataMember(Name = "created_timestamp")]
+        public string CreatedTimestamp { get; set; }
+
+        [DataMember(Name = "id")]
+        public string Id { get; set; }
+
+        [DataMember(Name = "message_create")]
+        public TwitterMessageEventCreate MessageCreate { get; set; }
+
+        [DataMember(Name = "type")]
+        public string Type { get; set; }
+    }
+
+    [DataContract]
+    public class TwitterMessageEventCreate
+    {
+        [DataMember(Name = "message_data")]
+        public Data MessageData { get; set; }
+
+        [DataContract]
+        public class Data
+        {
+            [DataMember(Name = "attachment", IsRequired = false)]
+            public MessageAttachment Attachment { get; set; }
+
+            [DataContract]
+            public class MessageAttachment
+            {
+                [DataMember(Name = "type")]
+                public string Type { get; set; }
+
+                [DataMember(Name = "media")]
+                public TwitterEntityMedia Media { get; set; }
+            }
+
+            [DataMember(Name = "text")]
+            public string Text { get; set; }
+
+            [DataMember(Name = "entities")]
+            public TwitterEntities Entities { get; set; }
+        }
+
+        [DataMember(Name = "sender_id")]
+        public string SenderId { get; set; }
+
+        [DataMember(Name = "source_app_id", IsRequired = false)]
+        public string SourceAppId { get; set; }
+
+        [DataMember(Name = "target")]
+        public MessageTarget Target { get; set; }
+
+        [DataContract]
+        public class MessageTarget
+        {
+            [DataMember(Name = "recipient_id")]
+            public string RecipientId { get; set; }
+        }
+    }
+}
index 48e5bf4..4704986 100644 (file)
@@ -432,6 +432,19 @@ namespace OpenTween.Api
             return this.apiConnection.PostLazyAsync<TwitterDirectMessage>(endpoint, param);
         }
 
+        public Task<TwitterMessageEventList> DirectMessagesEventsList(int? count = null, string cursor = null)
+        {
+            var endpoint = new Uri("direct_messages/events/list.json", UriKind.Relative);
+            var param = new Dictionary<string, string>();
+
+            if (count != null)
+                param["count"] = count.ToString();
+            if (cursor != null)
+                param["cursor"] = cursor;
+
+            return this.apiConnection.GetAsync<TwitterMessageEventList>(endpoint, param, "/direct_messages/events/list");
+        }
+
         public Task DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null)
         {
             var endpoint = new Uri("direct_messages/events/new.json", UriKind.Relative);
@@ -479,6 +492,20 @@ namespace OpenTween.Api
             return this.apiConnection.GetAsync<TwitterUser>(endpoint, param, "/users/show/:id");
         }
 
+        public Task<TwitterUser[]> UsersLookup(IReadOnlyList<string> userIds)
+        {
+            var endpoint = new Uri("users/lookup.json", UriKind.Relative);
+            var param = new Dictionary<string, string>
+            {
+                ["user_id"] = string.Join(",", userIds),
+                ["include_entities"] = "true",
+                ["include_ext_alt_text"] = "true",
+                ["tweet_mode"] = "extended",
+            };
+
+            return this.apiConnection.GetAsync<TwitterUser[]>(endpoint, param, "/users/lookup");
+        }
+
         public Task<LazyJson<TwitterUser>> UsersReportSpam(string screenName)
         {
             var endpoint = new Uri("users/report_spam.json", UriKind.Relative);
index 976cbe7..1513fc4 100644 (file)
@@ -57,6 +57,12 @@ namespace OpenTween.Models
 
             progress.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText8, backward ? -1 : 1));
 
+            if (!backward)
+            {
+                await tw.GetDirectMessageEvents(read)
+                    .ConfigureAwait(false);
+            }
+
             await tw.GetDirectMessageApi(read, MyCommon.WORKERTYPE.DirectMessegeRcv, backward)
                 .ConfigureAwait(false);
             await tw.GetDirectMessageApi(read, MyCommon.WORKERTYPE.DirectMessegeSnt, backward)
index c8bc3d9..13a0044 100644 (file)
@@ -73,6 +73,7 @@
     <Compile Include="Api\DataModel\TwitterError.cs" />
     <Compile Include="Api\DataModel\TwitterFriendship.cs" />
     <Compile Include="Api\DataModel\TwitterList.cs" />
+    <Compile Include="Api\DataModel\TwitterMessageEvent.cs" />
     <Compile Include="Api\DataModel\TwitterPageable.cs">
       <SubType>Code</SubType>
     </Compile>
index 2f89372..589a259 100644 (file)
@@ -1,6 +1,7 @@
 更新履歴
 
 ==== Ver 2.0.2-dev(2018/xx/xx)
+ * NEW: DMの一覧取得について新APIに対応しました
  * CHG: UserStreams停止によるエラーが発生した場合の再接続の間隔を10分に変更
 
 ==== Ver 2.0.1(2018/06/13)
index 30e4a04..392087b 100644 (file)
@@ -44,6 +44,7 @@ using OpenTween.Api.DataModel;
 using OpenTween.Connection;
 using OpenTween.Models;
 using OpenTween.Setting;
+using System.Globalization;
 
 namespace OpenTween
 {
@@ -1264,6 +1265,145 @@ namespace OpenTween
             CreateDirectMessagesFromJson(messages, gType, read);
         }
 
+        public async Task GetDirectMessageEvents(bool read)
+        {
+            this.CheckAccountState();
+            this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
+
+            var count = 50;
+            var eventLists = new List<TwitterMessageEventList>();
+
+            string cursor = null;
+            do
+            {
+                var eventList = await this.Api.DirectMessagesEventsList(count, cursor)
+                    .ConfigureAwait(false);
+
+                eventLists.Add(eventList);
+                cursor = eventList.NextCursor;
+            }
+            while (cursor != null);
+
+            var events = eventLists.SelectMany(x => x.Events);
+            var userIds = Enumerable.Concat(
+                events.Select(x => x.MessageCreate.SenderId),
+                events.Select(x => x.MessageCreate.Target.RecipientId)
+            ).Distinct().ToArray();
+
+            var users = (await this.Api.UsersLookup(userIds).ConfigureAwait(false))
+                .ToDictionary(x => x.IdStr);
+
+            var apps = eventLists.SelectMany(x => x.Apps)
+                .ToLookup(x => x.Key)
+                .ToDictionary(x => x.Key, x => x.First().Value);
+
+            this.CreateDirectMessagesEventFromJson(events, users, apps, read);
+        }
+
+        private void CreateDirectMessagesEventFromJson(IEnumerable<TwitterMessageEvent> events, IReadOnlyDictionary<string, TwitterUser> users,
+            IReadOnlyDictionary<string, TwitterMessageEventList.App> apps, bool read)
+        {
+            foreach (var eventItem in events)
+            {
+                var post = new PostClass();
+                try
+                {
+                    post.StatusId = long.Parse(eventItem.Id);
+
+                    var timestamp = long.Parse(eventItem.CreatedTimestamp);
+                    post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond);
+                    //本文
+                    var textFromApi = eventItem.MessageCreate.MessageData.Text;
+
+                    var entities = eventItem.MessageCreate.MessageData.Entities;
+                    var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media;
+
+                    if (mediaEntity != null)
+                        entities.Media = new[] { mediaEntity };
+
+                    //HTMLに整形
+                    post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null);
+                    post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null);
+                    post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
+                    post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+                    post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null);
+                    post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+                    post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
+                    post.IsFav = false;
+
+                    this.ExtractEntities(entities, post.ReplyToList, post.Media);
+
+                    post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null)
+                        .Distinct().ToArray();
+
+                    post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
+                        .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
+                        .ToArray();
+
+                    //以下、ユーザー情報
+                    TwitterUser user;
+                    if (eventItem.MessageCreate.SenderId != this.Api.CurrentUserId.ToString(CultureInfo.InvariantCulture))
+                    {
+                        user = users[eventItem.MessageCreate.SenderId];
+                        post.IsMe = false;
+                        post.IsOwl = true;
+                    }
+                    else
+                    {
+                        user = users[eventItem.MessageCreate.Target.RecipientId];
+                        post.IsMe = true;
+                        post.IsOwl = false;
+                    }
+
+                    post.UserId = user.Id;
+                    post.ScreenName = user.ScreenName;
+                    post.Nickname = user.Name.Trim();
+                    post.ImageUrl = user.ProfileImageUrlHttps;
+                    post.IsProtect = user.Protected;
+
+                    // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+                    if (post.Text == post.TextFromApi)
+                        post.Text = post.TextFromApi;
+                    if (post.AccessibleText == post.TextFromApi)
+                        post.AccessibleText = post.TextFromApi;
+
+                    // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+                    post.ScreenName = string.Intern(post.ScreenName);
+                    post.Nickname = string.Intern(post.Nickname);
+                    post.ImageUrl = string.Intern(post.ImageUrl);
+
+                    var appId = eventItem.MessageCreate.SourceAppId;
+                    if (appId != null)
+                    {
+                        var app = apps[appId];
+                        post.Source = string.Intern(app.Name);
+
+                        try
+                        {
+                            post.SourceUri = new Uri(SourceUriBase, app.Url);
+                        }
+                        catch (UriFormatException) { }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
+                    MessageBox.Show("Parse Error(CreateDirectMessagesEventFromJson)");
+                    continue;
+                }
+
+                post.IsRead = read;
+                if (post.IsMe && !read && this.ReadOwnPost)
+                    post.IsRead = true;
+                post.IsReply = false;
+                post.IsExcludeReply = false;
+                post.IsDm = true;
+
+                var dmTab = TabInformations.GetInstance().GetTabByType<DirectMessagesTabModel>();
+                dmTab.AddPostQueue(post);
+            }
+        }
+
         public async Task GetFavoritesApi(bool read, FavoritesTabModel tab, bool backward)
         {
             this.CheckAccountState();