OSDN Git Service

C#7.0で追加されたExpression-Bodiedメンバの構文を使用する
[opentween/open-tween.git] / OpenTween / Models / PostClass.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 //           (c) 2008-2011 Moz (@syo68k)
4 //           (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 //           (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 //           (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 //           (c) 2011      Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 //           (c) 2012      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
10 //
11 // This file is part of OpenTween.
12 //
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
16 // any later version.
17 //
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21 // for more details.
22 //
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
27
28 using System;
29 using System.Collections.Generic;
30 using System.Linq;
31 using System.Net;
32 using System.Text;
33 using System.Threading;
34 using System.Threading.Tasks;
35
36 namespace OpenTween.Models
37 {
38     public class PostClass : ICloneable
39     {
40         public struct StatusGeo : IEquatable<StatusGeo>
41         {
42             public double Longitude { get; }
43             public double Latitude { get; }
44
45             public StatusGeo(double longitude, double latitude)
46             {
47                 this.Longitude = longitude;
48                 this.Latitude = latitude;
49             }
50
51             public override int GetHashCode()
52                 => this.Longitude.GetHashCode() ^ this.Latitude.GetHashCode();
53
54             public override bool Equals(object obj)
55                 => obj is StatusGeo && this.Equals((StatusGeo)obj);
56
57             public bool Equals(StatusGeo other)
58                 => this.Longitude == other.Longitude && this.Latitude == other.Longitude;
59
60             public static bool operator ==(StatusGeo left, StatusGeo right)
61                 => left.Equals(right);
62
63             public static bool operator !=(StatusGeo left, StatusGeo right)
64                 => !left.Equals(right);
65         }
66         public string Nickname { get; set; }
67         public string TextFromApi { get; set; }
68
69         /// <summary>スクリーンリーダーでの読み上げを考慮したテキスト</summary>
70         public string AccessibleText { get; set; }
71
72         public string ImageUrl { get; set; }
73         public string ScreenName { get; set; }
74         public DateTime CreatedAt { get; set; }
75         public long StatusId { get; set; }
76         private bool _IsFav;
77
78         public string Text
79         {
80             get
81             {
82                 if (this.expandComplatedAll)
83                     return this._text;
84
85                 var expandedHtml = this.ReplaceToExpandedUrl(this._text, out this.expandComplatedAll);
86                 if (this.expandComplatedAll)
87                     this._text = expandedHtml;
88
89                 return expandedHtml;
90             }
91             set => this._text = value;
92         }
93         private string _text;
94
95         public bool IsRead { get; set; }
96         public bool IsReply { get; set; }
97         public bool IsExcludeReply { get; set; }
98         private bool _IsProtect;
99         public bool IsOwl { get; set; }
100         private bool _IsMark;
101         public string InReplyToUser { get; set; }
102         private long? _InReplyToStatusId;
103         public string Source { get; set; }
104         public Uri SourceUri { get; set; }
105         public List<string> ReplyToList { get; set; }
106         public bool IsMe { get; set; }
107         public bool IsDm { get; set; }
108         public long UserId { get; set; }
109         public bool FilterHit { get; set; }
110         public string RetweetedBy { get; set; }
111         public long? RetweetedId { get; set; }
112         private bool _IsDeleted = false;
113         private StatusGeo? _postGeo = null;
114         public int RetweetedCount { get; set; }
115         public long? RetweetedByUserId { get; set; }
116         public long? InReplyToUserId { get; set; }
117         public List<MediaInfo> Media { get; set; }
118         public long[] QuoteStatusIds { get; set; }
119         public ExpandedUrlInfo[] ExpandedUrls { get; set; }
120
121         /// <summary>
122         /// <see cref="PostClass"/> に含まれる t.co の展開後の URL を保持するクラス
123         /// </summary>
124         public class ExpandedUrlInfo : ICloneable
125         {
126             /// <summary>展開前の t.co ドメインの URL</summary>
127             public string Url { get; }
128
129             /// <summary>展開後の URL</summary>
130             /// <remarks>
131             /// <see cref="ShortUrl"/> による展開が完了するまでは Entity に含まれる expanded_url の値を返します
132             /// </remarks>
133             public string ExpandedUrl => this._expandedUrl;
134
135             /// <summary><see cref="ShortUrl"/> による展開を行うタスク</summary>
136             public Task ExpandTask { get; private set; }
137
138             /// <summary><see cref="DeepExpandAsync"/> による展開が完了したか否か</summary>
139             public bool ExpandedCompleted => this.ExpandTask.IsCompleted;
140
141             protected string _expandedUrl;
142
143             public ExpandedUrlInfo(string url, string expandedUrl)
144                 : this(url, expandedUrl, deepExpand: true)
145             {
146             }
147
148             public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand)
149             {
150                 this.Url = url;
151                 this._expandedUrl = expandedUrl;
152
153                 if (deepExpand)
154                     this.ExpandTask = this.DeepExpandAsync();
155                 else
156                     this.ExpandTask = Task.FromResult(0);
157             }
158
159             protected virtual async Task DeepExpandAsync()
160             {
161                 var origUrl = this._expandedUrl;
162                 var newUrl = await ShortUrl.Instance.ExpandUrlAsync(origUrl)
163                     .ConfigureAwait(false);
164
165                 Interlocked.CompareExchange(ref this._expandedUrl, newUrl, origUrl);
166             }
167
168             public ExpandedUrlInfo Clone()
169                 => new ExpandedUrlInfo(this.Url, this.ExpandedUrl, deepExpand: false);
170
171             object ICloneable.Clone()
172                 => this.Clone();
173         }
174
175         public int FavoritedCount { get; set; }
176
177         private States _states = States.None;
178         private bool expandComplatedAll = false;
179
180         [Flags]
181         private enum States
182         {
183             None = 0,
184             Protect = 1,
185             Mark = 2,
186             Reply = 4,
187             Geo = 8,
188         }
189
190         public PostClass()
191         {
192             RetweetedBy = "";
193             Media = new List<MediaInfo>();
194             ReplyToList = new List<string>();
195             QuoteStatusIds = new long[0];
196             ExpandedUrls = new ExpandedUrlInfo[0];
197         }
198
199         public string TextSingleLine
200         {
201             get
202             {
203                 return this.TextFromApi == null ? null : this.TextFromApi.Replace("\n", " ");
204             }
205         }
206
207         public bool IsFav
208         {
209             get
210             {
211                 if (this.RetweetedId != null)
212                 {
213                     var post = this.RetweetSource;
214                     if (post != null)
215                     {
216                         return post.IsFav;
217                     }
218                 }
219
220                 return _IsFav;
221             }
222             set
223             {
224                 _IsFav = value;
225                 if (this.RetweetedId != null)
226                 {
227                     var post = this.RetweetSource;
228                     if (post != null)
229                     {
230                         post.IsFav = value;
231                     }
232                 }
233             }
234         }
235
236         public bool IsProtect
237         {
238             get
239             {
240                 return _IsProtect;
241             }
242             set
243             {
244                 if (value)
245                 {
246                     _states = _states | States.Protect;
247                 }
248                 else
249                 {
250                     _states = _states & ~States.Protect;
251                 }
252                 _IsProtect = value;
253             }
254         }
255         public bool IsMark
256         {
257             get
258             {
259                 return _IsMark;
260             }
261             set
262             {
263                 if (value)
264                 {
265                     _states = _states | States.Mark;
266                 }
267                 else
268                 {
269                     _states = _states & ~States.Mark;
270                 }
271                 _IsMark = value;
272             }
273         }
274         public long? InReplyToStatusId
275         {
276             get
277             {
278                 return _InReplyToStatusId;
279             }
280             set
281             {
282                 if (value != null)
283                 {
284                     _states = _states | States.Reply;
285                 }
286                 else
287                 {
288                     _states = _states & ~States.Reply;
289                 }
290                 _InReplyToStatusId = value;
291             }
292         }
293
294         public bool IsDeleted
295         {
296             get
297             {
298                 return _IsDeleted;
299             }
300             set
301             {
302                 if (value)
303                 {
304                     this.InReplyToStatusId = null;
305                     this.InReplyToUser = "";
306                     this.InReplyToUserId = null;
307                     this.IsReply = false;
308                     this.ReplyToList = new List<string>();
309                     this._states = States.None;
310                 }
311                 _IsDeleted = value;
312             }
313         }
314
315         protected virtual PostClass RetweetSource
316         {
317             get
318             {
319                 return TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value);
320             }
321         }
322
323         public StatusGeo? PostGeo
324         {
325             get
326             {
327                 return _postGeo;
328             }
329             set
330             {
331                 if (value != null)
332                 {
333                     _states |= States.Geo;
334                 }
335                 else
336                 {
337                     _states &= ~States.Geo;
338                 }
339                 _postGeo = value;
340             }
341         }
342
343         public int StateIndex
344         {
345             get
346             {
347                 return (int)_states - 1;
348             }
349         }
350
351         // 互換性のために用意
352         public string SourceHtml
353         {
354             get
355             {
356                 if (this.SourceUri == null)
357                     return WebUtility.HtmlEncode(this.Source);
358
359                 return string.Format("<a href=\"{0}\" rel=\"nofollow\">{1}</a>",
360                     WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source));
361             }
362         }
363
364         /// <summary>
365         /// このツイートが指定したユーザーによって削除可能であるかを判定します
366         /// </summary>
367         /// <param name="selfUserId">ツイートを削除しようとするユーザーのID</param>
368         /// <returns>削除可能であれば true、そうでなければ false</returns>
369         public bool CanDeleteBy(long selfUserId)
370         {
371             // 自分が送った DM と自分に届いた DM のどちらも削除可能
372             if (this.IsDm)
373                 return true;
374
375             // 自分のツイート or 他人に RT された自分のツイート
376             if (this.UserId == selfUserId)
377                 return true;
378
379             // 自分が RT したツイート
380             if (this.RetweetedByUserId == selfUserId)
381                 return true;
382
383             return false;
384         }
385
386         /// <summary>
387         /// このツイートが指定したユーザーによってリツイート可能であるかを判定します
388         /// </summary>
389         /// <param name="selfUserId">リツイートしようとするユーザーのID</param>
390         /// <returns>リツイート可能であれば true、そうでなければ false</returns>
391         public bool CanRetweetBy(long selfUserId)
392         {
393             // DM は常にリツイート不可
394             if (this.IsDm)
395                 return false;
396
397             // 自分のツイートであれば鍵垢であるかに関わらずリツイート可
398             if (this.UserId == selfUserId)
399                 return true;
400
401             return !this.IsProtect;
402         }
403
404         public PostClass ConvertToOriginalPost()
405         {
406             if (this.RetweetedId == null)
407                 throw new InvalidOperationException();
408
409             var originalPost = this.Clone();
410
411             originalPost.StatusId = this.RetweetedId.Value;
412             originalPost.RetweetedId = null;
413             originalPost.RetweetedBy = "";
414             originalPost.RetweetedByUserId = null;
415             originalPost.RetweetedCount = 1;
416
417             return originalPost;
418         }
419
420         public string GetExpandedUrl(string urlStr)
421         {
422             var urlInfo = this.ExpandedUrls.FirstOrDefault(x => x.Url == urlStr);
423             if (urlInfo == null)
424                 return urlStr;
425
426             return urlInfo.ExpandedUrl;
427         }
428
429         public string[] GetExpandedUrls()
430             => this.ExpandedUrls.Select(x => x.ExpandedUrl).ToArray();
431
432         /// <summary>
433         /// <paramref name="html"/> に含まれる短縮 URL を展開済みの URL に置換します
434         /// </summary>
435         /// <param name="html">置換する対象の HTML 文字列</param>
436         /// <param name="completedAll">全ての URL の展開が完了していれば true、未完了の URL があれば false</param>
437         private string ReplaceToExpandedUrl(string html, out bool completedAll)
438         {
439             if (this.ExpandedUrls.Length == 0)
440             {
441                 completedAll = true;
442                 return html;
443             }
444
445             completedAll = true;
446
447             foreach (var urlInfo in this.ExpandedUrls)
448             {
449                 if (!urlInfo.ExpandedCompleted)
450                     completedAll = false;
451
452                 var tcoUrl = urlInfo.Url;
453                 var expandedUrl = MyCommon.ConvertToReadableUrl(urlInfo.ExpandedUrl);
454                 html = html.Replace($"title=\"{WebUtility.HtmlEncode(tcoUrl)}\"",
455                     $"title=\"{WebUtility.HtmlEncode(expandedUrl)}\"");
456             }
457
458             return html;
459         }
460
461         public PostClass Clone()
462         {
463             var clone = (PostClass)this.MemberwiseClone();
464             clone.ReplyToList = new List<string>(this.ReplyToList);
465             clone.Media = new List<MediaInfo>(this.Media);
466             clone.QuoteStatusIds = this.QuoteStatusIds.ToArray();
467             clone.ExpandedUrls = this.ExpandedUrls.Select(x => x.Clone()).ToArray();
468
469             return clone;
470         }
471
472         object ICloneable.Clone()
473             => this.Clone();
474
475         public override bool Equals(object obj)
476         {
477             if (obj == null || this.GetType() != obj.GetType()) return false;
478             return this.Equals((PostClass)obj);
479         }
480
481         public bool Equals(PostClass other)
482         {
483             if (other == null) return false;
484             return (this.Nickname == other.Nickname) &&
485                     (this.TextFromApi == other.TextFromApi) &&
486                     (this.ImageUrl == other.ImageUrl) &&
487                     (this.ScreenName == other.ScreenName) &&
488                     (this.CreatedAt == other.CreatedAt) &&
489                     (this.StatusId == other.StatusId) &&
490                     (this.IsFav == other.IsFav) &&
491                     (this.Text == other.Text) &&
492                     (this.IsRead == other.IsRead) &&
493                     (this.IsReply == other.IsReply) &&
494                     (this.IsExcludeReply == other.IsExcludeReply) &&
495                     (this.IsProtect == other.IsProtect) &&
496                     (this.IsOwl == other.IsOwl) &&
497                     (this.IsMark == other.IsMark) &&
498                     (this.InReplyToUser == other.InReplyToUser) &&
499                     (this.InReplyToStatusId == other.InReplyToStatusId) &&
500                     (this.Source == other.Source) &&
501                     (this.SourceUri == other.SourceUri) &&
502                     (this.ReplyToList.SequenceEqual(other.ReplyToList)) &&
503                     (this.IsMe == other.IsMe) &&
504                     (this.IsDm == other.IsDm) &&
505                     (this.UserId == other.UserId) &&
506                     (this.FilterHit == other.FilterHit) &&
507                     (this.RetweetedBy == other.RetweetedBy) &&
508                     (this.RetweetedId == other.RetweetedId) &&
509                     (this.IsDeleted == other.IsDeleted) &&
510                     (this.InReplyToUserId == other.InReplyToUserId);
511
512         }
513
514         public override int GetHashCode()
515         {
516             return this.StatusId.GetHashCode();
517         }
518     }
519 }