--- /dev/null
+// 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);
+ }
+ }
+}
}
[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())
}
[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())
--- /dev/null
+// 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; }
+ }
+ }
+}
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);
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);
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)
<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>
更新履歴
==== Ver 2.0.2-dev(2018/xx/xx)
+ * NEW: DMの一覧取得について新APIに対応しました
* CHG: UserStreams停止によるエラーが発生した場合の再接続の間隔を10分に変更
==== Ver 2.0.1(2018/06/13)
using OpenTween.Connection;
using OpenTween.Models;
using OpenTween.Setting;
+using System.Globalization;
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();