開発に使用するリポジトリ
Revision | 5491484758a7b9d70b13ad4cbc824d89a8055ad4 (tree) |
---|---|
Time | 2018-05-13 03:55:47 |
Author | Kimura Youichi <kim.upsilon@bucy...> |
Commiter | Kimura Youichi |
Merge branch 'quoted-status'
@@ -110,6 +110,216 @@ namespace OpenTween | ||
110 | 110 | } |
111 | 111 | |
112 | 112 | [Fact] |
113 | + public void CreateAccessibleText_MediaAltTest() | |
114 | + { | |
115 | + var text = "https://t.co/hoge"; | |
116 | + var entities = new TwitterEntities | |
117 | + { | |
118 | + Media = new[] | |
119 | + { | |
120 | + new TwitterEntityMedia | |
121 | + { | |
122 | + Indices = new[] { 0, 17 }, | |
123 | + Url = "https://t.co/hoge", | |
124 | + DisplayUrl = "pic.twitter.com/hoge", | |
125 | + ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", | |
126 | + AltText = "代替テキスト", | |
127 | + }, | |
128 | + }, | |
129 | + }; | |
130 | + | |
131 | + var expectedText = string.Format(Properties.Resources.ImageAltText, "代替テキスト"); | |
132 | + | |
133 | + Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus: null, quotedStatusLink: null)); | |
134 | + } | |
135 | + | |
136 | + [Fact] | |
137 | + public void CreateAccessibleText_MediaNoAltTest() | |
138 | + { | |
139 | + var text = "https://t.co/hoge"; | |
140 | + var entities = new TwitterEntities | |
141 | + { | |
142 | + Media = new[] | |
143 | + { | |
144 | + new TwitterEntityMedia | |
145 | + { | |
146 | + Indices = new[] { 0, 17 }, | |
147 | + Url = "https://t.co/hoge", | |
148 | + DisplayUrl = "pic.twitter.com/hoge", | |
149 | + ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", | |
150 | + AltText = null, | |
151 | + }, | |
152 | + }, | |
153 | + }; | |
154 | + | |
155 | + var expectedText = "pic.twitter.com/hoge"; | |
156 | + | |
157 | + Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus: null, quotedStatusLink: null)); | |
158 | + } | |
159 | + | |
160 | + [Fact] | |
161 | + public void CreateAccessibleText_QuotedUrlTest() | |
162 | + { | |
163 | + var text = "https://t.co/hoge"; | |
164 | + var entities = new TwitterEntities | |
165 | + { | |
166 | + Urls = new[] | |
167 | + { | |
168 | + new TwitterEntityUrl | |
169 | + { | |
170 | + Indices = new[] { 0, 17 }, | |
171 | + Url = "https://t.co/hoge", | |
172 | + DisplayUrl = "twitter.com/hoge/status/1…", | |
173 | + ExpandedUrl = "https://twitter.com/hoge/status/1234567890", | |
174 | + }, | |
175 | + }, | |
176 | + }; | |
177 | + var quotedStatus = new TwitterStatus | |
178 | + { | |
179 | + Id = 1234567890L, | |
180 | + IdStr = "1234567890", | |
181 | + User = new TwitterUser | |
182 | + { | |
183 | + Id = 1111, | |
184 | + IdStr = "1111", | |
185 | + ScreenName = "foo", | |
186 | + }, | |
187 | + FullText = "test", | |
188 | + }; | |
189 | + | |
190 | + var expectedText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); | |
191 | + | |
192 | + Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink: null)); | |
193 | + } | |
194 | + | |
195 | + [Fact] | |
196 | + public void CreateAccessibleText_QuotedUrlWithPermelinkTest() | |
197 | + { | |
198 | + var text = "hoge"; | |
199 | + var entities = new TwitterEntities(); | |
200 | + var quotedStatus = new TwitterStatus | |
201 | + { | |
202 | + Id = 1234567890L, | |
203 | + IdStr = "1234567890", | |
204 | + User = new TwitterUser | |
205 | + { | |
206 | + Id = 1111, | |
207 | + IdStr = "1111", | |
208 | + ScreenName = "foo", | |
209 | + }, | |
210 | + FullText = "test", | |
211 | + }; | |
212 | + var quotedStatusLink = new TwitterQuotedStatusPermalink | |
213 | + { | |
214 | + Url = "https://t.co/hoge", | |
215 | + Display = "twitter.com/hoge/status/1…", | |
216 | + Expanded = "https://twitter.com/hoge/status/1234567890", | |
217 | + }; | |
218 | + | |
219 | + var expectedText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); | |
220 | + | |
221 | + Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink)); | |
222 | + } | |
223 | + | |
224 | + [Fact] | |
225 | + public void CreateAccessibleText_QuotedUrlNoReferenceTest() | |
226 | + { | |
227 | + var text = "https://t.co/hoge"; | |
228 | + var entities = new TwitterEntities | |
229 | + { | |
230 | + Urls = new[] | |
231 | + { | |
232 | + new TwitterEntityUrl | |
233 | + { | |
234 | + Indices = new[] { 0, 17 }, | |
235 | + Url = "https://t.co/hoge", | |
236 | + DisplayUrl = "twitter.com/hoge/status/1…", | |
237 | + ExpandedUrl = "https://twitter.com/hoge/status/1234567890", | |
238 | + }, | |
239 | + }, | |
240 | + }; | |
241 | + var quotedStatus = (TwitterStatus)null; | |
242 | + | |
243 | + var expectedText = "twitter.com/hoge/status/1…"; | |
244 | + | |
245 | + Assert.Equal(expectedText, Twitter.CreateAccessibleText(text, entities, quotedStatus, quotedStatusLink: null)); | |
246 | + } | |
247 | + | |
248 | + [Fact] | |
249 | + public void CreateHtmlAnchor_Test() | |
250 | + { | |
251 | + var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3"; | |
252 | + var entities = new TwitterEntities | |
253 | + { | |
254 | + UserMentions = new[] | |
255 | + { | |
256 | + new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" }, | |
257 | + }, | |
258 | + Hashtags = new[] | |
259 | + { | |
260 | + new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" }, | |
261 | + }, | |
262 | + Urls = new[] | |
263 | + { | |
264 | + new TwitterEntityUrl | |
265 | + { | |
266 | + Indices = new[] { 31, 54 }, | |
267 | + Url ="https://t.co/mIJcSoVSK3", | |
268 | + DisplayUrl = "apps-of-a-feather.com", | |
269 | + ExpandedUrl = "http://apps-of-a-feather.com/", | |
270 | + }, | |
271 | + }, | |
272 | + }; | |
273 | + | |
274 | + var expectedHtml = @"<a class=""mention"" href=""https://twitter.com/twitterapi"">@twitterapi</a>" | |
275 | + + @" <a class=""hashtag"" href=""https://twitter.com/search?q=%23BreakingMyTwitter"">#BreakingMyTwitter</a>" | |
276 | + + @" <a href=""https://t.co/mIJcSoVSK3"" title=""https://t.co/mIJcSoVSK3"">apps-of-a-feather.com</a>"; | |
277 | + | |
278 | + Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); | |
279 | + } | |
280 | + | |
281 | + [Fact] | |
282 | + public void CreateHtmlAnchor_NicovideoTest() | |
283 | + { | |
284 | + var text = "sm9"; | |
285 | + var entities = new TwitterEntities(); | |
286 | + | |
287 | + var expectedHtml = @"<a href=""http://www.nicovideo.jp/watch/sm9"">sm9</a>"; | |
288 | + | |
289 | + Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); | |
290 | + } | |
291 | + | |
292 | + [Fact] | |
293 | + public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest() | |
294 | + { | |
295 | + var text = "hoge"; | |
296 | + var entities = new TwitterEntities(); | |
297 | + var quotedStatus = new TwitterStatus | |
298 | + { | |
299 | + Id = 1234567890L, | |
300 | + IdStr = "1234567890", | |
301 | + User = new TwitterUser | |
302 | + { | |
303 | + Id = 1111, | |
304 | + IdStr = "1111", | |
305 | + ScreenName = "foo", | |
306 | + }, | |
307 | + FullText = "test", | |
308 | + }; | |
309 | + var quotedStatusLink = new TwitterQuotedStatusPermalink | |
310 | + { | |
311 | + Url = "https://t.co/hoge", | |
312 | + Display = "twitter.com/hoge/status/1…", | |
313 | + Expanded = "https://twitter.com/hoge/status/1234567890", | |
314 | + }; | |
315 | + | |
316 | + var expectedHtml = @"hoge" | |
317 | + + @" <a href=""https://t.co/hoge"" title=""https://t.co/hoge"">twitter.com/hoge/status/1…</a>"; | |
318 | + | |
319 | + Assert.Equal(expectedHtml, Twitter.CreateHtmlAnchor(text, entities, quotedStatusLink)); | |
320 | + } | |
321 | + | |
322 | + [Fact] | |
113 | 323 | public void ParseSource_Test() |
114 | 324 | { |
115 | 325 | var sourceHtml = "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>"; |
@@ -186,7 +396,21 @@ namespace OpenTween | ||
186 | 396 | }, |
187 | 397 | }; |
188 | 398 | |
189 | - var statusIds = Twitter.GetQuoteTweetStatusIds(entities); | |
399 | + var statusIds = Twitter.GetQuoteTweetStatusIds(entities, quotedStatusLink: null); | |
400 | + Assert.Equal(new[] { 599261132361072640L }, statusIds); | |
401 | + } | |
402 | + | |
403 | + [Fact] | |
404 | + public void GetQuoteTweetStatusIds_QuotedStatusLinkTest() | |
405 | + { | |
406 | + var entities = new TwitterEntities(); | |
407 | + var quotedStatusLink = new TwitterQuotedStatusPermalink | |
408 | + { | |
409 | + Url = "https://t.co/3HXq0LrbJb", | |
410 | + Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640", | |
411 | + }; | |
412 | + | |
413 | + var statusIds = Twitter.GetQuoteTweetStatusIds(entities, quotedStatusLink); | |
190 | 414 | Assert.Equal(new[] { 599261132361072640L }, statusIds); |
191 | 415 | } |
192 | 416 |
@@ -2,6 +2,7 @@ | ||
2 | 2 | |
3 | 3 | ==== Ver 1.4.2-dev(xxxx/xx/xx) |
4 | 4 | * NEW: システムのタイムゾーンの変更を検知して、ツイートの投稿日時などの表示を新しいタイムゾーンに同期します |
5 | + * NEW: 5月中旬に予定されている引用ツイートに関する Twitter API の仕様変更に対応 | |
5 | 6 | * FIX: 動画のサムネイル画像に「URLをコピー」を行うとエラーが発生する不具合を修正 |
6 | 7 | |
7 | 8 | ==== Ver 1.4.1(2017/11/12) |
@@ -247,7 +247,7 @@ namespace OpenTween | ||
247 | 247 | if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream(); |
248 | 248 | } |
249 | 249 | |
250 | - public string PreProcessUrl(string orgData) | |
250 | + internal static string PreProcessUrl(string orgData) | |
251 | 251 | { |
252 | 252 | int posl1; |
253 | 253 | var posl2 = 0; |
@@ -820,16 +820,20 @@ namespace OpenTween | ||
820 | 820 | } |
821 | 821 | //HTMLに整形 |
822 | 822 | string textFromApi = post.TextFromApi; |
823 | - post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media); | |
823 | + var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink; | |
824 | + | |
825 | + post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink); | |
824 | 826 | post.TextFromApi = textFromApi; |
825 | - post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities); | |
827 | + post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink); | |
826 | 828 | post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); |
827 | 829 | post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); |
828 | - post.AccessibleText = this.CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus); | |
830 | + post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink); | |
829 | 831 | post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); |
830 | 832 | post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); |
831 | 833 | |
832 | - post.QuoteStatusIds = GetQuoteTweetStatusIds(entities) | |
834 | + this.ExtractEntities(entities, post.ReplyToList, post.Media); | |
835 | + | |
836 | + post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) | |
833 | 837 | .Where(x => x != post.StatusId && x != post.RetweetedId) |
834 | 838 | .Distinct().ToArray(); |
835 | 839 |
@@ -873,10 +877,13 @@ namespace OpenTween | ||
873 | 877 | /// <summary> |
874 | 878 | /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出 |
875 | 879 | /// </summary> |
876 | - public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities) | |
880 | + public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities, TwitterQuotedStatusPermalink quotedStatusLink) | |
877 | 881 | { |
878 | 882 | var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl); |
879 | 883 | |
884 | + if (quotedStatusLink != null) | |
885 | + urls = urls.Concat(new[] { quotedStatusLink.Expanded }); | |
886 | + | |
880 | 887 | return GetQuoteTweetStatusIds(urls); |
881 | 888 | } |
882 | 889 |
@@ -1181,16 +1188,19 @@ namespace OpenTween | ||
1181 | 1188 | //本文 |
1182 | 1189 | var textFromApi = message.Text; |
1183 | 1190 | //HTMLに整形 |
1184 | - post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media); | |
1185 | - post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities); | |
1191 | + post.Text = CreateHtmlAnchor(textFromApi, message.Entities, quotedStatusLink: null); | |
1192 | + post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities, quotedStatusLink: null); | |
1186 | 1193 | post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); |
1187 | 1194 | post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); |
1188 | - post.AccessibleText = this.CreateAccessibleText(textFromApi, message.Entities, quoteStatus: null); | |
1195 | + post.AccessibleText = CreateAccessibleText(textFromApi, message.Entities, quotedStatus: null, quotedStatusLink: null); | |
1189 | 1196 | post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); |
1190 | 1197 | post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); |
1191 | 1198 | post.IsFav = false; |
1192 | 1199 | |
1193 | - post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray(); | |
1200 | + this.ExtractEntities(message.Entities, post.ReplyToList, post.Media); | |
1201 | + | |
1202 | + post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities, quotedStatusLink: null) | |
1203 | + .Distinct().ToArray(); | |
1194 | 1204 | |
1195 | 1205 | post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>() |
1196 | 1206 | .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) |
@@ -1326,7 +1336,7 @@ namespace OpenTween | ||
1326 | 1336 | tab.OldestId = minimumId.Value; |
1327 | 1337 | } |
1328 | 1338 | |
1329 | - private string ReplaceTextFromApi(string text, TwitterEntities entities) | |
1339 | + private string ReplaceTextFromApi(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink) | |
1330 | 1340 | { |
1331 | 1341 | if (entities != null) |
1332 | 1342 | { |
@@ -1345,10 +1355,14 @@ namespace OpenTween | ||
1345 | 1355 | } |
1346 | 1356 | } |
1347 | 1357 | } |
1358 | + | |
1359 | + if (quotedStatusLink != null) | |
1360 | + text += " " + quotedStatusLink.Display; | |
1361 | + | |
1348 | 1362 | return text; |
1349 | 1363 | } |
1350 | 1364 | |
1351 | - private string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quoteStatus) | |
1365 | + internal static string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quotedStatus, TwitterQuotedStatusPermalink quotedStatusLink) | |
1352 | 1366 | { |
1353 | 1367 | if (entities == null) |
1354 | 1368 | return text; |
@@ -1357,19 +1371,19 @@ namespace OpenTween | ||
1357 | 1371 | { |
1358 | 1372 | foreach (var entity in entities.Urls) |
1359 | 1373 | { |
1360 | - if (quoteStatus != null) | |
1374 | + if (quotedStatus != null) | |
1361 | 1375 | { |
1362 | 1376 | var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl); |
1363 | - if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quoteStatus.IdStr) | |
1377 | + if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr) | |
1364 | 1378 | { |
1365 | - var quoteText = this.CreateAccessibleText(quoteStatus.FullText, quoteStatus.MergedEntities, quoteStatus: null); | |
1366 | - text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quoteStatus.User.ScreenName, quoteText)); | |
1379 | + var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null); | |
1380 | + text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText)); | |
1381 | + continue; | |
1367 | 1382 | } |
1368 | 1383 | } |
1369 | - else if (!string.IsNullOrEmpty(entity.DisplayUrl)) | |
1370 | - { | |
1384 | + | |
1385 | + if (!string.IsNullOrEmpty(entity.DisplayUrl)) | |
1371 | 1386 | text = text.Replace(entity.Url, entity.DisplayUrl); |
1372 | - } | |
1373 | 1387 | } |
1374 | 1388 | } |
1375 | 1389 |
@@ -1388,6 +1402,12 @@ namespace OpenTween | ||
1388 | 1402 | } |
1389 | 1403 | } |
1390 | 1404 | |
1405 | + if (quotedStatusLink != null) | |
1406 | + { | |
1407 | + var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null); | |
1408 | + text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText); | |
1409 | + } | |
1410 | + | |
1391 | 1411 | return text; |
1392 | 1412 | } |
1393 | 1413 |
@@ -1546,7 +1566,7 @@ namespace OpenTween | ||
1546 | 1566 | } |
1547 | 1567 | } |
1548 | 1568 | |
1549 | - public string CreateHtmlAnchor(string text, List<Tuple<long, string>> AtList, TwitterEntities entities, List<MediaInfo> media) | |
1569 | + private void ExtractEntities(TwitterEntities entities, List<Tuple<long, string>> AtList, List<MediaInfo> media) | |
1550 | 1570 | { |
1551 | 1571 | if (entities != null) |
1552 | 1572 | { |
@@ -1588,13 +1608,23 @@ namespace OpenTween | ||
1588 | 1608 | } |
1589 | 1609 | } |
1590 | 1610 | } |
1611 | + } | |
1591 | 1612 | |
1613 | + internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink) | |
1614 | + { | |
1592 | 1615 | // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない |
1593 | 1616 | text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true); |
1594 | 1617 | |
1595 | 1618 | 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>"); |
1596 | 1619 | text = PreProcessUrl(text); //IDN置換 |
1597 | 1620 | |
1621 | + if (quotedStatusLink != null) | |
1622 | + { | |
1623 | + text += string.Format(" <a href=\"{0}\" title=\"{0}\">{1}</a>", | |
1624 | + WebUtility.HtmlEncode(quotedStatusLink.Url), | |
1625 | + WebUtility.HtmlEncode(quotedStatusLink.Display)); | |
1626 | + } | |
1627 | + | |
1598 | 1628 | return text; |
1599 | 1629 | } |
1600 | 1630 |