using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Drawing;
using System.Windows.Forms;
using OpenTween.Api;
+using OpenTween.Api.DataModel;
using OpenTween.Connection;
+using OpenTween.Models;
+using System.Drawing.Imaging;
+using OpenTween.Setting;
namespace OpenTween
{
/// </summary>
public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ public TwitterApi Api { get; }
public TwitterConfiguration Configuration { get; private set; }
+ public TwitterTextConfiguration TextConfiguration { get; private set; }
delegate void GetIconImageDelegate(PostClass post);
private readonly object LockObj = new object();
private List<string> _hashList = new List<string>();
//max_idで古い発言を取得するために保持(lists分は個別タブで管理)
- private long minHomeTimeline = long.MaxValue;
- private long minMentions = long.MaxValue;
private long minDirectmessage = long.MaxValue;
private long minDirectmessageSent = long.MaxValue;
- //private FavoriteQueue favQueue;
+ private long previousStatusId = -1L;
- private HttpTwitter twCon = new HttpTwitter();
+ //private FavoriteQueue favQueue;
//private List<PostClass> _deletemessages = new List<PostClass>();
- public Twitter()
+ public Twitter() : this(new TwitterApi())
+ {
+ }
+
+ public Twitter(TwitterApi api)
{
+ this.Api = api;
this.Configuration = TwitterConfiguration.DefaultConfiguration();
+ this.TextConfiguration = TwitterTextConfiguration.DefaultConfiguration();
}
public TwitterApiAccessLevel AccessLevel
MyCommon.TwitterApiInfo.Reset();
}
- public void Authenticate(string username, string password)
- {
- this.ResetApiStatus();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.AuthUserAndPass(username, password, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
-
- _uname = username.ToLowerInvariant();
- if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
- }
-
- public string StartAuthentication()
- {
- //OAuth PIN Flow
- this.ResetApiStatus();
- try
- {
- string pinPageUrl = null;
- var res = twCon.AuthGetRequestToken(ref pinPageUrl);
- if (!res)
- throw new WebApiException("Err:Failed to access auth server.");
-
- return pinPageUrl;
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:Failed to access auth server.", ex);
- }
- }
-
- public void Authenticate(string pinCode)
- {
- this.ResetApiStatus();
-
- HttpStatusCode res;
- try
- {
- res = twCon.AuthGetAccessToken(pinCode);
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:Failed to access auth acc server.", ex);
- }
-
- this.CheckStatusCode(res, null);
-
- _uname = Username.ToLowerInvariant();
- if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
- }
-
public void ClearAuthInfo()
{
Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
this.ResetApiStatus();
- twCon.ClearAuthInfo();
}
+ [Obsolete]
public void VerifyCredentials()
{
- HttpStatusCode res;
- var content = "";
try
{
- res = twCon.VerifyCredentials(ref content);
+ this.VerifyCredentialsAsync().Wait();
}
- catch (Exception ex)
+ catch (AggregateException ex) when (ex.InnerException is WebApiException)
{
- throw new WebApiException("Err:" + ex.Message, ex);
+ throw new WebApiException(ex.InnerException.Message, ex);
}
+ }
- this.CheckStatusCode(res, content);
-
- try
- {
- var user = TwitterUser.ParseJson(content);
+ public async Task VerifyCredentialsAsync()
+ {
+ var user = await this.Api.AccountVerifyCredentials()
+ .ConfigureAwait(false);
- this.twCon.AuthenticatedUserId = user.Id;
- this.UpdateUserStats(user);
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
+ this.UpdateUserStats(user);
}
public void Initialize(string token, string tokenSecret, string username, long userId)
Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
}
this.ResetApiStatus();
- twCon.Initialize(token, tokenSecret, username, userId);
+ this.Api.Initialize(token, tokenSecret, userId, username);
_uname = username.ToLowerInvariant();
- if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
+ if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream();
}
public string PreProcessUrl(string orgData)
posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
urlStr = orgData.Substring(posl1, posl2 - posl1);
- if (!urlStr.StartsWith("http://") && !urlStr.StartsWith("https://") && !urlStr.StartsWith("ftp://"))
+ if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
+ && !urlStr.StartsWith("https://", StringComparison.Ordinal)
+ && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
{
continue;
}
return orgData;
}
- private string GetPlainText(string orgData)
- {
- return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
- }
-
- // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
-
- private string SanitizeHtml(string orgdata)
- {
- var retdata = orgdata;
-
- retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
- "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
-
- retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
-
- return retdata;
- }
-
- private string AdjustHtml(string orgData)
- {
- var retStr = orgData;
- //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
- //while (m.Success)
- //{
- // lock (LockObj)
- // {
- // _hashList.Add("#" + m.Groups(1).Value);
- // }
- // m = m.NextMatch;
- //}
- retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
- retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
- retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
-
- //半角スペースを置換(Thanks @anis774)
- var ret = false;
- do
- {
- ret = EscapeSpace(ref retStr);
- } while (!ret);
-
- return SanitizeHtml(retStr);
- }
-
- private bool EscapeSpace(ref string html)
- {
- //半角スペースを置換(Thanks @anis774)
- var isTag = false;
- for (int i = 0; i < html.Length; i++)
- {
- if (html[i] == '<')
- {
- isTag = true;
- }
- if (html[i] == '>')
- {
- isTag = false;
- }
-
- if ((!isTag) && (html[i] == ' '))
- {
- html = html.Remove(i, 1);
- html = html.Insert(i, " ");
- return false;
- }
- }
- return true;
- }
-
- private struct PostInfo
- {
- public string CreatedAt;
- public string Id;
- public string Text;
- public string UserId;
- public PostInfo(string Created, string IdStr, string txt, string uid)
- {
- CreatedAt = Created;
- Id = IdStr;
- Text = txt;
- UserId = uid;
- }
- public bool Equals(PostInfo dst)
- {
- if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
- {
- return true;
- }
- else
- {
- return false;
- }
- }
- }
-
- static private PostInfo _prev = new PostInfo("", "", "", "");
- private bool IsPostRestricted(TwitterStatus status)
- {
- var _current = new PostInfo("", "", "", "");
-
- _current.CreatedAt = status.CreatedAt;
- _current.Id = status.IdStr;
- if (status.Text == null)
- {
- _current.Text = "";
- }
- else
- {
- _current.Text = status.Text;
- }
- _current.UserId = status.User.IdStr;
-
- if (_current.Equals(_prev))
- {
- return true;
- }
- _prev.CreatedAt = _current.CreatedAt;
- _prev.Id = _current.Id;
- _prev.Text = _current.Text;
- _prev.UserId = _current.UserId;
-
- return false;
- }
-
- public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
+ public async Task PostStatus(string postStr, long? reply_to, IReadOnlyList<long> mediaIds = null)
{
this.CheckAccountState();
- if (mediaIds == null &&
- Twitter.DMSendTextRegex.IsMatch(postStr))
+ if (Twitter.DMSendTextRegex.IsMatch(postStr))
{
- SendDirectMessage(postStr);
+ await this.SendDirectMessage(postStr)
+ .ConfigureAwait(false);
return;
}
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
- if (res == HttpStatusCode.NotFound)
- return;
-
- this.CheckStatusCode(res, content);
-
- TwitterStatus status;
- try
- {
- status = TwitterStatus.ParseJson(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
-
- this.UpdateUserStats(status.User);
-
- if (IsPostRestricted(status))
- {
- throw new WebApiException("OK:Delaying?");
- }
- }
-
- public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
- if (res == HttpStatusCode.NotFound)
- return;
-
- this.CheckStatusCode(res, content);
+ var response = await this.Api.StatusesUpdate(postStr, reply_to, mediaIds)
+ .ConfigureAwait(false);
- TwitterStatus status;
- try
- {
- status = TwitterStatus.ParseJson(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
+ var status = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
this.UpdateUserStats(status.User);
- if (IsPostRestricted(status))
- {
+ if (status.Id == this.previousStatusId)
throw new WebApiException("OK:Delaying?");
- }
- }
- public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
- {
- this.CheckAccountState();
-
- if (Twitter.DMSendTextRegex.IsMatch(postStr))
- {
- SendDirectMessage(postStr);
- return;
- }
-
- var mediaIds = new List<long>();
-
- foreach (var item in mediaItems)
- {
- var mediaId = UploadMedia(item);
- mediaIds.Add(mediaId);
- }
-
- if (mediaIds.Count == 0)
- throw new WebApiException("Err:Invalid Files!");
-
- PostStatus(postStr, reply_to, mediaIds);
+ this.previousStatusId = status.Id;
}
- public long UploadMedia(IMediaItem item)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.UploadMedia(item, ref content);
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
+ public Task<long> UploadMedia(IMediaItem item)
+ => this.UploadMedia(item, SettingManager.Common.AlphaPNGWorkaround);
- TwitterUploadMediaResult status;
- try
- {
- status = TwitterUploadMediaResult.ParseJson(content);
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
-
- return status.MediaId;
- }
-
- public void SendDirectMessage(string postStr)
+ public async Task<long> UploadMedia(IMediaItem item, bool alphaPNGWorkaround)
{
this.CheckAccountState();
- this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
- var mc = Twitter.DMSendTextRegex.Match(postStr);
+ LazyJson<TwitterUploadMediaResult> response;
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
- }
- catch(Exception ex)
+ using (var origImage = item.CreateImage())
{
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
-
- TwitterDirectMessage status;
- try
- {
- status = TwitterDirectMessage.ParseJson(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
+ if (alphaPNGWorkaround && this.AddAlphaChannelIfNeeded(origImage.Image, out var newImage))
+ {
+ using (var newMediaItem = new MemoryImageMediaItem(newImage))
+ {
+ response = await this.Api.MediaUpload(newMediaItem)
+ .ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ response = await this.Api.MediaUpload(item)
+ .ConfigureAwait(false);
+ }
}
- this.UpdateUserStats(status.Sender);
- }
-
- public void RemoveStatus(long id)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- try
- {
- res = twCon.DestroyStatus(id);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
+ var media = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, null);
+ return media.MediaId;
}
- public void PostRetweet(long id, bool read)
- {
- this.CheckAccountState();
-
- //データ部分の生成
- var target = id;
- var post = TabInformations.GetInstance()[id];
- if (post == null)
- {
- throw new WebApiException("Err:Target isn't found.");
- }
- if (TabInformations.GetInstance()[id].RetweetedId != null)
- {
- target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
- }
+ /// <summary>
+ /// pic.twitter.com アップロード時に JPEG への変換を回避するための加工を行う
+ /// </summary>
+ /// <remarks>
+ /// pic.twitter.com へのアップロード時に、アルファチャンネルを持たない PNG 画像が
+ /// JPEG 形式に変換され画質が低下する問題を回避します。
+ /// PNG 以外の画像や、すでにアルファチャンネルを持つ PNG 画像に対しては何もしません。
+ /// </remarks>
+ /// <returns>加工が行われた場合は true、そうでない場合は false</returns>
+ private bool AddAlphaChannelIfNeeded(Image origImage, out MemoryImage newImage)
+ {
+ newImage = null;
+
+ // PNG 画像以外に対しては何もしない
+ if (origImage.RawFormat.Guid != ImageFormat.Png.Guid)
+ return false;
- HttpStatusCode res;
- var content = "";
- try
+ using (var bitmap = new Bitmap(origImage))
{
- res = twCon.RetweetStatus(target, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
+ // アルファ値が 255 以外のピクセルが含まれていた場合は何もしない
+ foreach (var x in Enumerable.Range(0, bitmap.Width))
+ {
+ foreach (var y in Enumerable.Range(0, bitmap.Height))
+ {
+ if (bitmap.GetPixel(x, y).A != 255)
+ return false;
+ }
+ }
- TwitterStatus status;
- try
- {
- status = TwitterStatus.ParseJson(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
+ // 左上の 1px だけアルファ値を 254 にする
+ var pixel = bitmap.GetPixel(0, 0);
+ var newPixel = Color.FromArgb(pixel.A - 1, pixel.R, pixel.G, pixel.B);
+ bitmap.SetPixel(0, 0, newPixel);
- //ReTweetしたものをTLに追加
- post = CreatePostsFromStatusData(status);
- if (post == null)
- throw new WebApiException("Invalid Json!", content);
+ // MemoryImage 作成時に画像はコピーされるため、この後 bitmap は破棄しても問題ない
+ newImage = MemoryImage.CopyFromImage(bitmap);
- //二重取得回避
- lock (LockObj)
- {
- if (TabInformations.GetInstance().ContainsKey(post.StatusId))
- return;
+ return true;
}
- //Retweet判定
- if (post.RetweetedId == null)
- throw new WebApiException("Invalid Json!", content);
- //ユーザー情報
- post.IsMe = true;
-
- post.IsRead = read;
- post.IsOwl = false;
- if (_readOwnPost) post.IsRead = true;
- post.IsDm = false;
-
- TabInformations.GetInstance().AddPost(post);
}
- public void RemoveDirectMessage(long id, PostClass post)
+ public async Task SendDirectMessage(string postStr)
{
this.CheckAccountState();
this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
- //if (post.IsMe)
- // _deletemessages.Add(post)
- //}
-
- HttpStatusCode res;
- try
- {
- res = twCon.DestroyDirectMessage(id);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, null);
- }
-
- public void PostFollowCommand(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.CreateFriendships(screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
- }
-
- public void PostRemoveCommand(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.DestroyFriendships(screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
- }
-
- public void PostCreateBlock(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.CreateBlock(screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
- }
-
- public void PostDestroyBlock(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.DestroyBlock(screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
- }
-
- public void PostReportSpam(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.ReportSpam(screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
- }
-
- public TwitterFriendship GetFriendshipInfo(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.ShowFriendships(_uname, screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- return TwitterFriendship.ParseJson(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
- }
-
- public TwitterUser GetUserInfo(string screenName)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.ShowUserInfo(screenName, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- return TwitterUser.ParseJson(content);
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
- }
-
- public int GetStatus_Retweeted_Count(long StatusId)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.ShowStatuses(StatusId, ref content);
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- var status = TwitterStatus.ParseJson(content);
- return status.RetweetCount;
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Invalid Json!", content, ex);
- }
- }
-
- public void PostFavAdd(long id)
- {
- this.CheckAccountState();
-
- //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
-
- //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.CreateFavorites(id, ref content);
- }
- catch(Exception ex)
- {
- //this.favQueue.Add(id)
- //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
-
- if (!RestrictFavCheck)
- return;
-
- //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
-
- try
- {
- res = twCon.ShowStatuses(id, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- this.CheckStatusCode(res, content);
-
- TwitterStatus status;
- try
- {
- status = TwitterStatus.ParseJson(content);
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
- if (status.Favorited != true)
- throw new WebApiException("NG(Restricted?)");
- }
-
- public void PostFavRemove(long id)
- {
- this.CheckAccountState();
-
- //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
+ var mc = Twitter.DMSendTextRegex.Match(postStr);
- //if (this.favQueue.Contains(id))
- // this.favQueue.Remove(id)
- // return "";
- //}
+ var response = await this.Api.DirectMessagesNew(mc.Groups["body"].Value, mc.Groups["id"].Value)
+ .ConfigureAwait(false);
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.DestroyFavorites(id, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
+ var dm = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, content);
+ this.UpdateUserStats(dm.Sender);
}
- public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
+ public async Task PostRetweet(long id, bool read)
{
this.CheckAccountState();
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.UpdateProfile(name, url, location, description, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, content, ex);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- return TwitterUser.ParseJson(content);
- }
- catch (SerializationException e)
- {
- var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- catch (Exception e)
- {
- var ex = new WebApiException("Err:Invalid Json!", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- }
+ //データ部分の生成
+ var post = TabInformations.GetInstance()[id];
+ if (post == null)
+ throw new WebApiException("Err:Target isn't found.");
- public void PostUpdateProfileImage(string filename)
- {
- this.CheckAccountState();
+ var target = post.RetweetedId ?? id; //再RTの場合は元発言をRT
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
- }
- catch(Exception ex)
+ var response = await this.Api.StatusesRetweet(target)
+ .ConfigureAwait(false);
+
+ var status = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
+
+ //二重取得回避
+ lock (LockObj)
{
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
+ if (TabInformations.GetInstance().ContainsKey(status.Id))
+ return;
}
- this.CheckStatusCode(res, content);
+ //Retweet判定
+ if (status.RetweetedStatus == null)
+ throw new WebApiException("Invalid Json!");
+
+ //ReTweetしたものをTLに追加
+ post = CreatePostsFromStatusData(status);
+
+ //ユーザー情報
+ post.IsMe = true;
+
+ post.IsRead = read;
+ post.IsOwl = false;
+ if (_readOwnPost) post.IsRead = true;
+ post.IsDm = false;
+
+ TabInformations.GetInstance().AddPost(post);
}
public string Username
- {
- get
- {
- return twCon.AuthenticatedUsername;
- }
- }
+ => this.Api.CurrentScreenName;
public long UserId
- {
- get
- {
- return twCon.AuthenticatedUserId;
- }
- }
-
- public string Password
- {
- get
- {
- return twCon.Password;
- }
- }
+ => this.Api.CurrentUserId;
private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
public static MyCommon.ACCOUNT_STATE AccountState
public bool RestrictFavCheck { get; set; }
-#region "バージョンアップ"
- public void GetTweenBinary(string strVer)
- {
- try
- {
- //本体
- if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
- Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
- {
- throw new WebApiException("Err:Download failed");
- }
- //英語リソース
- if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
- {
- Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
- }
- if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
- Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
- {
- throw new WebApiException("Err:Download failed");
- }
- //その他言語圏のリソース。取得失敗しても継続
- //UIの言語圏のリソース
- var curCul = "";
- if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
- {
- var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
- if (idx > -1)
- {
- curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
- }
- else
- {
- curCul = Thread.CurrentThread.CurrentUICulture.Name;
- }
- }
- else
- {
- curCul = Thread.CurrentThread.CurrentUICulture.Name;
- }
- if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
- {
- if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
- {
- Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
- }
- if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
- Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
- {
- //return "Err:Download failed";
- }
- }
- //スレッドの言語圏のリソース
- string curCul2;
- if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
- {
- var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
- if (idx > -1)
- {
- curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
- }
- else
- {
- curCul2 = Thread.CurrentThread.CurrentCulture.Name;
- }
- }
- else
- {
- curCul2 = Thread.CurrentThread.CurrentCulture.Name;
- }
- if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
- {
- if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
- {
- Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
- }
- if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
- Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
- {
- //return "Err:Download failed";
- }
- }
-
- //アップデータ
- if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
- Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
- {
- throw new WebApiException("Err:Download failed");
- }
- //シリアライザDLL
- if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
- Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
- {
- throw new WebApiException("Err:Download failed");
- }
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:Download failed", ex);
- }
- }
-#endregion
-
public bool ReadOwnPost
{
get
return 20;
}
- if (SettingCommon.Instance.UseAdditionalCount)
+ if (SettingManager.Common.UseAdditionalCount)
{
switch (type)
{
case MyCommon.WORKERTYPE.Favorites:
- if (SettingCommon.Instance.FavoritesCountApi != 0)
- return SettingCommon.Instance.FavoritesCountApi;
+ if (SettingManager.Common.FavoritesCountApi != 0)
+ return SettingManager.Common.FavoritesCountApi;
break;
case MyCommon.WORKERTYPE.List:
- if (SettingCommon.Instance.ListCountApi != 0)
- return SettingCommon.Instance.ListCountApi;
+ if (SettingManager.Common.ListCountApi != 0)
+ return SettingManager.Common.ListCountApi;
break;
case MyCommon.WORKERTYPE.PublicSearch:
- if (SettingCommon.Instance.SearchCountApi != 0)
- return SettingCommon.Instance.SearchCountApi;
+ if (SettingManager.Common.SearchCountApi != 0)
+ return SettingManager.Common.SearchCountApi;
break;
case MyCommon.WORKERTYPE.UserTimeline:
- if (SettingCommon.Instance.UserTimelineCountApi != 0)
- return SettingCommon.Instance.UserTimelineCountApi;
+ if (SettingManager.Common.UserTimelineCountApi != 0)
+ return SettingManager.Common.UserTimelineCountApi;
break;
}
- if (more && SettingCommon.Instance.MoreCountApi != 0)
+ if (more && SettingManager.Common.MoreCountApi != 0)
{
- return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
+ return Math.Min(SettingManager.Common.MoreCountApi, GetMaxApiResultCount(type));
}
- if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
+ if (startup && SettingManager.Common.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
{
- return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
+ return Math.Min(SettingManager.Common.FirstCountApi, GetMaxApiResultCount(type));
}
}
// 上記に当てはまらない場合の共通処理
- var count = SettingCommon.Instance.CountApi;
+ var count = SettingManager.Common.CountApi;
if (type == MyCommon.WORKERTYPE.Reply)
- count = SettingCommon.Instance.CountApiReply;
+ count = SettingManager.Common.CountApiReply;
return Math.Min(count, GetMaxApiResultCount(type));
}
- public void GetTimelineApi(bool read,
- MyCommon.WORKERTYPE gType,
- bool more,
- bool startup)
+ public async Task GetHomeTimelineApi(bool read, HomeTabModel tab, bool more, bool startup)
{
this.CheckAccountState();
- HttpStatusCode res;
- var content = "";
- var count = GetApiResultCount(gType, more, startup);
+ var count = GetApiResultCount(MyCommon.WORKERTYPE.Timeline, more, startup);
- try
+ TwitterStatus[] statuses;
+ if (more)
{
- if (gType == MyCommon.WORKERTYPE.Timeline)
- {
- if (more)
- {
- res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
- }
- else
- {
- res = twCon.HomeTimeline(count, null, null, ref content);
- }
- }
- else
- {
- if (more)
- {
- res = twCon.Mentions(count, this.minMentions, null, ref content);
- }
- else
- {
- res = twCon.Mentions(count, null, null, ref content);
- }
- }
+ statuses = await this.Api.StatusesHomeTimeline(count, maxId: tab.OldestId)
+ .ConfigureAwait(false);
}
- catch(Exception ex)
+ else
{
- throw new WebApiException("Err:" + ex.Message, ex);
+ statuses = await this.Api.StatusesHomeTimeline(count)
+ .ConfigureAwait(false);
}
- this.CheckStatusCode(res, content);
+ var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Timeline, tab, read);
+ if (minimumId != null)
+ tab.OldestId = minimumId.Value;
+ }
+
+ public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool more, bool startup)
+ {
+ this.CheckAccountState();
- var minimumId = CreatePostsFromJson(content, gType, null, read);
+ var count = GetApiResultCount(MyCommon.WORKERTYPE.Reply, more, startup);
- if (minimumId != null)
+ TwitterStatus[] statuses;
+ if (more)
{
- if (gType == MyCommon.WORKERTYPE.Timeline)
- this.minHomeTimeline = minimumId.Value;
- else
- this.minMentions = minimumId.Value;
+ statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId)
+ .ConfigureAwait(false);
}
+ else
+ {
+ statuses = await this.Api.StatusesMentionsTimeline(count)
+ .ConfigureAwait(false);
+ }
+
+ var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Reply, tab, read);
+ if (minimumId != null)
+ tab.OldestId = minimumId.Value;
}
- public void GetUserTimelineApi(bool read,
- string userName,
- TabClass tab,
- bool more)
+ public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTabModel tab, bool more)
{
this.CheckAccountState();
- HttpStatusCode res;
- var content = "";
var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
- try
+ TwitterStatus[] statuses;
+ if (string.IsNullOrEmpty(userName))
{
- if (string.IsNullOrEmpty(userName))
+ var target = tab.ScreenName;
+ if (string.IsNullOrEmpty(target)) return;
+ userName = target;
+ statuses = await this.Api.StatusesUserTimeline(userName, count)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ if (more)
{
- var target = tab.User;
- if (string.IsNullOrEmpty(target)) return;
- userName = target;
- res = twCon.UserTimeline(null, target, count, null, null, ref content);
+ statuses = await this.Api.StatusesUserTimeline(userName, count, maxId: tab.OldestId)
+ .ConfigureAwait(false);
}
else
{
- if (more)
- {
- res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
- }
- else
- {
- res = twCon.UserTimeline(null, userName, count, null, null, ref content);
- }
+ statuses = await this.Api.StatusesUserTimeline(userName, count)
+ .ConfigureAwait(false);
}
}
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- if (res == HttpStatusCode.Unauthorized)
- throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
- this.CheckStatusCode(res, content);
-
- var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
+ var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
if (minimumId != null)
tab.OldestId = minimumId.Value;
}
- public PostClass GetStatusApi(bool read, long id)
+ public async Task<PostClass> GetStatusApi(bool read, long id)
{
this.CheckAccountState();
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.ShowStatuses(id, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
-
- if (res == HttpStatusCode.Forbidden)
- throw new WebApiException("Err:protected user's tweet", content);
-
- this.CheckStatusCode(res, content);
-
- TwitterStatus status;
- try
- {
- status = TwitterStatus.ParseJson(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Invalid Json!", content, ex);
- }
+ var status = await this.Api.StatusesShow(id)
+ .ConfigureAwait(false);
var item = CreatePostsFromStatusData(status);
- if (item == null)
- throw new WebApiException("Err:Can't create post", content);
item.IsRead = read;
if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
return item;
}
- public void GetStatusApi(bool read, long id, TabClass tab)
+ public async Task GetStatusApi(bool read, long id, TabModel tab)
{
- var post = this.GetStatusApi(read, id);
+ var post = await this.GetStatusApi(read, id)
+ .ConfigureAwait(false);
//非同期アイコン取得&StatusDictionaryに追加
if (tab != null && tab.IsInnerStorageTabType)
- tab.AddPostToInnerStorage(post);
+ tab.AddPostQueue(post);
else
TabInformations.GetInstance().AddPost(post);
}
//Id
post.RetweetedId = retweeted.Id;
//本文
- post.TextFromApi = retweeted.Text;
+ post.TextFromApi = retweeted.FullText;
entities = retweeted.MergedEntities;
sourceHtml = retweeted.Source;
//Reply先
//以下、ユーザー情報
var user = retweeted.User;
-
- if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
-
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
+ if (user != null)
+ {
+ post.UserId = user.Id;
+ post.ScreenName = user.ScreenName;
+ post.Nickname = user.Name.Trim();
+ post.ImageUrl = user.ProfileImageUrlHttps;
+ post.IsProtect = user.Protected;
+ }
+ else
+ {
+ post.UserId = 0L;
+ post.ScreenName = "?????";
+ post.Nickname = "Unknown User";
+ }
//Retweetした人
- post.RetweetedBy = status.User.ScreenName;
- post.RetweetedByUserId = status.User.Id;
- post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+ if (status.User != null)
+ {
+ post.RetweetedBy = status.User.ScreenName;
+ post.RetweetedByUserId = status.User.Id;
+ post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+ }
+ else
+ {
+ post.RetweetedBy = "?????";
+ post.RetweetedByUserId = 0L;
+ }
}
else
{
post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
//本文
- post.TextFromApi = status.Text;
+ post.TextFromApi = status.FullText;
entities = status.MergedEntities;
sourceHtml = status.Source;
post.InReplyToStatusId = status.InReplyToStatusId;
//以下、ユーザー情報
var user = status.User;
-
- if (user == null || user.ScreenName == null) return null;
-
- post.UserId = user.Id;
- post.ScreenName = user.ScreenName;
- post.Nickname = user.Name.Trim();
- post.ImageUrl = user.ProfileImageUrlHttps;
- post.IsProtect = user.Protected;
- post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+ if (user != null)
+ {
+ post.UserId = user.Id;
+ post.ScreenName = user.ScreenName;
+ post.Nickname = user.Name.Trim();
+ post.ImageUrl = user.ProfileImageUrlHttps;
+ post.IsProtect = user.Protected;
+ post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+ }
+ else
+ {
+ post.UserId = 0L;
+ post.ScreenName = "?????";
+ post.Nickname = "Unknown User";
+ }
}
//HTMLに整形
string textFromApi = post.TextFromApi;
post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+ post.AccessibleText = this.CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus);
+ post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+ post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
.Where(x => x != post.StatusId && x != post.RetweetedId)
.Distinct().ToArray();
post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
- .Where(x => x != null)
.Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
.ToArray();
+ // メモリ使用量削減 (同一のテキストであれば同一の 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);
+ post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
+
//Source整形
- var source = ParseSource(sourceHtml);
- post.Source = source.Item1;
- post.SourceUri = source.Item2;
+ var (sourceText, sourceUri) = ParseSource(sourceHtml);
+ post.Source = string.Intern(sourceText);
+ post.SourceUri = sourceUri;
- post.IsReply = post.ReplyToList.Contains(_uname);
+ post.IsReply = post.RetweetedId == null && post.ReplyToList.Contains(_uname);
post.IsExcludeReply = false;
if (post.IsMe)
/// </summary>
public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
{
- var urls = entities.OfType<TwitterEntityUrl>().Where(x => x != null)
- .Select(x => x.ExpandedUrl);
+ var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
return GetQuoteTweetStatusIds(urls);
}
var match = Twitter.StatusUrlRegex.Match(url);
if (match.Success)
{
- long statusId;
- if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
+ if (long.TryParse(match.Groups["StatusId"].Value, out var statusId))
yield return statusId;
}
}
}
- private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
+ private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel tab, bool read)
{
- TwitterStatus[] items;
- try
- {
- items = TwitterStatus.ParseJsonArray(content);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Invalid Json!", content, ex);
- }
-
long? minimumId = null;
foreach (var status in items)
{
- PostClass post = null;
- post = CreatePostsFromStatusData(status);
- if (post == null) continue;
-
- if (minimumId == null || minimumId.Value > post.StatusId)
- minimumId = post.StatusId;
+ if (minimumId == null || minimumId.Value > status.Id)
+ minimumId = status.Id;
//二重取得回避
lock (LockObj)
{
if (tab == null)
{
- if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
+ if (TabInformations.GetInstance().ContainsKey(status.Id)) continue;
}
else
{
- if (tab.Contains(post.StatusId)) continue;
+ if (tab.Contains(status.Id)) continue;
}
}
//RT禁止ユーザーによるもの
if (gType != MyCommon.WORKERTYPE.UserTimeline &&
- post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
+ status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue;
+
+ var post = CreatePostsFromStatusData(status);
post.IsRead = read;
if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
- //非同期アイコン取得&StatusDictionaryに追加
if (tab != null && tab.IsInnerStorageTabType)
- tab.AddPostToInnerStorage(post);
+ tab.AddPostQueue(post);
else
TabInformations.GetInstance().AddPost(post);
}
return minimumId;
}
- private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
+ private long? CreatePostsFromSearchJson(TwitterSearchResult items, PublicSearchTabModel tab, bool read, bool more)
{
- TwitterSearchResult items;
- try
- {
- items = TwitterSearchResult.ParseJson(content);
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Invalid Json!", content, ex);
- }
-
long? minimumId = null;
- foreach (var result in items.Statuses)
+ foreach (var status in items.Statuses)
{
- PostClass post = null;
- post = CreatePostsFromStatusData(result);
-
- if (post == null)
- {
- // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
- try
- {
- post = this.GetStatusApi(read, result.Id);
- }
- catch (WebApiException)
- {
- continue;
- }
- }
-
- if (minimumId == null || minimumId.Value > post.StatusId)
- minimumId = post.StatusId;
+ if (minimumId == null || minimumId.Value > status.Id)
+ minimumId = status.Id;
- if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
+ if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id;
//二重取得回避
lock (LockObj)
{
- if (tab == null)
- {
- if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
- }
- else
- {
- if (tab.Contains(post.StatusId)) continue;
- }
+ if (tab.Contains(status.Id)) continue;
}
+ var post = CreatePostsFromStatusData(status);
+
post.IsRead = read;
if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
- //非同期アイコン取得&StatusDictionaryに追加
- if (tab != null && tab.IsInnerStorageTabType)
- tab.AddPostToInnerStorage(post);
- else
- TabInformations.GetInstance().AddPost(post);
+ tab.AddPostQueue(post);
}
return minimumId;
}
- private void CreateFavoritePostsFromJson(string content, bool read)
+ private long? CreateFavoritePostsFromJson(TwitterStatus[] items, bool read)
{
- TwitterStatus[] item;
- try
- {
- item = TwitterStatus.ParseJsonArray(content);
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Invalid Json!", content, ex);
- }
-
var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+ long? minimumId = null;
- foreach (var status in item)
+ foreach (var status in items)
{
+ if (minimumId == null || minimumId.Value > status.Id)
+ minimumId = status.Id;
+
//二重取得回避
lock (LockObj)
{
}
var post = CreatePostsFromStatusData(status, true);
- if (post == null) continue;
post.IsRead = read;
TabInformations.GetInstance().AddPost(post);
}
+
+ return minimumId;
}
- public void GetListStatus(bool read,
- TabClass tab,
- bool more,
- bool startup)
+ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, bool startup)
{
- HttpStatusCode res;
- var content = "";
var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
- try
+ TwitterStatus[] statuses;
+ if (more)
{
- if (more)
- {
- res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
- }
- else
- {
- res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
- }
+ statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Common.IsListsIncludeRts)
+ .ConfigureAwait(false);
}
- catch(Exception ex)
+ else
{
- throw new WebApiException("Err:" + ex.Message, ex);
+ statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingManager.Common.IsListsIncludeRts)
+ .ConfigureAwait(false);
}
- this.CheckStatusCode(res, content);
-
- var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
+ var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
if (minimumId != null)
tab.OldestId = minimumId.Value;
return nextPost;
}
- public void GetRelatedResult(bool read, TabClass tab)
+ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab)
{
+ var targetPost = tab.TargetPost;
var relPosts = new Dictionary<Int64, PostClass>();
- if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
+ if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null)
{
//検索結果対応
- var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
+ var p = TabInformations.GetInstance()[targetPost.StatusId];
if (p != null && p.InReplyToStatusId != null)
{
- tab.RelationTargetPost = p;
+ targetPost = p;
}
else
{
- p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
- tab.RelationTargetPost = p;
+ p = await this.GetStatusApi(read, targetPost.StatusId)
+ .ConfigureAwait(false);
+ targetPost = p;
}
}
- relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
+ relPosts.Add(targetPost.StatusId, targetPost);
Exception lastException = null;
// in_reply_to_status_id を使用してリプライチェインを辿る
- var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
+ var nextPost = FindTopOfReplyChain(relPosts, targetPost.StatusId);
var loopCount = 1;
while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
{
{
try
{
- inReplyToPost = this.GetStatusApi(read, inReplyToId);
+ inReplyToPost = await this.GetStatusApi(read, inReplyToId)
+ .ConfigureAwait(false);
}
catch (WebApiException ex)
{
}
//MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
- var text = tab.RelationTargetPost.Text;
+ var text = targetPost.Text;
var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
.Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
foreach (var _match in ma)
{
- Int64 _statusId;
- if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
+ if (Int64.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
{
if (relPosts.ContainsKey(_statusId))
continue;
{
try
{
- p = this.GetStatusApi(read, _statusId);
+ p = await this.GetStatusApi(read, _statusId)
+ .ConfigureAwait(false);
}
catch (WebApiException ex)
{
else
p.IsRead = read;
- tab.AddPostToInnerStorage(p);
+ tab.AddPostQueue(p);
});
if (lastException != null)
throw new WebApiException(lastException.Message, lastException);
}
- public void GetSearch(bool read,
- TabClass tab,
- bool more)
+ public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more)
{
- HttpStatusCode res;
- var content = "";
var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
+
long? maxId = null;
long? sinceId = null;
if (more)
sinceId = tab.SinceId;
}
- try
- {
- // TODO:一時的に40>100件に 件数変更UI作成の必要あり
- res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message, ex);
- }
- switch (res)
- {
- case HttpStatusCode.BadRequest:
- throw new WebApiException("Invalid query", content);
- case HttpStatusCode.NotFound:
- throw new WebApiException("Invalid query", content);
- case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
- throw new WebApiException("Search API Limit?", content);
- case HttpStatusCode.OK:
- break;
- default:
- throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
- }
+ var searchResult = await this.Api.SearchTweets(tab.SearchWords, tab.SearchLang, count, maxId, sinceId)
+ .ConfigureAwait(false);
if (!TabInformations.GetInstance().ContainsTab(tab))
return;
- var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
+ var minimumId = this.CreatePostsFromSearchJson(searchResult, tab, read, more);
if (minimumId != null)
tab.OldestId = minimumId.Value;
}
- private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
+ private void CreateDirectMessagesFromJson(TwitterDirectMessage[] item, MyCommon.WORKERTYPE gType, bool read)
{
- TwitterDirectMessage[] item;
- try
- {
- if (gType == MyCommon.WORKERTYPE.UserStream)
- {
- item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
- }
- else
- {
- item = TwitterDirectMessage.ParseJsonArray(content);
- }
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Invalid Json!", content, ex);
- }
-
foreach (var message in item)
{
var post = new PostClass();
post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+ post.AccessibleText = this.CreateAccessibleText(textFromApi, message.Entities, quoteStatus: null);
+ post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+ post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
post.IsFav = false;
post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
- .Where(x => x != null)
.Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
.ToArray();
TwitterUser user;
if (gType == MyCommon.WORKERTYPE.UserStream)
{
- if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
+ if (this.Api.CurrentUserId == message.Recipient.Id)
{
user = message.Sender;
post.IsMe = false;
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 + " " + content);
+ MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
continue;
}
post.IsDm = true;
var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
- dmTab.AddPostToInnerStorage(post);
+ dmTab.AddPostQueue(post);
}
}
- public void GetDirectMessageApi(bool read,
- MyCommon.WORKERTYPE gType,
- bool more)
+ public async Task GetDirectMessageApi(bool read, MyCommon.WORKERTYPE gType, bool more)
{
this.CheckAccountState();
this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
- HttpStatusCode res;
- var content = "";
var count = GetApiResultCount(gType, more, false);
- try
+ TwitterDirectMessage[] messages;
+ if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
{
- if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
+ if (more)
{
- if (more)
- {
- res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
- }
- else
- {
- res = twCon.DirectMessages(count, null, null, ref content);
- }
+ messages = await this.Api.DirectMessagesRecv(count, maxId: this.minDirectmessage)
+ .ConfigureAwait(false);
}
else
{
- if (more)
- {
- res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
- }
- else
- {
- res = twCon.DirectMessagesSent(count, null, null, ref content);
- }
+ messages = await this.Api.DirectMessagesRecv(count)
+ .ConfigureAwait(false);
}
}
- catch(Exception ex)
+ else
{
- throw new WebApiException("Err:" + ex.Message, ex);
+ if (more)
+ {
+ messages = await this.Api.DirectMessagesSent(count, maxId: this.minDirectmessageSent)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ messages = await this.Api.DirectMessagesSent(count)
+ .ConfigureAwait(false);
+ }
}
- this.CheckStatusCode(res, content);
-
- CreateDirectMessagesFromJson(content, gType, read);
+ CreateDirectMessagesFromJson(messages, gType, read);
}
- public void GetFavoritesApi(bool read,
- bool more)
+ public async Task GetFavoritesApi(bool read, FavoritesTabModel tab, bool backward)
{
this.CheckAccountState();
- HttpStatusCode res;
- var content = "";
- var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
+ var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, backward, false);
- try
+ TwitterStatus[] statuses;
+ if (backward)
{
- res = twCon.Favorites(count, ref content);
+ statuses = await this.Api.FavoritesList(count, maxId: tab.OldestId)
+ .ConfigureAwait(false);
}
- catch(Exception ex)
+ else
{
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
+ statuses = await this.Api.FavoritesList(count)
+ .ConfigureAwait(false);
}
- this.CheckStatusCode(res, content);
+ var minimumId = this.CreateFavoritePostsFromJson(statuses, read);
- CreateFavoritePostsFromJson(content, read);
+ if (minimumId != null)
+ tab.OldestId = minimumId.Value;
}
private string ReplaceTextFromApi(string text, TwitterEntities entities)
return text;
}
+ private string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quoteStatus)
+ {
+ if (entities == null)
+ return text;
+
+ if (entities.Urls != null)
+ {
+ foreach (var entity in entities.Urls)
+ {
+ if (quoteStatus != null)
+ {
+ var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
+ if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quoteStatus.IdStr)
+ {
+ var quoteText = this.CreateAccessibleText(quoteStatus.FullText, quoteStatus.MergedEntities, quoteStatus: null);
+ text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quoteStatus.User.ScreenName, quoteText));
+ }
+ }
+ else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+ {
+ text = text.Replace(entity.Url, entity.DisplayUrl);
+ }
+ }
+ }
+
+ if (entities.Media != null)
+ {
+ foreach (var entity in entities.Media)
+ {
+ if (!string.IsNullOrEmpty(entity.AltText))
+ {
+ text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
+ }
+ else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+ {
+ text = text.Replace(entity.Url, entity.DisplayUrl);
+ }
+ }
+ }
+
+ return text;
+ }
+
/// <summary>
/// フォロワーIDを更新します
/// </summary>
/// <exception cref="WebApiException"/>
- public void RefreshFollowerIds()
+ public async Task RefreshFollowerIds()
{
if (MyCommon._endingFlag) return;
var newFollowerIds = new HashSet<long>();
do
{
- var ret = this.GetFollowerIdsApi(ref cursor);
+ var ret = await this.Api.FollowersIds(cursor)
+ .ConfigureAwait(false);
+
+ if (ret.Ids == null)
+ throw new WebApiException("ret.ids == null");
+
newFollowerIds.UnionWith(ret.Ids);
cursor = ret.NextCursor;
} while (cursor != 0);
}
}
- private TwitterIds GetFollowerIdsApi(ref long cursor)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.FollowerIds(cursor, ref content);
- }
- catch(Exception e)
- {
- throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- var ret = TwitterIds.ParseJson(content);
-
- if (ret.Ids == null)
- {
- var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
- MyCommon.ExceptionOut(ex);
- throw ex;
- }
-
- return ret;
- }
- catch(SerializationException e)
- {
- var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- catch(Exception e)
- {
- var ex = new WebApiException("Err:Invalid Json!", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- }
-
/// <summary>
/// RT 非表示ユーザーを更新します
/// </summary>
/// <exception cref="WebApiException"/>
- public void RefreshNoRetweetIds()
+ public async Task RefreshNoRetweetIds()
{
if (MyCommon._endingFlag) return;
- this.noRTId = this.NoRetweetIdsApi();
+ this.noRTId = await this.Api.NoRetweetIds()
+ .ConfigureAwait(false);
this._GetNoRetweetResult = true;
}
- private long[] NoRetweetIdsApi()
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.NoRetweetIds(ref content);
- }
- catch(Exception e)
- {
- throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- return MyCommon.CreateDataFromJson<long[]>(content);
- }
- catch(SerializationException e)
- {
- var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- catch(Exception e)
- {
- var ex = new WebApiException("Err:Invalid Json!", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- }
-
public bool GetNoRetweetSuccess
{
get
/// t.co の文字列長などの設定情報を更新します
/// </summary>
/// <exception cref="WebApiException"/>
- public void RefreshConfiguration()
- {
- this.Configuration = this.ConfigurationApi();
- }
-
- private TwitterConfiguration ConfigurationApi()
+ public async Task RefreshConfiguration()
{
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.GetConfiguration(ref content);
- }
- catch(Exception e)
- {
- throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
- }
-
- this.CheckStatusCode(res, content);
+ this.Configuration = await this.Api.Configuration()
+ .ConfigureAwait(false);
- try
- {
- return TwitterConfiguration.ParseJson(content);
- }
- catch(SerializationException e)
- {
- var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- catch(Exception e)
- {
- var ex = new WebApiException("Err:Invalid Json!", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
+ // TextConfiguration 相当の JSON を得る API が存在しないため、TransformedURLLength のみ help/configuration.json に合わせて更新する
+ this.TextConfiguration.TransformedURLLength = this.Configuration.ShortUrlLengthHttps;
}
- public void GetListsApi()
+ public async Task GetListsApi()
{
this.CheckAccountState();
- HttpStatusCode res;
- IEnumerable<ListElement> lists;
- var content = "";
-
- try
- {
- res = twCon.GetLists(this.Username, ref content);
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- lists = TwitterList.ParseJsonArray(content)
- .Select(x => new ListElement(x, this));
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
-
- try
- {
- res = twCon.GetListsSubscriptions(this.Username, ref content);
- }
- catch (Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- lists = lists.Concat(TwitterList.ParseJsonArray(content)
- .Select(x => new ListElement(x, this)));
- }
- catch (SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
-
- TabInformations.GetInstance().SubscribableLists = lists.ToList();
- }
-
- public void DeleteList(string list_id)
- {
- HttpStatusCode res;
- var content = "";
-
- try
- {
- res = twCon.DeleteListID(this.Username, list_id, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- this.CheckStatusCode(res, content);
- }
-
- public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
- {
- HttpStatusCode res;
- var content = "";
-
- try
- {
- res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
+ var ownedLists = await TwitterLists.GetAllItemsAsync(x =>
+ this.Api.ListsOwnerships(this.Username, cursor: x, count: 1000))
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, content);
+ var subscribedLists = await TwitterLists.GetAllItemsAsync(x =>
+ this.Api.ListsSubscriptions(this.Username, cursor: x, count: 1000))
+ .ConfigureAwait(false);
- try
- {
- var le = TwitterList.ParseJson(content);
- return new ListElement(le, this);
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
+ TabInformations.GetInstance().SubscribableLists = Enumerable.Concat(ownedLists, subscribedLists)
+ .Select(x => new ListElement(x, this))
+ .ToList();
}
- public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
+ public async Task DeleteList(long listId)
{
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message);
- }
-
- this.CheckStatusCode(res, content);
+ await this.Api.ListsDestroy(listId)
+ .IgnoreResponse()
+ .ConfigureAwait(false);
- try
- {
- var users = TwitterUsers.ParseJson(content);
- Array.ForEach<TwitterUser>(
- users.Users,
- u => lists.Add(new UserInfo(u)));
+ var tabinfo = TabInformations.GetInstance();
- return users.NextCursor;
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
+ tabinfo.SubscribableLists = tabinfo.SubscribableLists
+ .Where(x => x.Id != listId)
+ .ToList();
}
- public void CreateListApi(string listName, bool isPrivate, string description)
+ public async Task<ListElement> EditList(long listId, string new_name, bool isPrivate, string description)
{
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.CreateLists(listName, isPrivate, description, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
+ var response = await this.Api.ListsUpdate(listId, new_name, description, isPrivate)
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, content);
+ var list = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
- try
- {
- var le = TwitterList.ParseJson(content);
- TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
- }
- catch(SerializationException ex)
- {
- MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
- throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- }
- catch(Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- throw new WebApiException("Err:Invalid Json!", content, ex);
- }
+ return new ListElement(list, this);
}
- public bool ContainsUserAtList(string listId, string user)
+ public async Task<long> GetListMembers(long listId, List<UserInfo> lists, long cursor)
{
this.CheckAccountState();
- HttpStatusCode res;
- var content = "";
-
- try
- {
- res = this.twCon.ShowListMember(listId, user, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
-
- if (res == HttpStatusCode.NotFound)
- {
- return false;
- }
+ var users = await this.Api.ListsMembers(listId, cursor)
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, content);
+ Array.ForEach(users.Users, u => lists.Add(new UserInfo(u)));
- try
- {
- TwitterUser.ParseJson(content);
- return true;
- }
- catch(Exception)
- {
- return false;
- }
+ return users.NextCursor;
}
- public void AddUserToList(string listId, string user)
+ public async Task CreateListApi(string listName, bool isPrivate, string description)
{
- HttpStatusCode res;
- var content = "";
+ this.CheckAccountState();
- try
- {
- res = twCon.CreateListMembers(listId, user, ref content);
- }
- catch(Exception ex)
- {
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
- }
+ var response = await this.Api.ListsCreate(listName, description, isPrivate)
+ .ConfigureAwait(false);
+
+ var list = await response.LoadJsonAsync()
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, content);
+ TabInformations.GetInstance().SubscribableLists.Add(new ListElement(list, this));
}
- public void RemoveUserToList(string listId, string user)
+ public async Task<bool> ContainsUserAtList(long listId, string user)
{
- HttpStatusCode res;
- var content = "";
+ this.CheckAccountState();
try
{
- res = twCon.DeleteListMembers(listId, user, ref content);
+ await this.Api.ListsMembersShow(listId, user)
+ .ConfigureAwait(false);
+
+ return true;
}
- catch(Exception ex)
+ catch (TwitterApiException ex)
+ when (ex.ErrorResponse.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
{
- throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
+ return false;
}
-
- this.CheckStatusCode(res, content);
}
public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
// .Where(v => v.ContentType == "video/mp4")
// .OrderByDescending(v => v.Bitrate)
// .Select(v => v.Url).FirstOrDefault();
- media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
+ media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
}
else
- media.Add(new MediaInfo(ent.MediaUrl));
+ media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
}
}
}
/// <summary>
/// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
/// </summary>
- public static Tuple<string, Uri> ParseSource(string sourceHtml)
+ internal static (string SourceText, Uri SourceUri) ParseSource(string sourceHtml)
{
if (string.IsNullOrEmpty(sourceHtml))
- return Tuple.Create<string, Uri>("", null);
+ return ("", null);
string sourceText;
Uri sourceUri;
sourceUri = null;
}
- return Tuple.Create(sourceText, sourceUri);
+ return (sourceText, sourceUri);
}
- public TwitterApiStatus GetInfoApi()
+ public async Task<TwitterApiStatus> GetInfoApi()
{
if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
if (MyCommon._endingFlag) return null;
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.RateLimitStatus(ref content);
- }
- catch (Exception)
- {
- this.ResetApiStatus();
- return null;
- }
+ var limits = await this.Api.ApplicationRateLimitStatus()
+ .ConfigureAwait(false);
- this.CheckStatusCode(res, content);
+ MyCommon.TwitterApiInfo.UpdateFromJson(limits);
- try
- {
- MyCommon.TwitterApiInfo.UpdateFromJson(content);
- return MyCommon.TwitterApiInfo;
- }
- catch (Exception ex)
- {
- MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
- MyCommon.TwitterApiInfo.Reset();
- return null;
- }
+ return MyCommon.TwitterApiInfo;
}
/// <summary>
/// ブロック中のユーザーを更新します
/// </summary>
/// <exception cref="WebApiException"/>
- public void RefreshBlockIds()
+ public async Task RefreshBlockIds()
{
if (MyCommon._endingFlag) return;
var newBlockIds = new HashSet<long>();
do
{
- var ret = this.GetBlockIdsApi(cursor);
+ var ret = await this.Api.BlocksIds(cursor)
+ .ConfigureAwait(false);
+
newBlockIds.UnionWith(ret.Ids);
cursor = ret.NextCursor;
} while (cursor != 0);
TabInformations.GetInstance().BlockIds = newBlockIds;
}
- public TwitterIds GetBlockIdsApi(long cursor)
- {
- this.CheckAccountState();
-
- HttpStatusCode res;
- var content = "";
- try
- {
- res = twCon.GetBlockUserIds(ref content, cursor);
- }
- catch(Exception e)
- {
- throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
- }
-
- this.CheckStatusCode(res, content);
-
- try
- {
- return TwitterIds.ParseJson(content);
- }
- catch(SerializationException e)
- {
- var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- catch(Exception e)
- {
- var ex = new WebApiException("Err:Invalid Json!", content, e);
- MyCommon.TraceOut(ex);
- throw ex;
- }
- }
-
/// <summary>
/// ミュート中のユーザーIDを更新します
/// </summary>
{
if (MyCommon._endingFlag) return;
- var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
+ var ids = await TwitterIds.GetAllItemsAsync(x => this.Api.MutesUsersIds(x))
.ConfigureAwait(false);
TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
}
- public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
- {
- var content = "";
-
- try
- {
- var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
- .ConfigureAwait(false);
-
- this.CheckStatusCode(res, content);
-
- return TwitterIds.ParseJson(content);
- }
- catch (WebException ex)
- {
- var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
- MyCommon.TraceOut(ex2);
- throw ex2;
- }
- catch (SerializationException ex)
- {
- var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
- MyCommon.TraceOut(ex2);
- throw ex2;
- }
- }
-
public string[] GetHashList()
{
string[] hashArray;
}
public string AccessToken
- {
- get
- {
- return twCon.AccessToken;
- }
- }
+ => ((TwitterApiConnection)this.Api.Connection).AccessToken;
public string AccessTokenSecret
- {
- get
- {
- return twCon.AccessTokenSecret;
- }
- }
+ => ((TwitterApiConnection)this.Api.Connection).AccessSecret;
private void CheckAccountState()
{
throw new WebApiException("Auth Err:try to re-authorization.");
}
- private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
- [CallerMemberName] string callerMethodName = "")
- {
- if (httpStatus == HttpStatusCode.OK)
- {
- Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
- return;
- }
-
- if (string.IsNullOrWhiteSpace(responseText))
- {
- if (httpStatus == HttpStatusCode.Unauthorized)
- Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
-
- throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
- }
-
- try
- {
- var errors = TwitterError.ParseJson(responseText).Errors;
- if (errors == null || !errors.Any())
- {
- throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
- }
-
- foreach (var error in errors)
- {
- if (error.Code == TwitterErrorCode.InvalidToken ||
- error.Code == TwitterErrorCode.SuspendedAccount)
- {
- Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
- }
- }
-
- throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
- }
- catch (SerializationException) { }
-
- throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
- }
-
public int GetTextLengthRemain(string postText)
{
var matchDm = Twitter.DMSendTextRegex.Match(postText);
if (matchDm.Success)
- return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
+ return this.GetTextLengthRemainDM(matchDm.Groups["body"].Value);
- return this.GetTextLengthRemainInternal(postText, isDm: false);
+ return this.GetTextLengthRemainWeighted(postText);
}
- private int GetTextLengthRemainInternal(string postText, bool isDm)
+ private int GetTextLengthRemainDM(string postText)
{
var textLength = 0;
textLength += shortUrlLength - url.Length;
}
- if (isDm)
- return this.Configuration.DmTextCharacterLimit - textLength;
- else
- return 140 - textLength;
+ return this.Configuration.DmTextCharacterLimit - textLength;
+ }
+
+ private int GetTextLengthRemainWeighted(string postText)
+ {
+ var config = this.TextConfiguration;
+ var totalWeight = 0;
+
+ var urls = TweetExtractor.ExtractUrlEntities(postText).ToArray();
+
+ var pos = 0;
+ while (pos < postText.Length)
+ {
+ var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == pos);
+ if (urlEntity != null)
+ {
+ totalWeight += config.TransformedURLLength * config.Scale;
+
+ var urlLength = urlEntity.Indices[1] - urlEntity.Indices[0];
+ pos += urlLength;
+
+ continue;
+ }
+
+ var codepoint = postText.GetCodepointAtSafe(pos);
+ var weight = config.DefaultWeight;
+
+ foreach (var weightRange in config.Ranges)
+ {
+ if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
+ {
+ weight = weightRange.Weight;
+ break;
+ }
+ }
+
+ totalWeight += weight;
+
+ var isSurrogatePair = codepoint > 0xffff;
+ if (isSurrogatePair)
+ pos += 2; // サロゲートペアの場合は2文字分進める
+ else
+ pos++;
+ }
+
+ var remainWeight = config.MaxWeightedTweetLength * config.Scale - totalWeight;
+
+ return remainWeight / config.Scale;
}
if (isDm)
{
- CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
+ try
+ {
+ var message = TwitterStreamEventDirectMessage.ParseJson(line).DirectMessage;
+ this.CreateDirectMessagesFromJson(new[] { message }, MyCommon.WORKERTYPE.UserStream, false);
+ }
+ catch (SerializationException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex, line);
+ }
}
else
{
- CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
+ try
+ {
+ var status = TwitterStatusCompat.ParseJson(line);
+ this.CreatePostsFromJson(new[] { status.Normalize() }, MyCommon.WORKERTYPE.UserStream, null, false);
+ }
+ catch (SerializationException ex)
+ {
+ throw TwitterApiException.CreateFromException(ex, line);
+ }
}
}
catch (WebApiException ex)
MyCommon.TraceOut(ex);
return;
}
+ catch (XmlException)
+ {
+ MyCommon.TraceOut("XmlException (StatusArrived): " + line);
+ }
catch(NullReferenceException)
{
MyCommon.TraceOut("NullRef StatusArrived: " + line);
evt.Username = eventData.Source.ScreenName;
evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
- MyCommon.EVENTTYPE eventType;
- eventTable.TryGetValue(eventData.Event, out eventType);
+ eventTable.TryGetValue(eventData.Event, out var eventType);
evt.Eventtype = eventType;
- TwitterStreamEvent<TwitterStatus> tweetEvent;
+ TwitterStreamEvent<TwitterStatusCompat> tweetEvent;
+ TwitterStatus tweet;
switch (eventData.Event)
{
return;
case "favorite":
case "unfavorite":
- tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
- evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
- evt.Id = tweetEvent.TargetObject.Id;
+ tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
+ tweet = tweetEvent.TargetObject.Normalize();
+ evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
+ evt.Id = tweet.Id;
- if (SettingCommon.Instance.IsRemoveSameEvent)
+ 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();
- PostClass post;
- var statusId = tweetEvent.TargetObject.Id;
- if (!tabinfo.Posts.TryGetValue(statusId, out post))
+ var statusId = tweet.Id;
+ if (!tabinfo.Posts.TryGetValue(statusId, out var post))
break;
if (eventData.Event == "favorite")
{
var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
- if (!favTab.Contains(post.StatusId))
- favTab.AddPostImmediately(post.StatusId, post.IsRead);
+ favTab.AddPostQueue(post);
if (tweetEvent.Source.Id == this.UserId)
{
{
post.FavoritedCount++;
- if (SettingCommon.Instance.FavEventUnread)
+ if (SettingManager.Common.FavEventUnread)
tabinfo.SetReadAllTab(post.StatusId, read: false);
}
}
case "quoted_tweet":
if (evt.IsMe) return;
- tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
- evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
- evt.Id = tweetEvent.TargetObject.Id;
+ tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
+ tweet = tweetEvent.TargetObject.Normalize();
+ evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
+ evt.Id = tweet.Id;
- if (SettingCommon.Instance.IsRemoveSameEvent)
+ if (SettingManager.Common.IsRemoveSameEvent)
{
if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
return;
this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
}
- public bool UserStreamEnabled
- {
- get
- {
- return userStream == null ? false : userStream.Enabled;
- }
- }
+ public bool UserStreamActive
+ => this.userStream != null && this.userStream.IsStreamActive;
public void StartUserStream()
{
- if (userStream != null)
- {
- StopUserStream();
- }
- userStream = new TwitterUserstream(twCon);
- userStream.StatusArrived += userStream_StatusArrived;
- userStream.Started += userStream_Started;
- userStream.Stopped += userStream_Stopped;
- userStream.Start(this.AllAtReply, this.TrackWord);
+ var newStream = new TwitterUserstream(this.Api);
+
+ newStream.StatusArrived += userStream_StatusArrived;
+ newStream.Started += userStream_Started;
+ newStream.Stopped += userStream_Stopped;
+
+ newStream.Start(this.AllAtReply, this.TrackWord);
+
+ var oldStream = Interlocked.Exchange(ref this.userStream, newStream);
+ oldStream?.Dispose();
}
public void StopUserStream()
{
- userStream?.Dispose();
- userStream = null;
- if (!MyCommon._endingFlag)
- {
- this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
- }
+ var oldStream = Interlocked.Exchange(ref this.userStream, null);
+ oldStream?.Dispose();
}
public void ReconnectUserStream()
{
- if (userStream != null)
+ if (this.userStream != null)
{
this.StartUserStream();
}
private class TwitterUserstream : IDisposable
{
+ public bool AllAtReplies { get; private set; }
+ public string TrackWords { get; private set; }
+
+ public bool IsStreamActive { get; private set; }
+
public event Action<string> StatusArrived;
public event Action Stopped;
public event Action Started;
- private HttpTwitter twCon;
- private Thread _streamThread;
- private bool _streamActive;
+ private TwitterApi twitterApi;
- private bool _allAtreplies = false;
- private string _trackwords = "";
+ private Task streamTask;
+ private CancellationTokenSource streamCts;
- public TwitterUserstream(HttpTwitter twitterConnection)
+ public TwitterUserstream(TwitterApi twitterApi)
{
- twCon = (HttpTwitter)twitterConnection.Clone();
+ this.twitterApi = twitterApi;
}
public void Start(bool allAtReplies, string trackwords)
{
this.AllAtReplies = allAtReplies;
this.TrackWords = trackwords;
- _streamActive = true;
- if (_streamThread != null && _streamThread.IsAlive) return;
- _streamThread = new Thread(UserStreamLoop);
- _streamThread.Name = "UserStreamReceiver";
- _streamThread.IsBackground = true;
- _streamThread.Start();
- }
- public bool Enabled
- {
- get
- {
- return _streamActive;
- }
- }
+ var cts = new CancellationTokenSource();
- public bool AllAtReplies
- {
- get
+ this.streamCts = cts;
+ this.streamTask = Task.Run(async () =>
{
- return _allAtreplies;
- }
- set
- {
- _allAtreplies = value;
- }
+ try
+ {
+ await this.UserStreamLoop(cts.Token)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { }
+ });
}
- public string TrackWords
+ public void Stop()
{
- get
- {
- return _trackwords;
- }
- set
- {
- _trackwords = value;
- }
+ this.streamCts?.Cancel();
+
+ // streamTask の完了を待たずに IsStreamActive を false にセットする
+ this.IsStreamActive = false;
+ this.Stopped?.Invoke();
}
- private void UserStreamLoop()
+ private async Task UserStreamLoop(CancellationToken cancellationToken)
{
- var sleepSec = 0;
- do
+ TimeSpan sleep = TimeSpan.Zero;
+ for (;;)
{
- Stream st = null;
- StreamReader sr = null;
- try
+ if (sleep != TimeSpan.Zero)
{
- if (!MyCommon.IsNetworkAvailable())
- {
- sleepSec = 30;
- continue;
- }
+ await Task.Delay(sleep, cancellationToken)
+ .ConfigureAwait(false);
+ sleep = TimeSpan.Zero;
+ }
- Started?.Invoke();
+ if (!MyCommon.IsNetworkAvailable())
+ {
+ sleep = TimeSpan.FromSeconds(30);
+ continue;
+ }
- var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
+ this.IsStreamActive = true;
+ this.Started?.Invoke();
- switch (res)
- {
- case HttpStatusCode.OK:
- Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
- break;
- case HttpStatusCode.Unauthorized:
- Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
- sleepSec = 120;
- continue;
- }
+ try
+ {
+ var replies = this.AllAtReplies ? "all" : null;
- if (st == null)
+ using (var stream = await this.twitterApi.UserStreams(replies, this.TrackWords)
+ .ConfigureAwait(false))
+ using (var reader = new StreamReader(stream))
{
- sleepSec = 30;
- //MyCommon.TraceOut("Stop:stream is null")
- continue;
- }
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync()
+ .ConfigureAwait(false);
- sr = new StreamReader(st);
+ cancellationToken.ThrowIfCancellationRequested();
- while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
- {
- StatusArrived?.Invoke(sr.ReadLine());
- //this.LastTime = Now;
+ this.StatusArrived?.Invoke(line);
+ }
}
- if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
- {
- sleepSec = 30;
- //MyCommon.TraceOut("Stop:EndOfStream")
- continue;
- }
- break;
- }
- catch(WebException ex)
- {
- if (ex.Status == WebExceptionStatus.Timeout)
- {
- sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
- }
- else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
- {
- //MyCommon.TraceOut("Stop:Connection Limit")
- break;
- }
- else
- {
- sleepSec = 30;
- //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
- }
- }
- catch(ThreadAbortException)
- {
- break;
- }
- catch(IOException)
- {
- sleepSec = 30;
- //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
+ // キャンセルされていないのにストリームが終了した場合
+ sleep = TimeSpan.FromSeconds(30);
}
- catch(ArgumentException ex)
+ catch (TwitterApiException) { sleep = TimeSpan.FromSeconds(30); }
+ catch (IOException) { sleep = TimeSpan.FromSeconds(30); }
+ catch (OperationCanceledException)
{
- //System.ArgumentException: ストリームを読み取れませんでした。
- //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
- sleepSec = 30;
- MyCommon.TraceOut(ex, "Stop:ArgumentException");
+ if (cancellationToken.IsCancellationRequested)
+ throw;
+
+ // cancellationToken によるキャンセルではない(=タイムアウトエラー)
+ sleep = TimeSpan.FromSeconds(30);
}
- catch(Exception ex)
+ catch (Exception ex)
{
- MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
MyCommon.ExceptionOut(ex);
- sleepSec = 30;
+ sleep = TimeSpan.FromSeconds(30);
}
finally
{
- if (_streamActive)
- {
- Stopped?.Invoke();
- }
- twCon.RequestAbort();
- sr?.Close();
- if (sleepSec > 0)
- {
- var ms = 0;
- while (_streamActive && ms < sleepSec * 1000)
- {
- Thread.Sleep(500);
- ms += 500;
- }
- }
- sleepSec = 0;
+ this.IsStreamActive = false;
+ this.Stopped?.Invoke();
}
- } while (this._streamActive);
-
- if (_streamActive)
- {
- Stopped?.Invoke();
}
- MyCommon.TraceOut("Stop:EndLoop");
}
-#region "IDisposable Support"
- private bool disposedValue; // 重複する呼び出しを検出するには
+ private bool disposed = false;
- // IDisposable
- protected virtual void Dispose(bool disposing)
+ public void Dispose()
{
- if (!this.disposedValue)
- {
- if (disposing)
- {
- _streamActive = false;
- if (_streamThread != null && _streamThread.IsAlive)
- {
- _streamThread.Abort();
- }
- }
- }
- this.disposedValue = true;
- }
+ if (this.disposed)
+ return;
- //protected Overrides void Finalize()
- //{
- // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
- // Dispose(false)
- // MyBase.Finalize()
- //}
+ this.disposed = true;
- // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
- public void Dispose()
- {
- // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-#endregion
+ this.Stop();
+ this.Started = null;
+ this.Stopped = null;
+ this.StatusArrived = null;
+ }
}
#endregion