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.
11 // This file is part of OpenTween.
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)
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
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.
31 using System.Collections.Generic;
32 using System.Diagnostics.CodeAnalysis;
36 using System.Threading;
37 using System.Threading.Tasks;
39 namespace OpenTween.Models
41 public class PostClass : ICloneable
43 public readonly struct StatusGeo : IEquatable<StatusGeo>
45 public double Longitude { get; }
46 public double Latitude { get; }
48 public StatusGeo(double longitude, double latitude)
50 this.Longitude = longitude;
51 this.Latitude = latitude;
54 public override int GetHashCode()
55 => this.Longitude.GetHashCode() ^ this.Latitude.GetHashCode();
57 public override bool Equals(object obj)
58 => obj is StatusGeo && this.Equals((StatusGeo)obj);
60 public bool Equals(StatusGeo other)
61 => this.Longitude == other.Longitude && this.Latitude == other.Longitude;
63 public static bool operator ==(StatusGeo left, StatusGeo right)
64 => left.Equals(right);
66 public static bool operator !=(StatusGeo left, StatusGeo right)
67 => !left.Equals(right);
70 public string Nickname { get; set; } = "";
71 public string TextFromApi { get; set; } = "";
73 /// <summary>スクリーンリーダーでの読み上げを考慮したテキスト</summary>
74 public string AccessibleText { get; set; } = "";
76 public string ImageUrl { get; set; } = "";
77 public string ScreenName { get; set; } = "";
78 public DateTimeUtc CreatedAt { get; set; }
79 public long StatusId { get; set; }
86 if (this.expandComplatedAll)
89 var expandedHtml = this.ReplaceToExpandedUrl(this._text, out this.expandComplatedAll);
90 if (this.expandComplatedAll)
91 this._text = expandedHtml;
95 set => this._text = value;
97 private string _text = "";
99 public bool IsRead { get; set; }
100 public bool IsReply { get; set; }
101 public bool IsExcludeReply { get; set; }
102 private bool _IsProtect;
103 public bool IsOwl { get; set; }
104 private bool _IsMark;
105 public string? InReplyToUser { get; set; }
106 private long? _InReplyToStatusId;
107 public string Source { get; set; } = "";
108 public Uri? SourceUri { get; set; }
109 public List<(long UserId, string ScreenName)> ReplyToList { get; set; }
110 public bool IsMe { get; set; }
111 public bool IsDm { get; set; }
112 public long UserId { get; set; }
113 public bool FilterHit { get; set; }
114 public string? RetweetedBy { get; set; }
115 public long? RetweetedId { get; set; }
116 private bool _IsDeleted = false;
117 private StatusGeo? _postGeo = null;
118 public int RetweetedCount { get; set; }
119 public long? RetweetedByUserId { get; set; }
120 public long? InReplyToUserId { get; set; }
121 public List<MediaInfo> Media { get; set; }
122 public long[] QuoteStatusIds { get; set; }
123 public ExpandedUrlInfo[] ExpandedUrls { get; set; }
126 /// <see cref="PostClass"/> に含まれる t.co の展開後の URL を保持するクラス
128 public class ExpandedUrlInfo : ICloneable
130 /// <summary>展開前の t.co ドメインの URL</summary>
131 public string Url { get; }
133 /// <summary>展開後の URL</summary>
135 /// <see cref="ShortUrl"/> による展開が完了するまでは Entity に含まれる expanded_url の値を返します
137 public string ExpandedUrl => this._expandedUrl;
139 /// <summary><see cref="ShortUrl"/> による展開を行うタスク</summary>
140 public Task ExpandTask { get; private set; }
142 /// <summary><see cref="DeepExpandAsync"/> による展開が完了したか否か</summary>
143 public bool ExpandedCompleted => this.ExpandTask.IsCompleted;
145 protected string _expandedUrl;
147 public ExpandedUrlInfo(string url, string expandedUrl)
148 : this(url, expandedUrl, deepExpand: true)
152 public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand)
155 this._expandedUrl = expandedUrl;
158 this.ExpandTask = this.DeepExpandAsync();
160 this.ExpandTask = Task.CompletedTask;
163 protected virtual async Task DeepExpandAsync()
165 var origUrl = this._expandedUrl;
166 var newUrl = await ShortUrl.Instance.ExpandUrlAsync(origUrl)
167 .ConfigureAwait(false);
169 Interlocked.CompareExchange(ref this._expandedUrl, newUrl, origUrl);
172 public ExpandedUrlInfo Clone()
173 => new ExpandedUrlInfo(this.Url, this.ExpandedUrl, deepExpand: false);
175 object ICloneable.Clone()
179 public int FavoritedCount { get; set; }
181 private States _states = States.None;
182 private bool expandComplatedAll = false;
196 Media = new List<MediaInfo>();
197 ReplyToList = new List<(long, string)>();
198 QuoteStatusIds = Array.Empty<long>();
199 ExpandedUrls = Array.Empty<ExpandedUrlInfo>();
202 public string TextSingleLine
203 => this.TextFromApi.Replace("\n", " ");
209 if (this.RetweetedId != null)
211 var post = this.RetweetSource;
223 if (this.RetweetedId != null)
225 var post = this.RetweetSource;
234 public bool IsProtect
236 get => this._IsProtect;
240 _states |= States.Protect;
242 _states &= ~States.Protect;
253 _states |= States.Mark;
255 _states &= ~States.Mark;
260 public long? InReplyToStatusId
262 get => this._InReplyToStatusId;
266 _states |= States.Reply;
268 _states &= ~States.Reply;
270 _InReplyToStatusId = value;
274 public bool IsDeleted
276 get => this._IsDeleted;
281 this.InReplyToStatusId = null;
282 this.InReplyToUser = "";
283 this.InReplyToUserId = null;
284 this.IsReply = false;
285 this.ReplyToList = new List<(long, string)>();
286 this._states = States.None;
292 protected virtual PostClass? RetweetSource
293 => this.RetweetedId != null ? TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value) : null;
295 public StatusGeo? PostGeo
297 get => this._postGeo;
302 _states |= States.Geo;
306 _states &= ~States.Geo;
312 public int StateIndex
316 public string SourceHtml
320 if (this.SourceUri == null)
321 return WebUtility.HtmlEncode(this.Source);
323 return string.Format("<a href=\"{0}\" rel=\"nofollow\">{1}</a>",
324 WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source));
329 /// このツイートが指定したユーザーによって削除可能であるかを判定します
331 /// <param name="selfUserId">ツイートを削除しようとするユーザーのID</param>
332 /// <returns>削除可能であれば true、そうでなければ false</returns>
333 public bool CanDeleteBy(long selfUserId)
335 // 自分が送った DM と自分に届いた DM のどちらも削除可能
339 // 自分のツイート or 他人に RT された自分のツイート
340 if (this.UserId == selfUserId)
344 if (this.RetweetedByUserId == selfUserId)
351 /// このツイートが指定したユーザーによってリツイート可能であるかを判定します
353 /// <param name="selfUserId">リツイートしようとするユーザーのID</param>
354 /// <returns>リツイート可能であれば true、そうでなければ false</returns>
355 public bool CanRetweetBy(long selfUserId)
361 // 自分のツイートであれば鍵垢であるかに関わらずリツイート可
362 if (this.UserId == selfUserId)
365 return !this.IsProtect;
368 public PostClass ConvertToOriginalPost()
370 if (this.RetweetedId == null)
371 throw new InvalidOperationException();
373 var originalPost = this.Clone();
375 originalPost.StatusId = this.RetweetedId.Value;
376 originalPost.RetweetedId = null;
377 originalPost.RetweetedBy = "";
378 originalPost.RetweetedByUserId = null;
379 originalPost.RetweetedCount = 1;
384 public string GetExpandedUrl(string urlStr)
386 var urlInfo = this.ExpandedUrls.FirstOrDefault(x => x.Url == urlStr);
390 return urlInfo.ExpandedUrl;
393 public string[] GetExpandedUrls()
394 => this.ExpandedUrls.Select(x => x.ExpandedUrl).ToArray();
397 /// <paramref name="html"/> に含まれる短縮 URL を展開済みの URL に置換します
399 /// <param name="html">置換する対象の HTML 文字列</param>
400 /// <param name="completedAll">全ての URL の展開が完了していれば true、未完了の URL があれば false</param>
401 private string ReplaceToExpandedUrl(string html, out bool completedAll)
403 if (this.ExpandedUrls.Length == 0)
411 foreach (var urlInfo in this.ExpandedUrls)
413 if (!urlInfo.ExpandedCompleted)
414 completedAll = false;
416 var tcoUrl = urlInfo.Url;
417 var expandedUrl = MyCommon.ConvertToReadableUrl(urlInfo.ExpandedUrl);
418 html = html.Replace($"title=\"{WebUtility.HtmlEncode(tcoUrl)}\"",
419 $"title=\"{WebUtility.HtmlEncode(expandedUrl)}\"");
425 public PostClass Clone()
427 var clone = (PostClass)this.MemberwiseClone();
428 clone.ReplyToList = new List<(long, string)>(this.ReplyToList);
429 clone.Media = new List<MediaInfo>(this.Media);
430 clone.QuoteStatusIds = this.QuoteStatusIds.ToArray();
431 clone.ExpandedUrls = this.ExpandedUrls.Select(x => x.Clone()).ToArray();
436 object ICloneable.Clone()
439 public override bool Equals(object? obj)
441 if (obj == null || this.GetType() != obj.GetType()) return false;
442 return this.Equals((PostClass)obj);
445 public bool Equals(PostClass? other)
447 if (other == null) return false;
448 return (this.Nickname == other.Nickname) &&
449 (this.TextFromApi == other.TextFromApi) &&
450 (this.ImageUrl == other.ImageUrl) &&
451 (this.ScreenName == other.ScreenName) &&
452 (this.CreatedAt == other.CreatedAt) &&
453 (this.StatusId == other.StatusId) &&
454 (this.IsFav == other.IsFav) &&
455 (this.Text == other.Text) &&
456 (this.IsRead == other.IsRead) &&
457 (this.IsReply == other.IsReply) &&
458 (this.IsExcludeReply == other.IsExcludeReply) &&
459 (this.IsProtect == other.IsProtect) &&
460 (this.IsOwl == other.IsOwl) &&
461 (this.IsMark == other.IsMark) &&
462 (this.InReplyToUser == other.InReplyToUser) &&
463 (this.InReplyToStatusId == other.InReplyToStatusId) &&
464 (this.Source == other.Source) &&
465 (this.SourceUri == other.SourceUri) &&
466 (this.ReplyToList.SequenceEqual(other.ReplyToList)) &&
467 (this.IsMe == other.IsMe) &&
468 (this.IsDm == other.IsDm) &&
469 (this.UserId == other.UserId) &&
470 (this.FilterHit == other.FilterHit) &&
471 (this.RetweetedBy == other.RetweetedBy) &&
472 (this.RetweetedId == other.RetweetedId) &&
473 (this.IsDeleted == other.IsDeleted) &&
474 (this.InReplyToUserId == other.InReplyToUserId);
478 public override int GetHashCode()
479 => this.StatusId.GetHashCode();