// OpenTween - Client of Twitter // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) // (c) 2008-2011 Moz (@syo68k) // (c) 2008-2011 takeshik (@takeshik) // (c) 2010-2011 anis774 (@anis774) // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 Egtra (@egtra) // (c) 2012 kim_upsilon (@kim_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 , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace OpenTween.Models { public class PostClass : ICloneable { public readonly struct StatusGeo : IEquatable { public double Longitude { get; } public double Latitude { get; } public StatusGeo(double longitude, double latitude) { this.Longitude = longitude; this.Latitude = latitude; } public override int GetHashCode() => this.Longitude.GetHashCode() ^ this.Latitude.GetHashCode(); public override bool Equals(object obj) => obj is StatusGeo && this.Equals((StatusGeo)obj); public bool Equals(StatusGeo other) => this.Longitude == other.Longitude && this.Latitude == other.Longitude; public static bool operator ==(StatusGeo left, StatusGeo right) => left.Equals(right); public static bool operator !=(StatusGeo left, StatusGeo right) => !left.Equals(right); } public string Nickname { get; set; } = ""; public string TextFromApi { get; set; } = ""; /// スクリーンリーダーでの読み上げを考慮したテキスト public string AccessibleText { get; set; } = ""; public string ImageUrl { get; set; } = ""; public string ScreenName { get; set; } = ""; public DateTimeUtc CreatedAt { get; set; } public long StatusId { get; set; } private bool _IsFav; public string Text { get { if (this.expandComplatedAll) return this._text; var expandedHtml = this.ReplaceToExpandedUrl(this._text, out this.expandComplatedAll); if (this.expandComplatedAll) this._text = expandedHtml; return expandedHtml; } set => this._text = value; } private string _text = ""; public bool IsRead { get; set; } public bool IsReply { get; set; } public bool IsExcludeReply { get; set; } private bool _IsProtect; public bool IsOwl { get; set; } private bool _IsMark; public string? InReplyToUser { get; set; } private long? _InReplyToStatusId; public string Source { get; set; } = ""; public Uri? SourceUri { get; set; } public List<(long UserId, string ScreenName)> ReplyToList { get; set; } public bool IsMe { get; set; } public bool IsDm { get; set; } public long UserId { get; set; } public bool FilterHit { get; set; } public string? RetweetedBy { get; set; } public long? RetweetedId { get; set; } private bool _IsDeleted = false; private StatusGeo? _postGeo = null; public int RetweetedCount { get; set; } public long? RetweetedByUserId { get; set; } public long? InReplyToUserId { get; set; } public List Media { get; set; } public long[] QuoteStatusIds { get; set; } public ExpandedUrlInfo[] ExpandedUrls { get; set; } /// /// に含まれる t.co の展開後の URL を保持するクラス /// public class ExpandedUrlInfo : ICloneable { /// 展開前の t.co ドメインの URL public string Url { get; } /// 展開後の URL /// /// による展開が完了するまでは Entity に含まれる expanded_url の値を返します /// public string ExpandedUrl => this._expandedUrl; /// による展開を行うタスク public Task ExpandTask { get; private set; } /// による展開が完了したか否か public bool ExpandedCompleted => this.ExpandTask.IsCompleted; protected string _expandedUrl; public ExpandedUrlInfo(string url, string expandedUrl) : this(url, expandedUrl, deepExpand: true) { } public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) { this.Url = url; this._expandedUrl = expandedUrl; if (deepExpand) this.ExpandTask = this.DeepExpandAsync(); else this.ExpandTask = Task.CompletedTask; } protected virtual async Task DeepExpandAsync() { var origUrl = this._expandedUrl; var newUrl = await ShortUrl.Instance.ExpandUrlAsync(origUrl) .ConfigureAwait(false); Interlocked.CompareExchange(ref this._expandedUrl, newUrl, origUrl); } public ExpandedUrlInfo Clone() => new ExpandedUrlInfo(this.Url, this.ExpandedUrl, deepExpand: false); object ICloneable.Clone() => this.Clone(); } public int FavoritedCount { get; set; } private States _states = States.None; private bool expandComplatedAll = false; [Flags] private enum States { None = 0, Protect = 1, Mark = 2, Reply = 4, Geo = 8, } public PostClass() { Media = new List(); ReplyToList = new List<(long, string)>(); QuoteStatusIds = Array.Empty(); ExpandedUrls = Array.Empty(); } public string TextSingleLine => this.TextFromApi.Replace("\n", " "); public bool IsFav { get { if (this.RetweetedId != null) { var post = this.RetweetSource; if (post != null) { return post.IsFav; } } return _IsFav; } set { _IsFav = value; if (this.RetweetedId != null) { var post = this.RetweetSource; if (post != null) { post.IsFav = value; } } } } public bool IsProtect { get => this._IsProtect; set { if (value) _states |= States.Protect; else _states &= ~States.Protect; _IsProtect = value; } } public bool IsMark { get => this._IsMark; set { if (value) _states |= States.Mark; else _states &= ~States.Mark; _IsMark = value; } } public long? InReplyToStatusId { get => this._InReplyToStatusId; set { if (value != null) _states |= States.Reply; else _states &= ~States.Reply; _InReplyToStatusId = value; } } public bool IsDeleted { get => this._IsDeleted; set { if (value) { this.InReplyToStatusId = null; this.InReplyToUser = ""; this.InReplyToUserId = null; this.IsReply = false; this.ReplyToList = new List<(long, string)>(); this._states = States.None; } _IsDeleted = value; } } protected virtual PostClass? RetweetSource => this.RetweetedId != null ? TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value) : null; public StatusGeo? PostGeo { get => this._postGeo; set { if (value != null) { _states |= States.Geo; } else { _states &= ~States.Geo; } _postGeo = value; } } public int StateIndex => (int)_states - 1; // 互換性のために用意 public string SourceHtml { get { if (this.SourceUri == null) return WebUtility.HtmlEncode(this.Source); return string.Format("{1}", WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source)); } } /// /// このツイートが指定したユーザーによって削除可能であるかを判定します /// /// ツイートを削除しようとするユーザーのID /// 削除可能であれば true、そうでなければ false public bool CanDeleteBy(long selfUserId) { // 自分が送った DM と自分に届いた DM のどちらも削除可能 if (this.IsDm) return true; // 自分のツイート or 他人に RT された自分のツイート if (this.UserId == selfUserId) return true; // 自分が RT したツイート if (this.RetweetedByUserId == selfUserId) return true; return false; } /// /// このツイートが指定したユーザーによってリツイート可能であるかを判定します /// /// リツイートしようとするユーザーのID /// リツイート可能であれば true、そうでなければ false public bool CanRetweetBy(long selfUserId) { // DM は常にリツイート不可 if (this.IsDm) return false; // 自分のツイートであれば鍵垢であるかに関わらずリツイート可 if (this.UserId == selfUserId) return true; return !this.IsProtect; } public PostClass ConvertToOriginalPost() { if (this.RetweetedId == null) throw new InvalidOperationException(); var originalPost = this.Clone(); originalPost.StatusId = this.RetweetedId.Value; originalPost.RetweetedId = null; originalPost.RetweetedBy = ""; originalPost.RetweetedByUserId = null; originalPost.RetweetedCount = 1; return originalPost; } public string GetExpandedUrl(string urlStr) { var urlInfo = this.ExpandedUrls.FirstOrDefault(x => x.Url == urlStr); if (urlInfo == null) return urlStr; return urlInfo.ExpandedUrl; } public string[] GetExpandedUrls() => this.ExpandedUrls.Select(x => x.ExpandedUrl).ToArray(); /// /// に含まれる短縮 URL を展開済みの URL に置換します /// /// 置換する対象の HTML 文字列 /// 全ての URL の展開が完了していれば true、未完了の URL があれば false private string ReplaceToExpandedUrl(string html, out bool completedAll) { if (this.ExpandedUrls.Length == 0) { completedAll = true; return html; } completedAll = true; foreach (var urlInfo in this.ExpandedUrls) { if (!urlInfo.ExpandedCompleted) completedAll = false; var tcoUrl = urlInfo.Url; var expandedUrl = MyCommon.ConvertToReadableUrl(urlInfo.ExpandedUrl); html = html.Replace($"title=\"{WebUtility.HtmlEncode(tcoUrl)}\"", $"title=\"{WebUtility.HtmlEncode(expandedUrl)}\""); } return html; } public PostClass Clone() { var clone = (PostClass)this.MemberwiseClone(); clone.ReplyToList = new List<(long, string)>(this.ReplyToList); clone.Media = new List(this.Media); clone.QuoteStatusIds = this.QuoteStatusIds.ToArray(); clone.ExpandedUrls = this.ExpandedUrls.Select(x => x.Clone()).ToArray(); return clone; } object ICloneable.Clone() => this.Clone(); public override bool Equals(object? obj) { if (obj == null || this.GetType() != obj.GetType()) return false; return this.Equals((PostClass)obj); } public bool Equals(PostClass? other) { if (other == null) return false; return (this.Nickname == other.Nickname) && (this.TextFromApi == other.TextFromApi) && (this.ImageUrl == other.ImageUrl) && (this.ScreenName == other.ScreenName) && (this.CreatedAt == other.CreatedAt) && (this.StatusId == other.StatusId) && (this.IsFav == other.IsFav) && (this.Text == other.Text) && (this.IsRead == other.IsRead) && (this.IsReply == other.IsReply) && (this.IsExcludeReply == other.IsExcludeReply) && (this.IsProtect == other.IsProtect) && (this.IsOwl == other.IsOwl) && (this.IsMark == other.IsMark) && (this.InReplyToUser == other.InReplyToUser) && (this.InReplyToStatusId == other.InReplyToStatusId) && (this.Source == other.Source) && (this.SourceUri == other.SourceUri) && (this.ReplyToList.SequenceEqual(other.ReplyToList)) && (this.IsMe == other.IsMe) && (this.IsDm == other.IsDm) && (this.UserId == other.UserId) && (this.FilterHit == other.FilterHit) && (this.RetweetedBy == other.RetweetedBy) && (this.RetweetedId == other.RetweetedId) && (this.IsDeleted == other.IsDeleted) && (this.InReplyToUserId == other.InReplyToUserId); } public override int GetHashCode() => this.StatusId.GetHashCode(); } }