From: Kimura Youichi Date: Wed, 21 Sep 2016 02:31:06 +0000 (+0900) Subject: Extended Mode (tweet_mode=extended) でのツイートの取得に対応 X-Git-Tag: OpenTween_v1.3.5~13 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=18413771b7c2f956a980b47998bebbfa82ffe56d;p=opentween%2Fopen-tween.git Extended Mode (tweet_mode=extended) でのツイートの取得に対応 * TwitterStatus クラスのメンバーを Extended Mode の JSON に合わせて変更 * TwitterStatusCompat は UserStreams から流れてくる Compatibility Mode の JSON に使用する * TwitterStatusCompat から TwitterStatus への変換は Normarize メソッドで行う --- diff --git a/OpenTween/Api/DataModel/TwitterStatus.cs b/OpenTween/Api/DataModel/TwitterStatus.cs index 1019de3c..c3cd10c0 100644 --- a/OpenTween/Api/DataModel/TwitterStatus.cs +++ b/OpenTween/Api/DataModel/TwitterStatus.cs @@ -55,6 +55,9 @@ namespace OpenTween.Api.DataModel [DataMember(Name = "created_at")] public string CreatedAt { get; set; } + [DataMember(Name = "display_text_range")] + public int[] DisplayTextRange { get; set; } + [DataMember(Name = "entities")] public TwitterEntities Entities { get; set; } @@ -70,6 +73,9 @@ namespace OpenTween.Api.DataModel [DataMember(Name = "filter_level")] public string FilterLevel { get; set; } + [DataMember(Name = "full_text")] + public string FullText { get; set; } + [DataMember(Name = "id")] public long Id { get; set; } @@ -121,9 +127,6 @@ namespace OpenTween.Api.DataModel [DataMember(Name = "source")] public string Source { get; set; } - [DataMember(Name = "text")] - public string Text { get; set; } - [DataMember(Name = "truncated")] public bool Truncated { get; set; } @@ -173,6 +176,197 @@ namespace OpenTween.Api.DataModel } } + /// + /// Streaming API または tweet_mode=compat の REST API から返されるツイート (Compatibility mode) + /// + [DataContract] + public class TwitterStatusCompat + { + [DataMember(Name = "contributors", IsRequired = false)] + public TwitterStatus.Contributor[] Contributors { get; set; } // Nullable + + [DataMember(Name = "coordinates", IsRequired = false)] + public GeoJsonPoint Coordinates { get; set; } + + [DataMember(Name = "created_at")] + public string CreatedAt { get; set; } + + [DataMember(Name = "entities")] + public TwitterEntities Entities { get; set; } + + [DataMember(Name = "extended_entities", IsRequired = false)] + public TwitterEntities ExtendedEntities { get; set; } + + [DataMember(Name = "extended_tweet", IsRequired = false)] + public TwitterStatusCompat.Extended ExtendedTweet { get; set; } + + [DataContract] + public class Extended + { + [DataMember(Name = "display_text_range")] + public int[] DisplayTextRange { get; set; } + + [DataMember(Name = "entities")] + public TwitterEntities Entities { get; set; } + + [DataMember(Name = "extended_entities", IsRequired = false)] + public TwitterEntities ExtendedEntities { get; set; } + + [DataMember(Name = "full_text")] + public string FullText { get; set; } + } + + [DataMember(Name = "favorite_count")] + public int? FavoriteCount { get; set; } + + [DataMember(Name = "favorited")] + public bool? Favorited { get; set; } + + [DataMember(Name = "filter_level")] + public string FilterLevel { get; set; } + + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "id_str")] + public string IdStr { get; set; } + + [DataMember(Name = "in_reply_to_screen_name")] + public string InReplyToScreenName { get; set; } // Nullable + + [DataMember(Name = "in_reply_to_status_id")] + public long? InReplyToStatusId { get; set; } + + [DataMember(Name = "in_reply_to_status_id_str")] + public string InReplyToStatusIdStr { get; set; } // Nullable + + [DataMember(Name = "in_reply_to_user_id")] + public long? InReplyToUserId { get; set; } + + [DataMember(Name = "in_reply_to_user_id_str")] + public string InReplyToUserIdStr { get; set; } // Nullable + + [DataMember(Name = "lang")] + public string Lang { get; set; } // Nullable + + [DataMember(Name = "place", IsRequired = false)] + public TwitterPlace Place { get; set; } + + [DataMember(Name = "possibly_sensitive")] + public bool? PossiblySensitive { get; set; } + + [DataMember(Name = "quoted_status_id", IsRequired = false)] + public long? QuotedStatusId { get; set; } + + [DataMember(Name = "quoted_status_id_str", IsRequired = false)] + public string QuotedStatusIdStr { get; set; } + + [DataMember(Name = "quoted_status", IsRequired = false)] + public TwitterStatusCompat QuotedStatus { get; set; } + + [DataMember(Name = "retweet_count")] + public int RetweetCount { get; set; } + + [DataMember(Name = "retweeted")] + public bool Retweeted { get; set; } + + [DataMember(Name = "retweeted_status", IsRequired = false)] + public TwitterStatusCompat RetweetedStatus { get; set; } + + [DataMember(Name = "source")] + public string Source { get; set; } + + [DataMember(Name = "text")] + public string Text { get; set; } + + [DataMember(Name = "truncated")] + public bool Truncated { get; set; } + + [DataMember(Name = "user")] + public TwitterUser User { get; set; } + + [DataMember(Name = "withheld_copyright")] + public bool WithheldCopyright { get; set; } + + [DataMember(Name = "withheld_in_countries")] + public string[] WithheldInCountries { get; set; } + + [DataMember(Name = "withheld_scope")] + public string WithheldScope { get; set; } + + /// Compatibility Modeのツイートをに変換します + public TwitterStatus Normarize() + { + var normarized = new TwitterStatus + { + Contributors = this.Contributors, + Coordinates = this.Coordinates, + CreatedAt = this.CreatedAt, + FavoriteCount = this.FavoriteCount, + Favorited = this.Favorited, + FilterLevel = this.FilterLevel, + Id = this.Id, + IdStr = this.IdStr, + InReplyToScreenName = this.InReplyToScreenName, + InReplyToStatusId = this.InReplyToStatusId, + InReplyToStatusIdStr = this.InReplyToStatusIdStr, + InReplyToUserId = this.InReplyToUserId, + InReplyToUserIdStr = this.InReplyToUserIdStr, + Lang = this.Lang, + Place = this.Place, + PossiblySensitive = this.PossiblySensitive, + QuotedStatusId = this.QuotedStatusId, + QuotedStatusIdStr = this.QuotedStatusIdStr, + QuotedStatus = this.QuotedStatus?.Normarize(), + RetweetCount = this.RetweetCount, + Retweeted = this.Retweeted, + RetweetedStatus = this.RetweetedStatus?.Normarize(), + Source = this.Source, + User = this.User, + WithheldCopyright = this.WithheldCopyright, + WithheldInCountries = this.WithheldInCountries, + WithheldScope = this.WithheldScope, + }; + + if (this.ExtendedTweet != null) + { + // Extended Tweet + normarized.DisplayTextRange = this.ExtendedTweet.DisplayTextRange; + normarized.Entities = this.ExtendedTweet.Entities; + normarized.ExtendedEntities = this.ExtendedTweet.ExtendedEntities; + normarized.FullText = this.ExtendedTweet.FullText; + normarized.Truncated = false; + } + else + { + // Classic Tweet + normarized.DisplayTextRange = new[] { 0, this.GetCodePointCount(this.Text) }; + normarized.Entities = this.Entities; + normarized.ExtendedEntities = this.ExtendedEntities; + normarized.FullText = this.Text; + normarized.Truncated = this.Truncated; + } + + return normarized; + } + + /// Unicodeコードポイント単位の文字数を返します + private int GetCodePointCount(string text) + => text.Length - text.Sum(x => char.IsHighSurrogate(x) ? 1 : 0); + + /// + public static TwitterStatusCompat ParseJson(string json) + { + return MyCommon.CreateDataFromJson(json); + } + + /// + public static TwitterStatusCompat[] ParseJsonArray(string json) + { + return MyCommon.CreateDataFromJson(json); + } + } + [DataContract] public class TwitterDirectMessage { diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 5f904d58..c708cafb 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -57,6 +57,7 @@ namespace OpenTween.Api { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (count != null) @@ -76,6 +77,7 @@ namespace OpenTween.Api { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (count != null) @@ -97,6 +99,7 @@ namespace OpenTween.Api ["include_rts"] = "true", ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (count != null) @@ -117,6 +120,7 @@ namespace OpenTween.Api ["id"] = statusId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; return this.apiConnection.GetAsync(endpoint, param, "/statuses/show/:id"); @@ -130,6 +134,7 @@ namespace OpenTween.Api ["status"] = status, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (replyToId != null) @@ -159,6 +164,7 @@ namespace OpenTween.Api ["id"] = statusId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -173,6 +179,7 @@ namespace OpenTween.Api ["result_type"] = "recent", ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (lang != null) @@ -290,6 +297,7 @@ namespace OpenTween.Api ["list_id"] = listId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (count != null) @@ -312,6 +320,7 @@ namespace OpenTween.Api ["list_id"] = listId.ToString(), ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (cursor != null) @@ -329,6 +338,7 @@ namespace OpenTween.Api ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; return this.apiConnection.GetAsync(endpoint, param, "/lists/members/show"); @@ -343,6 +353,7 @@ namespace OpenTween.Api ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -357,6 +368,7 @@ namespace OpenTween.Api ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -433,6 +445,7 @@ namespace OpenTween.Api ["screen_name"] = screenName, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; return this.apiConnection.GetAsync(endpoint, param, "/users/show/:id"); @@ -444,6 +457,7 @@ namespace OpenTween.Api var param = new Dictionary { ["screen_name"] = screenName, + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -456,6 +470,7 @@ namespace OpenTween.Api { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (count != null) @@ -474,6 +489,7 @@ namespace OpenTween.Api var param = new Dictionary { ["id"] = statusId.ToString(), + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -485,6 +501,7 @@ namespace OpenTween.Api var param = new Dictionary { ["id"] = statusId.ToString(), + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -570,6 +587,7 @@ namespace OpenTween.Api var param = new Dictionary { ["screen_name"] = screenName, + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -581,6 +599,7 @@ namespace OpenTween.Api var param = new Dictionary { ["screen_name"] = screenName, + ["tweet_mode"] = "extended", }; return this.apiConnection.PostLazyAsync(endpoint, param); @@ -593,6 +612,7 @@ namespace OpenTween.Api { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; var user = await this.apiConnection.GetAsync(endpoint, param, "/account/verify_credentials") @@ -611,6 +631,7 @@ namespace OpenTween.Api { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; if (name != null) @@ -638,6 +659,7 @@ namespace OpenTween.Api { ["include_entities"] = "true", ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", }; var paramMedia = new Dictionary { diff --git a/OpenTween/Resources/ChangeLog.txt b/OpenTween/Resources/ChangeLog.txt index aa47f946..f2c0035b 100644 --- a/OpenTween/Resources/ChangeLog.txt +++ b/OpenTween/Resources/ChangeLog.txt @@ -1,6 +1,7 @@ 更新履歴 ==== Ver 1.3.5-dev(2016/xx/xx) + * NEW: 140文字を越えるツイートの表示に対応しました * NEW: リスト管理で未取得(グレー表示)の状態が無くなり、常に追加・未追加の状態が表示されるようになりました * CHG: リストの一覧を表示する際に20件を越える場合のAPIリクエスト回数が少なくなりました * FIX: UserStreamsが正常に動作しないことがある不具合を修正 diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index 28aa5052..5a9bed15 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -657,7 +657,7 @@ namespace OpenTween //Id post.RetweetedId = retweeted.Id; //本文 - post.TextFromApi = retweeted.Text; + post.TextFromApi = retweeted.FullText; entities = retweeted.MergedEntities; sourceHtml = retweeted.Source; //Reply先 @@ -713,7 +713,7 @@ namespace OpenTween { post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt); //本文 - post.TextFromApi = status.Text; + post.TextFromApi = status.FullText; entities = status.MergedEntities; sourceHtml = status.Source; post.InReplyToStatusId = status.InReplyToStatusId; @@ -1297,7 +1297,7 @@ namespace OpenTween var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl); if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quoteStatus.IdStr) { - var quoteText = this.CreateAccessibleText(quoteStatus.Text, quoteStatus.MergedEntities, quoteStatus: null); + 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)); } } @@ -1905,8 +1905,8 @@ namespace OpenTween { try { - var status = TwitterStatus.ParseJson(line); - this.CreatePostsFromJson(new[] { status }, MyCommon.WORKERTYPE.UserStream, null, false); + var status = TwitterStatusCompat.ParseJson(line); + this.CreatePostsFromJson(new[] { status.Normarize() }, MyCommon.WORKERTYPE.UserStream, null, false); } catch (SerializationException ex) { @@ -1974,7 +1974,7 @@ namespace OpenTween eventTable.TryGetValue(eventData.Event, out eventType); evt.Eventtype = eventType; - TwitterStreamEvent tweetEvent; + TwitterStreamEvent tweetEvent; switch (eventData.Event) { @@ -2002,7 +2002,7 @@ namespace OpenTween return; case "favorite": case "unfavorite": - tweetEvent = TwitterStreamEvent.ParseJson(content); + tweetEvent = TwitterStreamEvent.ParseJson(content); evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text); evt.Id = tweetEvent.TargetObject.Id; @@ -2052,7 +2052,7 @@ namespace OpenTween case "quoted_tweet": if (evt.IsMe) return; - tweetEvent = TwitterStreamEvent.ParseJson(content); + tweetEvent = TwitterStreamEvent.ParseJson(content); evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text); evt.Id = tweetEvent.TargetObject.Id; diff --git a/OpenTween/UserInfo.cs b/OpenTween/UserInfo.cs index 343df32d..71f37346 100644 --- a/OpenTween/UserInfo.cs +++ b/OpenTween/UserInfo.cs @@ -60,7 +60,7 @@ namespace OpenTween this.Verified = user.Verified; if (user.Status != null) { - this.RecentPost = user.Status.Text; + this.RecentPost = user.Status.FullText; this.PostCreatedAt = MyCommon.DateTimeParse(user.Status.CreatedAt); this.PostSource = user.Status.Source; } diff --git a/OpenTween/UserInfoDialog.cs b/OpenTween/UserInfoDialog.cs index bfade68f..5ca03425 100644 --- a/OpenTween/UserInfoDialog.cs +++ b/OpenTween/UserInfoDialog.cs @@ -247,7 +247,7 @@ namespace OpenTween foreach (var entity in entities.Urls) entity.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(entity.ExpandedUrl); - var html = TweetFormatter.AutoLinkHtml(status.Text, entities); + var html = TweetFormatter.AutoLinkHtml(status.FullText, entities); html = this.mainForm.createDetailHtml(html + " Posted at " + MyCommon.DateTimeParse(status.CreatedAt) + " via " + status.Source);