// 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;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using System;
-using System.Reflection;
-using System.Collections.Generic;
using System.Windows.Forms;
using OpenTween.Api;
using OpenTween.Api.DataModel;
using OpenTween.Connection;
using OpenTween.Models;
using OpenTween.Setting;
-using System.Globalization;
namespace OpenTween
{
// implied. See the License for the specific language governing
// permissions and limitations under the License.
- //Hashtag用正規表現
+ // Hashtag用正規表現
private const string LATIN_ACCENTS = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff";
private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
- //private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u3096\u3400-\u4DBF\u4E00-\u9FFF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F\u2F800-\u2FA1F";
private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
- private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
- private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
- private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
+ private const string HASHTAG_ALPHA = "[A-Za-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
+ private const string HASHTAG_ALPHANUMERIC = "[A-Za-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
+ private const string HASHTAG_TERMINATOR = "[^A-Za-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
- //URL正規表現
+ // URL正規表現
private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
private const string url_invalid_domain_chars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e";
/// <summary>
/// attachment_url に指定可能な URL を判定する正規表現
/// </summary>
- public static readonly Regex AttachmentUrlRegex = new Regex(@"https?://(
+ public static readonly Regex AttachmentUrlRegex = new Regex(
+ @"https?://(
twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
| mobile\.twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
| twitter\.com/messages/compose\?recipient_id=[0-9]+(&.+)?
-)$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+)$",
+ RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
/// <summary>
/// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
/// </summary>
- public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
+ public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(
+ @"https?://(?:[^.]+\.)?(?:
favstar\.fm/users/[a-zA-Z0-9_]+/status/ # Favstar
| favstar\.fm/t/ # Favstar (short)
| aclog\.koba789\.com/i/ # aclog
| frtrt\.net/solo_status\.php\?status= # RtRT
-)(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+)(?<StatusId>[0-9]+)",
+ RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
/// <summary>
/// DM送信かどうかを判定する正規表現
delegate void GetIconImageDelegate(PostClass post);
private readonly object LockObj = new object();
private ISet<long> followerId = new HashSet<long>();
- private long[] noRTId = new long[0];
+ private long[] noRTId = Array.Empty<long>();
- //プロパティからアクセスされる共通情報
- private List<string> _hashList = new List<string>();
+ // プロパティからアクセスされる共通情報
+ private readonly List<string> _hashList = new List<string>();
- //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
- private long minDirectmessage = long.MaxValue;
- private long minDirectmessageSent = long.MaxValue;
+ private string? nextCursorDirectMessage = null;
private long previousStatusId = -1L;
- //private FavoriteQueue favQueue;
-
- //private List<PostClass> _deletemessages = new List<PostClass>();
-
- public Twitter() : this(new TwitterApi())
- {
- }
-
public Twitter(TwitterApi api)
{
this.Api = api;
this.ResetApiStatus();
}
- [Obsolete]
public void VerifyCredentials()
{
try
public void Initialize(string token, string tokenSecret, string username, long userId)
{
- //OAuth認証
- if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
+ // OAuth認証
+ if (MyCommon.IsNullOrEmpty(token) || MyCommon.IsNullOrEmpty(tokenSecret) || MyCommon.IsNullOrEmpty(username))
{
Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
}
this.ResetApiStatus();
this.Api.Initialize(token, tokenSecret, userId, username);
- if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream();
}
internal static string PreProcessUrl(string orgData)
{
int posl1;
var posl2 = 0;
- //var IDNConveter = new IdnMapping();
var href = "<a href=\"";
while (true)
{
if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
{
- var urlStr = "";
// IDN展開
posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
posl1 += href.Length;
posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
- urlStr = orgData.Substring(posl1, posl2 - posl1);
+ var urlStr = orgData.Substring(posl1, posl2 - posl1);
if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
&& !urlStr.StartsWith("https://", StringComparison.Ordinal)
return orgData;
}
- public async Task PostStatus(PostStatusParams param)
+ public async Task<PostClass?> PostStatus(PostStatusParams param)
{
this.CheckAccountState();
await this.SendDirectMessage(param.Text, mediaId)
.ConfigureAwait(false);
- return;
+ return null;
}
- var response = await this.Api.StatusesUpdate(param.Text, param.InReplyToStatusId, param.MediaIds,
- param.AutoPopulateReplyMetadata, param.ExcludeReplyUserIds, param.AttachmentUrl)
+ var response = await this.Api.StatusesUpdate(
+ param.Text,
+ param.InReplyToStatusId,
+ param.MediaIds,
+ param.AutoPopulateReplyMetadata,
+ param.ExcludeReplyUserIds,
+ param.AttachmentUrl
+ )
.ConfigureAwait(false);
var status = await response.LoadJsonAsync()
throw new WebApiException("OK:Delaying?");
this.previousStatusId = status.Id;
+
+ // 投稿したものを返す
+ var post = this.CreatePostsFromStatusData(status);
+ if (this.ReadOwnPost) post.IsRead = true;
+ return post;
}
- public async Task<long> UploadMedia(IMediaItem item, string mediaCategory = null)
+ public async Task<long> UploadMedia(IMediaItem item, string? mediaCategory = null)
{
this.CheckAccountState();
- string mediaType;
-
- switch (item.Extension)
+ var mediaType = item.Extension switch
{
- case ".png":
- mediaType = "image/png";
- break;
- case ".jpg":
- case ".jpeg":
- mediaType = "image/jpeg";
- break;
- case ".gif":
- mediaType = "image/gif";
- break;
- default:
- mediaType = "application/octet-stream";
- break;
- }
+ ".png" => "image/png",
+ ".jpg" => "image/jpeg",
+ ".jpeg" => "image/jpeg",
+ ".gif" => "image/gif",
+ _ => "application/octet-stream",
+ };
var initResponse = await this.Api.MediaUploadInit(item.Size, mediaType, mediaCategory)
.ConfigureAwait(false);
var recipient = await this.Api.UsersShow(recipientName)
.ConfigureAwait(false);
- await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
+ var response = await this.Api.DirectMessagesEventsNew(recipient.Id, body, mediaId)
+ .ConfigureAwait(false);
+
+ var messageEventSingle = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
+
+ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true)
.ConfigureAwait(false);
}
- public async Task PostRetweet(long id, bool read)
+ public async Task<PostClass?> PostRetweet(long id, bool read)
{
this.CheckAccountState();
- //データ部分の生成
+ // データ部分の生成
var post = TabInformations.GetInstance()[id];
if (post == null)
throw new WebApiException("Err:Target isn't found.");
- var target = post.RetweetedId ?? id; //再RTの場合は元発言をRT
+ var target = post.RetweetedId ?? id; // 再RTの場合は元発言をRT
var response = await this.Api.StatusesRetweet(target)
.ConfigureAwait(false);
var status = await response.LoadJsonAsync()
.ConfigureAwait(false);
- //二重取得回避
- lock (LockObj)
+ // 二重取得回避
+ lock (this.LockObj)
{
if (TabInformations.GetInstance().ContainsKey(status.Id))
- return;
+ return null;
}
- //Retweet判定
+ // Retweet判定
if (status.RetweetedStatus == null)
throw new WebApiException("Invalid Json!");
- //ReTweetしたものをTLに追加
- post = CreatePostsFromStatusData(status);
-
- //ユーザー情報
+ // Retweetしたものを返す
+ post = this.CreatePostsFromStatusData(status);
+
+ // ユーザー情報
post.IsMe = true;
post.IsRead = read;
if (this.ReadOwnPost) post.IsRead = true;
post.IsDm = false;
- TabInformations.GetInstance().AddPost(post);
+ return post;
}
public string Username
this.FollowersCount = self.FollowersCount;
this.FriendsCount = self.FriendsCount;
this.StatusesCount = self.StatusesCount;
- this.Location = self.Location;
- this.Bio = self.Description;
+ this.Location = self.Location ?? "";
+ this.Bio = self.Description ?? "";
}
/// <summary>
{
// 参照: REST APIs - 各endpointのcountパラメータ
// https://dev.twitter.com/rest/public
- switch (type)
- {
- case MyCommon.WORKERTYPE.Timeline:
- case MyCommon.WORKERTYPE.Reply:
- case MyCommon.WORKERTYPE.UserTimeline:
- case MyCommon.WORKERTYPE.Favorites:
- case MyCommon.WORKERTYPE.DirectMessegeRcv:
- case MyCommon.WORKERTYPE.DirectMessegeSnt:
- case MyCommon.WORKERTYPE.List: // 不明
- return 200;
-
- case MyCommon.WORKERTYPE.PublicSearch:
- return 100;
-
- default:
- throw new InvalidOperationException("Invalid type: " + type);
- }
+ return type switch
+ {
+ MyCommon.WORKERTYPE.Timeline => 200,
+ MyCommon.WORKERTYPE.Reply => 200,
+ MyCommon.WORKERTYPE.UserTimeline => 200,
+ MyCommon.WORKERTYPE.Favorites => 200,
+ MyCommon.WORKERTYPE.List => 200, // 不明
+ MyCommon.WORKERTYPE.PublicSearch => 100,
+ _ => throw new InvalidOperationException("Invalid type: " + type),
+ };
}
/// <summary>
/// </summary>
public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
{
- if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
- type == MyCommon.WORKERTYPE.DirectMessegeSnt)
- {
- return 20;
- }
-
if (SettingManager.Common.UseAdditionalCount)
{
switch (type)
var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
TwitterStatus[] statuses;
- if (string.IsNullOrEmpty(userName))
+ if (MyCommon.IsNullOrEmpty(userName))
{
var target = tab.ScreenName;
- if (string.IsNullOrEmpty(target)) return;
+ if (MyCommon.IsNullOrEmpty(target)) return;
userName = target;
statuses = await this.Api.StatusesUserTimeline(userName, count)
.ConfigureAwait(false);
}
}
- var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
+ var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
if (minimumId != null)
tab.OldestId = minimumId.Value;
var status = await this.Api.StatusesShow(id)
.ConfigureAwait(false);
- var item = CreatePostsFromStatusData(status);
+ var item = this.CreatePostsFromStatusData(status);
item.IsRead = read;
if (item.IsMe && !read && this.ReadOwnPost) item.IsRead = true;
var post = await this.GetStatusApi(read, id)
.ConfigureAwait(false);
- //非同期アイコン取得&StatusDictionaryに追加
+ // 非同期アイコン取得&StatusDictionaryに追加
if (tab != null && tab.IsInnerStorageTabType)
tab.AddPostQueue(post);
else
post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
- //Id
+ // Id
post.RetweetedId = retweeted.Id;
- //本文
+ // 本文
post.TextFromApi = retweeted.FullText;
entities = retweeted.MergedEntities;
sourceHtml = retweeted.Source;
- //Reply先
+ // Reply先
post.InReplyToStatusId = retweeted.InReplyToStatusId;
post.InReplyToUser = retweeted.InReplyToScreenName;
post.InReplyToUserId = status.InReplyToUserId;
}
else
{
- //幻覚fav対策
- var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+ // 幻覚fav対策
+ var tc = TabInformations.GetInstance().FavoriteTab;
post.IsFav = tc.Contains(retweeted.Id);
}
if (retweeted.Coordinates != null)
post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
- //以下、ユーザー情報
+ // 以下、ユーザー情報
var user = retweeted.User;
if (user != null)
{
post.Nickname = "Unknown User";
}
- //Retweetした人
+ // Retweetした人
if (status.User != null)
{
post.RetweetedBy = status.User.ScreenName;
else
{
post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
- //本文
+ // 本文
post.TextFromApi = status.FullText;
entities = status.MergedEntities;
sourceHtml = status.Source;
}
else
{
- //幻覚fav対策
- var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
- post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
+ // 幻覚fav対策
+ var tc = TabInformations.GetInstance().FavoriteTab;
+ post.IsFav = tc.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav;
}
if (status.Coordinates != null)
post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
- //以下、ユーザー情報
+ // 以下、ユーザー情報
var user = status.User;
if (user != null)
{
post.Nickname = "Unknown User";
}
}
- //HTMLに整形
- string textFromApi = post.TextFromApi;
+ // HTMLに整形
+ var textFromApi = post.TextFromApi;
var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink;
post.ImageUrl = string.Intern(post.ImageUrl);
post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
- //Source整形
+ // Source整形
var (sourceText, sourceUri) = ParseSource(sourceHtml);
post.Source = string.Intern(sourceText);
post.SourceUri = sourceUri;
- post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.Item1 == this.UserId);
+ post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == this.UserId);
post.IsExcludeReply = false;
if (post.IsMe)
}
else
{
- if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
+ if (this.followerId.Count > 0) post.IsOwl = !this.followerId.Contains(post.UserId);
}
post.IsDm = false;
/// <summary>
/// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
/// </summary>
- public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities, TwitterQuotedStatusPermalink quotedStatusLink)
+ public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity>? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
{
+ entities ??= Enumerable.Empty<TwitterEntity>();
+
var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
if (quotedStatusLink != null)
- urls = urls.Concat(new[] { quotedStatusLink.Expanded });
+ urls = urls.Append(quotedStatusLink.Expanded);
return GetQuoteTweetStatusIds(urls);
}
}
}
- private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel tab, bool read)
+ private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read)
{
long? minimumId = null;
if (minimumId == null || minimumId.Value > status.Id)
minimumId = status.Id;
- //二重取得回避
- lock (LockObj)
+ // 二重取得回避
+ lock (this.LockObj)
{
if (tab == null)
{
}
}
- //RT禁止ユーザーによるもの
+ // RT禁止ユーザーによるもの
if (gType != MyCommon.WORKERTYPE.UserTimeline &&
status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue;
- var post = CreatePostsFromStatusData(status);
+ var post = this.CreatePostsFromStatusData(status);
post.IsRead = read;
if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
minimumId = status.Id;
if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id;
- //二重取得回避
- lock (LockObj)
+ // 二重取得回避
+ lock (this.LockObj)
{
if (tab.Contains(status.Id)) continue;
}
- var post = CreatePostsFromStatusData(status);
+ var post = this.CreatePostsFromStatusData(status);
post.IsRead = read;
if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true;
private long? CreateFavoritePostsFromJson(TwitterStatus[] items, bool read)
{
- var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+ var favTab = TabInformations.GetInstance().FavoriteTab;
long? minimumId = null;
foreach (var status in items)
if (minimumId == null || minimumId.Value > status.Id)
minimumId = status.Id;
- //二重取得回避
- lock (LockObj)
+ // 二重取得回避
+ lock (this.LockObj)
{
if (favTab.Contains(status.Id)) continue;
}
- var post = CreatePostsFromStatusData(status, true);
+ var post = this.CreatePostsFromStatusData(status, true);
post.IsRead = read;
.ConfigureAwait(false);
}
- var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
+ var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
if (minimumId != null)
tab.OldestId = minimumId.Value;
/// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
/// </summary>
/// <returns>posts の中から検索されたリプライチェインの末端</returns>
- internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
+ internal static PostClass FindTopOfReplyChain(IDictionary<long, PostClass> posts, long startStatusId)
{
if (!posts.ContainsKey(startStatusId))
throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab)
{
var targetPost = tab.TargetPost;
- var relPosts = new Dictionary<Int64, PostClass>();
+ var relPosts = new Dictionary<long, PostClass>();
if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null)
{
- //検索結果対応
+ // 検索結果対応
var p = TabInformations.GetInstance()[targetPost.StatusId];
if (p != null && p.InReplyToStatusId != null)
{
}
relPosts.Add(targetPost.StatusId, targetPost);
- Exception lastException = null;
+ Exception? lastException = null;
// in_reply_to_status_id を使用してリプライチェインを辿る
var nextPost = FindTopOfReplyChain(relPosts, targetPost.StatusId);
nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
}
- //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
+ // MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
var text = targetPost.Text;
var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
.Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
foreach (var _match in ma)
{
- if (Int64.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
+ if (long.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
{
if (relPosts.ContainsKey(_statusId))
continue;
tab.OldestId = minimumId.Value;
}
- private void CreateDirectMessagesFromJson(TwitterDirectMessage[] item, MyCommon.WORKERTYPE gType, bool read)
- {
- foreach (var message in item)
- {
- var post = new PostClass();
- try
- {
- post.StatusId = message.Id;
- if (gType != MyCommon.WORKERTYPE.UserStream)
- {
- if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
- {
- if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
- }
- else
- {
- if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
- }
- }
-
- //二重取得回避
- lock (LockObj)
- {
- if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
- }
- //sender_id
- //recipient_id
- post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
- //本文
- var textFromApi = message.Text;
- //HTMLに整形
- post.Text = CreateHtmlAnchor(textFromApi, message.Entities, quotedStatusLink: null);
- post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities, quotedStatusLink: null);
- post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
- post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
- post.AccessibleText = CreateAccessibleText(textFromApi, message.Entities, quotedStatus: null, quotedStatusLink: null);
- post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
- post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
- post.IsFav = false;
-
- this.ExtractEntities(message.Entities, post.ReplyToList, post.Media);
-
- post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities, quotedStatusLink: null)
- .Distinct().ToArray();
-
- post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
- .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
- .ToArray();
-
- //以下、ユーザー情報
- TwitterUser user;
- if (gType == MyCommon.WORKERTYPE.UserStream)
- {
- if (this.Api.CurrentUserId == message.Recipient.Id)
- {
- user = message.Sender;
- post.IsMe = false;
- post.IsOwl = true;
- }
- else
- {
- user = message.Recipient;
- post.IsMe = true;
- post.IsOwl = false;
- }
- }
- else
- {
- if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
- {
- user = message.Sender;
- post.IsMe = false;
- post.IsOwl = true;
- }
- else
- {
- user = message.Recipient;
- post.IsMe = true;
- post.IsOwl = false;
- }
- }
-
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
-
- // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
- if (post.Text == post.TextFromApi)
- post.Text = post.TextFromApi;
- if (post.AccessibleText == post.TextFromApi)
- post.AccessibleText = post.TextFromApi;
-
- // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
- post.ScreenName = string.Intern(post.ScreenName);
- post.Nickname = string.Intern(post.Nickname);
- post.ImageUrl = string.Intern(post.ImageUrl);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
- MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
- continue;
- }
-
- post.IsRead = read;
- if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
- post.IsReply = false;
- post.IsExcludeReply = false;
- post.IsDm = true;
-
- var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
- dmTab.AddPostQueue(post);
- }
- }
-
- public async Task GetDirectMessageApi(bool read, MyCommon.WORKERTYPE gType, bool more)
+ public async Task GetDirectMessageEvents(bool read, bool backward)
{
this.CheckAccountState();
this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
- var count = GetApiResultCount(gType, more, false);
+ var count = 50;
- TwitterDirectMessage[] messages;
- if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
+ TwitterMessageEventList eventList;
+ if (backward)
{
- if (more)
- {
- messages = await this.Api.DirectMessagesRecv(count, maxId: this.minDirectmessage)
- .ConfigureAwait(false);
- }
- else
- {
- messages = await this.Api.DirectMessagesRecv(count)
- .ConfigureAwait(false);
- }
+ eventList = await this.Api.DirectMessagesEventsList(count, this.nextCursorDirectMessage)
+ .ConfigureAwait(false);
}
else
{
- if (more)
- {
- messages = await this.Api.DirectMessagesSent(count, maxId: this.minDirectmessageSent)
- .ConfigureAwait(false);
- }
- else
- {
- messages = await this.Api.DirectMessagesSent(count)
- .ConfigureAwait(false);
- }
+ eventList = await this.Api.DirectMessagesEventsList(count)
+ .ConfigureAwait(false);
}
- CreateDirectMessagesFromJson(messages, gType, read);
+ this.nextCursorDirectMessage = eventList.NextCursor;
+
+ await this.CreateDirectMessagesEventFromJson(eventList, read)
+ .ConfigureAwait(false);
}
- public async Task GetDirectMessageEvents(bool read)
+ private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventSingle eventSingle, bool read)
{
- this.CheckAccountState();
- this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
-
- var count = 50;
- var eventLists = new List<TwitterMessageEventList>();
-
- string cursor = null;
- do
+ var eventList = new TwitterMessageEventList
{
- var eventList = await this.Api.DirectMessagesEventsList(count, cursor)
- .ConfigureAwait(false);
+ Apps = new Dictionary<string, TwitterMessageEventList.App>(),
+ Events = new[] { eventSingle.Event },
+ };
- eventLists.Add(eventList);
- cursor = eventList.NextCursor;
- }
- while (cursor != null);
+ await this.CreateDirectMessagesEventFromJson(eventList, read)
+ .ConfigureAwait(false);
+ }
+
+ private async Task CreateDirectMessagesEventFromJson(TwitterMessageEventList eventList, bool read)
+ {
+ var events = eventList.Events
+ .Where(x => x.Type == "message_create")
+ .ToArray();
- var events = eventLists.SelectMany(x => x.Events).ToArray();
if (events.Length == 0)
return;
var users = (await this.Api.UsersLookup(userIds).ConfigureAwait(false))
.ToDictionary(x => x.IdStr);
- var apps = eventLists
- .Where(x => x.Apps != null)
- .SelectMany(x => x.Apps)
- .ToLookup(x => x.Key)
- .ToDictionary(x => x.Key, x => x.First().Value);
+ var apps = eventList.Apps ?? new Dictionary<string, TwitterMessageEventList.App>();
this.CreateDirectMessagesEventFromJson(events, users, apps, read);
}
- private void CreateDirectMessagesEventFromJson(IEnumerable<TwitterMessageEvent> events, IReadOnlyDictionary<string, TwitterUser> users,
- IReadOnlyDictionary<string, TwitterMessageEventList.App> apps, bool read)
+ private void CreateDirectMessagesEventFromJson(
+ IEnumerable<TwitterMessageEvent> events,
+ IReadOnlyDictionary<string, TwitterUser> users,
+ IReadOnlyDictionary<string, TwitterMessageEventList.App> apps,
+ bool read)
{
foreach (var eventItem in events)
{
var post = new PostClass();
- try
- {
- post.StatusId = long.Parse(eventItem.Id);
+ post.StatusId = long.Parse(eventItem.Id);
- var timestamp = long.Parse(eventItem.CreatedTimestamp);
- post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond);
- //本文
- var textFromApi = eventItem.MessageCreate.MessageData.Text;
+ var timestamp = long.Parse(eventItem.CreatedTimestamp);
+ post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond);
+ // 本文
+ var textFromApi = eventItem.MessageCreate.MessageData.Text;
- var entities = eventItem.MessageCreate.MessageData.Entities;
- var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media;
+ var entities = eventItem.MessageCreate.MessageData.Entities;
+ var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media;
- if (mediaEntity != null)
- entities.Media = new[] { mediaEntity };
+ if (mediaEntity != null)
+ entities.Media = new[] { mediaEntity };
- //HTMLに整形
- post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null);
- post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null);
- post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
- post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
- post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null);
- post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
- post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
- post.IsFav = false;
+ // HTMLに整形
+ post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null);
+ post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null);
+ post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
+ post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+ post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null);
+ post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+ post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
+ post.IsFav = false;
- this.ExtractEntities(entities, post.ReplyToList, post.Media);
+ this.ExtractEntities(entities, post.ReplyToList, post.Media);
- post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null)
- .Distinct().ToArray();
+ post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null)
+ .Distinct().ToArray();
- post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
- .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
- .ToArray();
+ post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
+ .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
+ .ToArray();
- //以下、ユーザー情報
- TwitterUser user;
- if (eventItem.MessageCreate.SenderId != this.Api.CurrentUserId.ToString(CultureInfo.InvariantCulture))
- {
- user = users[eventItem.MessageCreate.SenderId];
- post.IsMe = false;
- post.IsOwl = true;
- }
- else
- {
- user = users[eventItem.MessageCreate.Target.RecipientId];
- post.IsMe = true;
- post.IsOwl = false;
- }
-
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
+ // 以下、ユーザー情報
+ string userId;
+ if (eventItem.MessageCreate.SenderId != this.Api.CurrentUserId.ToString(CultureInfo.InvariantCulture))
+ {
+ userId = eventItem.MessageCreate.SenderId;
+ post.IsMe = false;
+ post.IsOwl = true;
+ }
+ else
+ {
+ userId = eventItem.MessageCreate.Target.RecipientId;
+ post.IsMe = true;
+ post.IsOwl = false;
+ }
- // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
- if (post.Text == post.TextFromApi)
- post.Text = post.TextFromApi;
- if (post.AccessibleText == post.TextFromApi)
- post.AccessibleText = post.TextFromApi;
+ if (!users.TryGetValue(userId, out var user))
+ continue;
- // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
- post.ScreenName = string.Intern(post.ScreenName);
- post.Nickname = string.Intern(post.Nickname);
- post.ImageUrl = string.Intern(post.ImageUrl);
+ post.UserId = user.Id;
+ post.ScreenName = user.ScreenName;
+ post.Nickname = user.Name.Trim();
+ post.ImageUrl = user.ProfileImageUrlHttps;
+ post.IsProtect = user.Protected;
+
+ // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+ if (post.Text == post.TextFromApi)
+ post.Text = post.TextFromApi;
+ if (post.AccessibleText == post.TextFromApi)
+ post.AccessibleText = post.TextFromApi;
+
+ // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+ post.ScreenName = string.Intern(post.ScreenName);
+ post.Nickname = string.Intern(post.Nickname);
+ post.ImageUrl = string.Intern(post.ImageUrl);
+
+ var appId = eventItem.MessageCreate.SourceAppId;
+ if (appId != null && apps.TryGetValue(appId, out var app))
+ {
+ post.Source = string.Intern(app.Name);
- var appId = eventItem.MessageCreate.SourceAppId;
- if (appId != null)
+ try
{
- var app = apps[appId];
- post.Source = string.Intern(app.Name);
-
- try
- {
- post.SourceUri = new Uri(SourceUriBase, app.Url);
- }
- catch (UriFormatException) { }
+ post.SourceUri = new Uri(SourceUriBase, app.Url);
}
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
- MessageBox.Show("Parse Error(CreateDirectMessagesEventFromJson)");
- continue;
+ catch (UriFormatException) { }
}
post.IsRead = read;
post.IsExcludeReply = false;
post.IsDm = true;
- var dmTab = TabInformations.GetInstance().GetTabByType<DirectMessagesTabModel>();
+ var dmTab = TabInformations.GetInstance().DirectMessageTab;
dmTab.AddPostQueue(post);
}
}
tab.OldestId = minimumId.Value;
}
- private string ReplaceTextFromApi(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
+ private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
{
if (entities != null)
{
{
foreach (var m in entities.Urls)
{
- if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
+ if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
}
}
if (entities.Media != null)
{
foreach (var m in entities.Media)
{
- if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
+ if (!MyCommon.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
}
}
}
return text;
}
- internal static string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quotedStatus, TwitterQuotedStatusPermalink quotedStatusLink)
+ internal static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink)
{
if (entities == null)
return text;
}
}
- if (!string.IsNullOrEmpty(entity.DisplayUrl))
+ if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
text = text.Replace(entity.Url, entity.DisplayUrl);
}
}
{
foreach (var entity in entities.Media)
{
- if (!string.IsNullOrEmpty(entity.AltText))
+ if (!MyCommon.IsNullOrEmpty(entity.AltText))
{
text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
}
- else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+ else if (!MyCommon.IsNullOrEmpty(entity.DisplayUrl))
{
text = text.Replace(entity.Url, entity.DisplayUrl);
}
}
}
- if (quotedStatusLink != null)
+ if (quotedStatus != null && quotedStatusLink != null)
{
var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText);
if (MyCommon._endingFlag) return;
var cursor = -1L;
- var newFollowerIds = new HashSet<long>();
+ var newFollowerIds = Enumerable.Empty<long>();
do
{
var ret = await this.Api.FollowersIds(cursor)
if (ret.Ids == null)
throw new WebApiException("ret.ids == null");
- newFollowerIds.UnionWith(ret.Ids);
+ newFollowerIds = newFollowerIds.Concat(ret.Ids);
cursor = ret.NextCursor;
} while (cursor != 0);
- this.followerId = newFollowerIds;
+ this.followerId = newFollowerIds.ToHashSet();
TabInformations.GetInstance().RefreshOwl(this.followerId);
this.GetFollowersSuccess = true;
return true;
}
catch (TwitterApiException ex)
- when (ex.ErrorResponse.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
+ when (ex.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
{
return false;
}
}
- private void ExtractEntities(TwitterEntities entities, List<Tuple<long, string>> AtList, List<MediaInfo> media)
+ private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> AtList, List<MediaInfo> media)
{
if (entities != null)
{
{
foreach (var ent in entities.UserMentions)
{
- AtList.Add(Tuple.Create(ent.Id, ent.ScreenName));
+ AtList.Add((ent.Id, ent.ScreenName));
}
}
if (entities.Media != null)
if (ent.VideoInfo != null &&
ent.Type == "animated_gif" || ent.Type == "video")
{
- //var videoUrl = ent.VideoInfo.Variants
- // .Where(v => v.ContentType == "video/mp4")
- // .OrderByDescending(v => v.Bitrate)
- // .Select(v => v.Url).FirstOrDefault();
media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
}
else
}
}
- internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
+ internal static string CreateHtmlAnchor(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink)
{
+ var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(text));
+
// PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
- text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
+ text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true);
- 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>");
- text = PreProcessUrl(text); //IDN置換
+ text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"https://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
+ text = PreProcessUrl(text); // IDN置換
if (quotedStatusLink != null)
{
/// <summary>
/// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
/// </summary>
- internal static (string SourceText, Uri SourceUri) ParseSource(string sourceHtml)
+ internal static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml)
{
- if (string.IsNullOrEmpty(sourceHtml))
+ if (MyCommon.IsNullOrEmpty(sourceHtml))
return ("", null);
string sourceText;
- Uri sourceUri;
+ Uri? sourceUri;
// sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
return (sourceText, sourceUri);
}
- public async Task<TwitterApiStatus> GetInfoApi()
+ public async Task<TwitterApiStatus?> GetInfoApi()
{
if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
if (MyCommon._endingFlag) return;
var cursor = -1L;
- var newBlockIds = new HashSet<long>();
+ var newBlockIds = Enumerable.Empty<long>();
do
{
var ret = await this.Api.BlocksIds(cursor)
.ConfigureAwait(false);
- newBlockIds.UnionWith(ret.Ids);
+ newBlockIds = newBlockIds.Concat(ret.Ids);
cursor = ret.NextCursor;
} while (cursor != 0);
- newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
+ var blockIdsSet = newBlockIds.ToHashSet();
+ blockIdsSet.Remove(this.UserId); // 元のソースにあったので一応残しておく
- TabInformations.GetInstance().BlockIds = newBlockIds;
+ TabInformations.GetInstance().BlockIds = blockIdsSet;
}
/// <summary>
var ids = await TwitterIds.GetAllItemsAsync(x => this.Api.MutesUsersIds(x))
.ConfigureAwait(false);
- TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
+ TabInformations.GetInstance().MuteUserIds = ids.ToHashSet();
}
public string[] GetHashList()
{
string[] hashArray;
- lock (LockObj)
+ lock (this.LockObj)
{
- hashArray = _hashList.ToArray();
- _hashList.Clear();
+ hashArray = this._hashList.ToArray();
+ this._hashList.Clear();
}
return hashArray;
}
var config = this.TextConfiguration;
var totalWeight = 0;
+ int GetWeightFromCodepoint(int codepoint)
+ {
+ foreach (var weightRange in config.Ranges)
+ {
+ if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
+ return weightRange.Weight;
+ }
+
+ return config.DefaultWeight;
+ }
+
var urls = TweetExtractor.ExtractUrlEntities(postText).ToArray();
+ var emojis = config.EmojiParsingEnabled
+ ? TweetExtractor.ExtractEmojiEntities(postText).ToArray()
+ : Array.Empty<TwitterEntityEmoji>();
- var pos = 0;
- while (pos < postText.Length)
+ var codepoints = postText.ToCodepoints().ToArray();
+ var index = 0;
+ while (index < codepoints.Length)
{
- var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == pos);
+ var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == index);
if (urlEntity != null)
{
totalWeight += config.TransformedURLLength * config.Scale;
-
- var urlLength = urlEntity.Indices[1] - urlEntity.Indices[0];
- pos += urlLength;
-
+ index = urlEntity.Indices[1];
continue;
}
- var codepoint = postText.GetCodepointAtSafe(pos);
- var weight = config.DefaultWeight;
-
- foreach (var weightRange in config.Ranges)
+ var emojiEntity = emojis.FirstOrDefault(x => x.Indices[0] == index);
+ if (emojiEntity != null)
{
- if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
- {
- weight = weightRange.Weight;
- break;
- }
+ totalWeight += GetWeightFromCodepoint(codepoints[index]);
+ index = emojiEntity.Indices[1];
+ continue;
}
- totalWeight += weight;
+ var codepoint = codepoints[index];
+ totalWeight += GetWeightFromCodepoint(codepoint);
- var isSurrogatePair = codepoint > 0xffff;
- if (isSurrogatePair)
- pos += 2; // サロゲートペアの場合は2文字分進める
- else
- pos++;
+ index++;
}
var remainWeight = config.MaxWeightedTweetLength * config.Scale - totalWeight;
return remainWeight / config.Scale;
}
+ public bool IsDisposed { get; private set; } = false;
-#region "UserStream"
- public string TrackWord { get; set; } = "";
- public bool AllAtReply { get; set; } = false;
-
- public event EventHandler NewPostFromStream;
- public event EventHandler UserStreamStarted;
- public event EventHandler UserStreamStopped;
- public event EventHandler<PostDeletedEventArgs> PostDeleted;
- public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
- private DateTimeUtc _lastUserstreamDataReceived;
- private StreamAutoConnector userStreamConnector;
-
- public class FormattedEvent
- {
- public MyCommon.EVENTTYPE Eventtype { get; set; }
- public DateTimeUtc CreatedAt { get; set; }
- public string Event { get; set; }
- public string Username { get; set; }
- public string Target { get; set; }
- public Int64 Id { get; set; }
- public bool IsMe { get; set; }
- }
-
- public List<FormattedEvent> StoredEvent { get; } = new List<FormattedEvent>();
-
- private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
- {
- ["favorite"] = MyCommon.EVENTTYPE.Favorite,
- ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
- ["follow"] = MyCommon.EVENTTYPE.Follow,
- ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
- ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
- ["block"] = MyCommon.EVENTTYPE.Block,
- ["unblock"] = MyCommon.EVENTTYPE.Unblock,
- ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
- ["deleted"] = MyCommon.EVENTTYPE.Deleted,
- ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
- ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
- ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
- ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
- ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
- ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
- ["mute"] = MyCommon.EVENTTYPE.Mute,
- ["unmute"] = MyCommon.EVENTTYPE.Unmute,
- ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
- };
-
- public bool IsUserstreamDataReceived
- => (DateTimeUtc.Now - this._lastUserstreamDataReceived).TotalSeconds < 31;
-
- private void userStream_MessageReceived(ITwitterStreamMessage message)
- {
- this._lastUserstreamDataReceived = DateTimeUtc.Now;
-
- switch (message)
- {
- case StreamMessageStatus statusMessage:
- var status = statusMessage.Status.Normalize();
-
- if (status.RetweetedStatus is TwitterStatus retweetedStatus)
- {
- var sourceUserId = statusMessage.Status.User.Id;
- var targetUserId = retweetedStatus.User.Id;
-
- // 自分に関係しないリツイートの場合は無視する
- var selfUserId = this.UserId;
- if (sourceUserId == selfUserId || targetUserId == selfUserId)
- {
- // 公式 RT をイベントとしても扱う
- var evt = this.CreateEventFromRetweet(status);
- this.StoredEvent.Insert(0, evt);
- this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
- }
- // 従来通り公式 RT の表示も行うため break しない
- }
-
- this.CreatePostsFromJson(new[] { status }, MyCommon.WORKERTYPE.UserStream, null, false);
- this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
- break;
-
- case StreamMessageDirectMessage dmMessage:
- this.CreateDirectMessagesFromJson(new[] { dmMessage.DirectMessage }, MyCommon.WORKERTYPE.UserStream, false);
- this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
- break;
-
- case StreamMessageDelete deleteMessage:
- var deletedId = deleteMessage.Status?.Id ?? deleteMessage.DirectMessage?.Id;
- if (deletedId == null)
- break;
-
- this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(deletedId.Value));
-
- foreach (var index in MyCommon.CountDown(this.StoredEvent.Count - 1, 0))
- {
- var evt = this.StoredEvent[index];
- if (evt.Id == deletedId.Value && (evt.Event == "favorite" || evt.Event == "unfavorite"))
- {
- this.StoredEvent.RemoveAt(index);
- }
- }
- break;
-
- case StreamMessageEvent eventMessage:
- this.CreateEventFromJson(eventMessage);
- break;
-
- case StreamMessageScrubGeo scrubGeoMessage:
- TabInformations.GetInstance().ScrubGeoReserve(scrubGeoMessage.UserId, scrubGeoMessage.UpToStatusId);
- break;
-
- default:
- break;
- }
- }
-
- /// <summary>
- /// UserStreamsから受信した公式RTをイベントに変換します
- /// </summary>
- private FormattedEvent CreateEventFromRetweet(TwitterStatus status)
- {
- return new FormattedEvent
- {
- Eventtype = MyCommon.EVENTTYPE.Retweet,
- Event = "retweet",
- CreatedAt = MyCommon.DateTimeParse(status.CreatedAt),
- IsMe = status.User.Id == this.UserId,
- Username = status.User.ScreenName,
- Target = string.Format("@{0}:{1}", new[]
- {
- status.RetweetedStatus.User.ScreenName,
- WebUtility.HtmlDecode(status.RetweetedStatus.FullText),
- }),
- Id = status.RetweetedStatus.Id,
- };
- }
-
- private void CreateEventFromJson(StreamMessageEvent message)
- {
- var eventData = message.Event;
-
- var evt = new FormattedEvent
- {
- CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt),
- Event = eventData.Event,
- Username = eventData.Source.ScreenName,
- IsMe = eventData.Source.Id == this.UserId,
- Eventtype = eventTable.TryGetValue(eventData.Event, out var eventType) ? eventType : MyCommon.EVENTTYPE.None,
- };
-
- TwitterStreamEvent<TwitterStatusCompat> tweetEvent;
- TwitterStatus tweet;
-
- switch (eventData.Event)
- {
- case "access_revoked":
- case "access_unrevoked":
- case "user_delete":
- case "user_suspend":
- return;
- case "follow":
- if (eventData.Target.Id == this.UserId)
- {
- if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
- }
- else
- {
- return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
- }
- evt.Target = "";
- break;
- case "unfollow":
- evt.Target = "@" + eventData.Target.ScreenName;
- break;
- case "favorited_retweet":
- case "retweeted_retweet":
- return;
- case "favorite":
- case "unfavorite":
- tweetEvent = message.ParseTargetObjectAs<TwitterStatusCompat>();
- tweet = tweetEvent.TargetObject.Normalize();
- evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
- evt.Id = tweet.Id;
-
- if (SettingManager.Common.IsRemoveSameEvent)
- {
- if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
- return;
- }
-
- var tabinfo = TabInformations.GetInstance();
-
- var statusId = tweet.Id;
- if (!tabinfo.Posts.TryGetValue(statusId, out var post))
- break;
-
- if (eventData.Event == "favorite")
- {
- var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
- favTab.AddPostQueue(post);
-
- if (tweetEvent.Source.Id == this.UserId)
- {
- post.IsFav = true;
- }
- else if (tweetEvent.Target.Id == this.UserId)
- {
- post.FavoritedCount++;
-
- if (SettingManager.Common.FavEventUnread)
- tabinfo.SetReadAllTab(post.StatusId, read: false);
- }
- }
- else // unfavorite
- {
- if (tweetEvent.Source.Id == this.UserId)
- {
- post.IsFav = false;
- }
- else if (tweetEvent.Target.Id == this.UserId)
- {
- post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
- }
- }
- break;
- case "quoted_tweet":
- if (evt.IsMe) return;
-
- tweetEvent = message.ParseTargetObjectAs<TwitterStatusCompat>();
- tweet = tweetEvent.TargetObject.Normalize();
- evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
- evt.Id = tweet.Id;
-
- if (SettingManager.Common.IsRemoveSameEvent)
- {
- if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
- return;
- }
- break;
- case "list_member_added":
- case "list_member_removed":
- case "list_created":
- case "list_destroyed":
- case "list_updated":
- case "list_user_subscribed":
- case "list_user_unsubscribed":
- var listEvent = message.ParseTargetObjectAs<TwitterList>();
- evt.Target = listEvent.TargetObject.FullName;
- break;
- case "block":
- if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
- evt.Target = "";
- break;
- case "unblock":
- if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
- evt.Target = "";
- break;
- case "user_update":
- evt.Target = "";
- break;
-
- // Mute / Unmute
- case "mute":
- evt.Target = "@" + eventData.Target.ScreenName;
- if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
- {
- TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
- }
- break;
- case "unmute":
- evt.Target = "@" + eventData.Target.ScreenName;
- if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
- {
- TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
- }
- break;
-
- default:
- MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + message.Json);
- break;
- }
- this.StoredEvent.Insert(0, evt);
-
- this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
- }
-
- private void userStream_Started()
- => this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
-
- private void userStream_Stopped()
- => this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
-
- public bool UserStreamActive
- => this.userStreamConnector != null && this.userStreamConnector.IsStreamActive;
-
- public void StartUserStream()
- {
- var replies = this.AllAtReply ? "all" : null;
- var streamObservable = this.Api.UserStreams(replies, this.TrackWord);
- var newConnector = new StreamAutoConnector(streamObservable);
-
- newConnector.MessageReceived += userStream_MessageReceived;
- newConnector.Started += userStream_Started;
- newConnector.Stopped += userStream_Stopped;
-
- newConnector.Start();
-
- var oldConnector = Interlocked.Exchange(ref this.userStreamConnector, newConnector);
- oldConnector?.Dispose();
- }
-
- public void StopUserStream()
- {
- var oldConnector = Interlocked.Exchange(ref this.userStreamConnector, null);
- oldConnector?.Dispose();
- }
-
- public void ReconnectUserStream()
- {
- if (this.userStreamConnector != null)
- {
- this.StartUserStream();
- }
- }
-
- private class StreamAutoConnector : IDisposable
+ protected virtual void Dispose(bool disposing)
{
- private readonly TwitterStreamObservable streamObservable;
-
- public bool IsStreamActive { get; private set; }
- public bool IsDisposed { get; private set; }
-
- public event Action<ITwitterStreamMessage> MessageReceived;
- public event Action Stopped;
- public event Action Started;
-
- private Task streamTask;
- private CancellationTokenSource streamCts = new CancellationTokenSource();
-
- public StreamAutoConnector(TwitterStreamObservable streamObservable)
- => this.streamObservable = streamObservable;
-
- public void Start()
- {
- var cts = new CancellationTokenSource();
-
- this.streamCts = cts;
- this.streamTask = Task.Run(async () =>
- {
- try
- {
- await this.StreamLoop(cts.Token)
- .ConfigureAwait(false);
- }
- catch (OperationCanceledException) { }
- });
- }
-
- public void Stop()
- {
- this.streamCts?.Cancel();
-
- // streamTask の完了を待たずに IsStreamActive を false にセットする
- this.IsStreamActive = false;
- this.Stopped?.Invoke();
- }
-
- private async Task StreamLoop(CancellationToken cancellationToken)
- {
- TimeSpan sleep = TimeSpan.Zero;
- for (; ; )
- {
- if (sleep != TimeSpan.Zero)
- {
- await Task.Delay(sleep, cancellationToken)
- .ConfigureAwait(false);
- sleep = TimeSpan.Zero;
- }
-
- if (!MyCommon.IsNetworkAvailable())
- {
- sleep = TimeSpan.FromSeconds(30);
- continue;
- }
-
- this.IsStreamActive = true;
- this.Started?.Invoke();
-
- try
- {
- await this.streamObservable.ForEachAsync(
- x => this.MessageReceived?.Invoke(x),
- cancellationToken);
-
- // キャンセルされていないのにストリームが終了した場合
- sleep = TimeSpan.FromSeconds(30);
- }
- catch (TwitterApiException ex) when (ex.StatusCode == HttpStatusCode.Gone)
- {
- // UserStreams停止によるエラーの場合は長めに間隔を開ける
- sleep = TimeSpan.FromMinutes(10);
- }
- catch (TwitterApiException) { sleep = TimeSpan.FromSeconds(30); }
- catch (IOException) { sleep = TimeSpan.FromSeconds(30); }
- catch (OperationCanceledException)
- {
- if (cancellationToken.IsCancellationRequested)
- throw;
-
- // cancellationToken によるキャンセルではない(=タイムアウトエラー)
- sleep = TimeSpan.FromSeconds(30);
- }
- catch (Exception ex)
- {
- MyCommon.ExceptionOut(ex);
- sleep = TimeSpan.FromSeconds(30);
- }
- finally
- {
- this.IsStreamActive = false;
- this.Stopped?.Invoke();
- }
- }
- }
+ if (this.IsDisposed)
+ return;
- public void Dispose()
+ if (disposing)
{
- if (this.IsDisposed)
- return;
-
- this.IsDisposed = true;
-
- this.Stop();
-
- this.Started = null;
- this.Stopped = null;
- this.MessageReceived = null;
+ this.Api.Dispose();
}
- }
-#endregion
-#region "IDisposable Support"
- private bool disposedValue; // 重複する呼び出しを検出するには
-
- // IDisposable
- protected virtual void Dispose(bool disposing)
- {
- if (!this.disposedValue)
- {
- if (disposing)
- {
- this.StopUserStream();
- }
- }
- this.disposedValue = true;
+ this.IsDisposed = true;
}
- //protected Overrides void Finalize()
- //{
- // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
- // Dispose(false)
- // MyBase.Finalize()
- //}
-
- // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
public void Dispose()
{
- // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
- Dispose(true);
+ this.Dispose(true);
GC.SuppressFinalize(this);
}
-#endregion
- }
-
- public class PostDeletedEventArgs : EventArgs
- {
- public long StatusId { get; }
-
- public PostDeletedEventArgs(long statusId)
- => this.StatusId = statusId;
- }
-
- public class UserStreamEventReceivedEventArgs : EventArgs
- {
- public Twitter.FormattedEvent EventData { get; }
-
- public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
- => this.EventData = eventData;
}
}