OSDN Git Service

ツイート,DMの削除をTwitterApiクラスに移行
[opentween/open-tween.git] / OpenTween / Twitter.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 //           (c) 2008-2011 Moz (@syo68k)
4 //           (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 //           (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 //           (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 //           (c) 2011      Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 //           (c) 2013      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
10 //
11 // This file is part of OpenTween.
12 //
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
16 // any later version.
17 //
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21 // for more details.
22 //
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
27
28 using System.Diagnostics;
29 using System.IO;
30 using System.Linq;
31 using System.Net;
32 using System.Runtime.CompilerServices;
33 using System.Runtime.Serialization;
34 using System.Runtime.Serialization.Json;
35 using System.Text;
36 using System.Text.RegularExpressions;
37 using System.Threading;
38 using System.Threading.Tasks;
39 using System.Web;
40 using System.Xml;
41 using System.Xml.Linq;
42 using System.Xml.XPath;
43 using System;
44 using System.Reflection;
45 using System.Collections.Generic;
46 using System.Drawing;
47 using System.Windows.Forms;
48 using OpenTween.Api;
49 using OpenTween.Api.DataModel;
50 using OpenTween.Connection;
51
52 namespace OpenTween
53 {
54     public class Twitter : IDisposable
55     {
56         #region Regexp from twitter-text-js
57
58         // The code in this region code block incorporates works covered by
59         // the following copyright and permission notices:
60         //
61         //   Copyright 2011 Twitter, Inc.
62         //
63         //   Licensed under the Apache License, Version 2.0 (the "License"); you
64         //   may not use this work except in compliance with the License. You
65         //   may obtain a copy of the License in the LICENSE file, or at:
66         //
67         //   http://www.apache.org/licenses/LICENSE-2.0
68         //
69         //   Unless required by applicable law or agreed to in writing, software
70         //   distributed under the License is distributed on an "AS IS" BASIS,
71         //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
72         //   implied. See the License for the specific language governing
73         //   permissions and limitations under the License.
74
75         //Hashtag用正規表現
76         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";
77         private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
78         //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";
79         private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
80         private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
81         private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
82         private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
83         private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
84         public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
85         //URL正規表現
86         private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
87         public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
88         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";
89         private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]";
90         private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
91         private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
92         private const string url_valid_GTLD = @"(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))";
93         private const string url_valid_CCTLD = @"(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))";
94         private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)";
95         private const string url_valid_domain = @"(?<domain>" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")";
96         public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")";
97         public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$";
98         private const string url_valid_port_number = @"[0-9]+";
99
100         private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]";
101         private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))";
102         private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")";
103         private const string pth = "(?:" +
104             "(?:" +
105                 url_valid_general_path_chars + "*" +
106                 "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" +
107                 url_valid_path_ending_chars +
108                 ")|(?:@" + url_valid_general_path_chars + "+/)" +
109             ")";
110         private const string qry = @"(?<query>\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?";
111         public const string rgUrl = @"(?<before>" + url_valid_preceding_chars + ")" +
112                                     "(?<url>(?<protocol>https?://)?" +
113                                     "(?<domain>" + url_valid_domain + ")" +
114                                     "(?::" + url_valid_port_number + ")?" +
115                                     "(?<path>/" + pth + "*)?" +
116                                     qry +
117                                     ")";
118
119         #endregion
120
121         /// <summary>
122         /// Twitter API のステータスページのURL
123         /// </summary>
124         public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617";
125
126         /// <summary>
127         /// ツイートへのパーマリンクURLを判定する正規表現
128         /// </summary>
129         public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
130
131         /// <summary>
132         /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
133         /// </summary>
134         public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
135   favstar\.fm/users/[a-zA-Z0-9_]+/status/       # Favstar
136 | favstar\.fm/t/                                # Favstar (short)
137 | aclog\.koba789\.com/i/                        # aclog
138 | frtrt\.net/solo_status\.php\?status=          # RtRT
139 )(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
140
141         /// <summary>
142         /// DM送信かどうかを判定する正規表現
143         /// </summary>
144         public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
145
146         public TwitterConfiguration Configuration { get; private set; }
147
148         delegate void GetIconImageDelegate(PostClass post);
149         private readonly object LockObj = new object();
150         private ISet<long> followerId = new HashSet<long>();
151         private bool _GetFollowerResult = false;
152         private long[] noRTId = new long[0];
153         private bool _GetNoRetweetResult = false;
154
155         //プロパティからアクセスされる共通情報
156         private string _uname;
157
158         private bool _readOwnPost;
159         private List<string> _hashList = new List<string>();
160
161         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
162         private long minHomeTimeline = long.MaxValue;
163         private long minMentions = long.MaxValue;
164         private long minDirectmessage = long.MaxValue;
165         private long minDirectmessageSent = long.MaxValue;
166
167         //private FavoriteQueue favQueue;
168
169         private HttpTwitter twCon = new HttpTwitter();
170
171         //private List<PostClass> _deletemessages = new List<PostClass>();
172
173         public Twitter()
174         {
175             this.Configuration = TwitterConfiguration.DefaultConfiguration();
176         }
177
178         public TwitterApiAccessLevel AccessLevel
179         {
180             get
181             {
182                 return MyCommon.TwitterApiInfo.AccessLevel;
183             }
184         }
185
186         protected void ResetApiStatus()
187         {
188             MyCommon.TwitterApiInfo.Reset();
189         }
190
191         public void Authenticate(string username, string password)
192         {
193             this.ResetApiStatus();
194
195             HttpStatusCode res;
196             var content = "";
197             try
198             {
199                 res = twCon.AuthUserAndPass(username, password, ref content);
200             }
201             catch(Exception ex)
202             {
203                 throw new WebApiException("Err:" + ex.Message, ex);
204             }
205
206             this.CheckStatusCode(res, content);
207
208             _uname = username.ToLowerInvariant();
209             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
210         }
211
212         public string StartAuthentication()
213         {
214             //OAuth PIN Flow
215             this.ResetApiStatus();
216             try
217             {
218                 string pinPageUrl = null;
219                 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
220                 if (!res)
221                     throw new WebApiException("Err:Failed to access auth server.");
222
223                 return pinPageUrl;
224             }
225             catch (Exception ex)
226             {
227                 throw new WebApiException("Err:Failed to access auth server.", ex);
228             }
229         }
230
231         public void Authenticate(string pinCode)
232         {
233             this.ResetApiStatus();
234
235             HttpStatusCode res;
236             try
237             {
238                 res = twCon.AuthGetAccessToken(pinCode);
239             }
240             catch (Exception ex)
241             {
242                 throw new WebApiException("Err:Failed to access auth acc server.", ex);
243             }
244
245             this.CheckStatusCode(res, null);
246
247             _uname = Username.ToLowerInvariant();
248             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
249         }
250
251         public void ClearAuthInfo()
252         {
253             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
254             this.ResetApiStatus();
255             twCon.ClearAuthInfo();
256         }
257
258         public void VerifyCredentials()
259         {
260             HttpStatusCode res;
261             var content = "";
262             try
263             {
264                 res = twCon.VerifyCredentials(ref content);
265             }
266             catch (Exception ex)
267             {
268                 throw new WebApiException("Err:" + ex.Message, ex);
269             }
270
271             this.CheckStatusCode(res, content);
272
273             try
274             {
275                 var user = TwitterUser.ParseJson(content);
276
277                 this.twCon.AuthenticatedUserId = user.Id;
278                 this.UpdateUserStats(user);
279             }
280             catch (SerializationException ex)
281             {
282                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
283                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
284             }
285         }
286
287         public void Initialize(string token, string tokenSecret, string username, long userId)
288         {
289             //OAuth認証
290             if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
291             {
292                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
293             }
294             this.ResetApiStatus();
295             twCon.Initialize(token, tokenSecret, username, userId);
296             _uname = username.ToLowerInvariant();
297             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
298         }
299
300         public string PreProcessUrl(string orgData)
301         {
302             int posl1;
303             var posl2 = 0;
304             //var IDNConveter = new IdnMapping();
305             var href = "<a href=\"";
306
307             while (true)
308             {
309                 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
310                 {
311                     var urlStr = "";
312                     // IDN展開
313                     posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
314                     posl1 += href.Length;
315                     posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
316                     urlStr = orgData.Substring(posl1, posl2 - posl1);
317
318                     if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
319                         && !urlStr.StartsWith("https://", StringComparison.Ordinal)
320                         && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
321                     {
322                         continue;
323                     }
324
325                     var replacedUrl = MyCommon.IDNEncode(urlStr);
326                     if (replacedUrl == null) continue;
327                     if (replacedUrl == urlStr) continue;
328
329                     orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
330                     posl2 = 0;
331                 }
332                 else
333                 {
334                     break;
335                 }
336             }
337             return orgData;
338         }
339
340         private string GetPlainText(string orgData)
341         {
342             return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
343         }
344
345         // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
346
347         private string SanitizeHtml(string orgdata)
348         {
349             var retdata = orgdata;
350
351             retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
352                 "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
353
354             retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
355
356             return retdata;
357         }
358
359         private string AdjustHtml(string orgData)
360         {
361             var retStr = orgData;
362             //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
363             //while (m.Success)
364             //{
365             //    lock (LockObj)
366             //    {
367             //        _hashList.Add("#" + m.Groups(1).Value);
368             //    }
369             //    m = m.NextMatch;
370             //}
371             retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
372             retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
373             retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
374
375             //半角スペースを置換(Thanks @anis774)
376             var ret = false;
377             do
378             {
379                 ret = EscapeSpace(ref retStr);
380             } while (!ret);
381
382             return SanitizeHtml(retStr);
383         }
384
385         private bool EscapeSpace(ref string html)
386         {
387             //半角スペースを置換(Thanks @anis774)
388             var isTag = false;
389             for (int i = 0; i < html.Length; i++)
390             {
391                 if (html[i] == '<')
392                 {
393                     isTag = true;
394                 }
395                 if (html[i] == '>')
396                 {
397                     isTag = false;
398                 }
399
400                 if ((!isTag) && (html[i] == ' '))
401                 {
402                     html = html.Remove(i, 1);
403                     html = html.Insert(i, "&nbsp;");
404                     return false;
405                 }
406             }
407             return true;
408         }
409
410         private struct PostInfo
411         {
412             public string CreatedAt;
413             public string Id;
414             public string Text;
415             public string UserId;
416             public PostInfo(string Created, string IdStr, string txt, string uid)
417             {
418                 CreatedAt = Created;
419                 Id = IdStr;
420                 Text = txt;
421                 UserId = uid;
422             }
423             public bool Equals(PostInfo dst)
424             {
425                 if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
426                 {
427                     return true;
428                 }
429                 else
430                 {
431                     return false;
432                 }
433             }
434         }
435
436         static private PostInfo _prev = new PostInfo("", "", "", "");
437         private bool IsPostRestricted(TwitterStatus status)
438         {
439             var _current = new PostInfo("", "", "", "");
440
441             _current.CreatedAt = status.CreatedAt;
442             _current.Id = status.IdStr;
443             if (status.Text == null)
444             {
445                 _current.Text = "";
446             }
447             else
448             {
449                 _current.Text = status.Text;
450             }
451             _current.UserId = status.User.IdStr;
452
453             if (_current.Equals(_prev))
454             {
455                 return true;
456             }
457             _prev.CreatedAt = _current.CreatedAt;
458             _prev.Id = _current.Id;
459             _prev.Text = _current.Text;
460             _prev.UserId = _current.UserId;
461
462             return false;
463         }
464
465         public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
466         {
467             this.CheckAccountState();
468
469             if (mediaIds == null &&
470                 Twitter.DMSendTextRegex.IsMatch(postStr))
471             {
472                 SendDirectMessage(postStr);
473                 return;
474             }
475
476             HttpStatusCode res;
477             var content = "";
478             try
479             {
480                 res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
481             }
482             catch(Exception ex)
483             {
484                 throw new WebApiException("Err:" + ex.Message, ex);
485             }
486
487             // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
488             if (res == HttpStatusCode.NotFound)
489                 return;
490
491             this.CheckStatusCode(res, content);
492
493             TwitterStatus status;
494             try
495             {
496                 status = TwitterStatus.ParseJson(content);
497             }
498             catch(SerializationException ex)
499             {
500                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
501                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
502             }
503             catch(Exception ex)
504             {
505                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
506                 throw new WebApiException("Err:Invalid Json!", content, ex);
507             }
508
509             this.UpdateUserStats(status.User);
510
511             if (IsPostRestricted(status))
512             {
513                 throw new WebApiException("OK:Delaying?");
514             }
515         }
516
517         public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
518         {
519             this.CheckAccountState();
520
521             HttpStatusCode res;
522             var content = "";
523             try
524             {
525                 res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
526             }
527             catch(Exception ex)
528             {
529                 throw new WebApiException("Err:" + ex.Message, ex);
530             }
531
532             // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
533             if (res == HttpStatusCode.NotFound)
534                 return;
535
536             this.CheckStatusCode(res, content);
537
538             TwitterStatus status;
539             try
540             {
541                 status = TwitterStatus.ParseJson(content);
542             }
543             catch(SerializationException ex)
544             {
545                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
546                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
547             }
548             catch(Exception ex)
549             {
550                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
551                 throw new WebApiException("Err:Invalid Json!", content, ex);
552             }
553
554             this.UpdateUserStats(status.User);
555
556             if (IsPostRestricted(status))
557             {
558                 throw new WebApiException("OK:Delaying?");
559             }
560         }
561
562         public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
563         {
564             this.CheckAccountState();
565
566             if (Twitter.DMSendTextRegex.IsMatch(postStr))
567             {
568                 SendDirectMessage(postStr);
569                 return;
570             }
571
572             var mediaIds = new List<long>();
573
574             foreach (var item in mediaItems)
575             {
576                 var mediaId = UploadMedia(item);
577                 mediaIds.Add(mediaId);
578             }
579
580             if (mediaIds.Count == 0)
581                 throw new WebApiException("Err:Invalid Files!");
582
583             PostStatus(postStr, reply_to, mediaIds);
584         }
585
586         public long UploadMedia(IMediaItem item)
587         {
588             this.CheckAccountState();
589
590             HttpStatusCode res;
591             var content = "";
592             try
593             {
594                 res = twCon.UploadMedia(item, ref content);
595             }
596             catch (Exception ex)
597             {
598                 throw new WebApiException("Err:" + ex.Message, ex);
599             }
600
601             this.CheckStatusCode(res, content);
602
603             TwitterUploadMediaResult status;
604             try
605             {
606                 status = TwitterUploadMediaResult.ParseJson(content);
607             }
608             catch (SerializationException ex)
609             {
610                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
611                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
612             }
613             catch (Exception ex)
614             {
615                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
616                 throw new WebApiException("Err:Invalid Json!", content, ex);
617             }
618
619             return status.MediaId;
620         }
621
622         public void SendDirectMessage(string postStr)
623         {
624             this.CheckAccountState();
625             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
626
627             var mc = Twitter.DMSendTextRegex.Match(postStr);
628
629             HttpStatusCode res;
630             var content = "";
631             try
632             {
633                 res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
634             }
635             catch(Exception ex)
636             {
637                 throw new WebApiException("Err:" + ex.Message, ex);
638             }
639
640             this.CheckStatusCode(res, content);
641
642             TwitterDirectMessage status;
643             try
644             {
645                 status = TwitterDirectMessage.ParseJson(content);
646             }
647             catch(SerializationException ex)
648             {
649                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
650                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
651             }
652             catch(Exception ex)
653             {
654                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
655                 throw new WebApiException("Err:Invalid Json!", content, ex);
656             }
657
658             this.UpdateUserStats(status.Sender);
659         }
660
661         public void PostRetweet(long id, bool read)
662         {
663             this.CheckAccountState();
664
665             //データ部分の生成
666             var target = id;
667             var post = TabInformations.GetInstance()[id];
668             if (post == null)
669             {
670                 throw new WebApiException("Err:Target isn't found.");
671             }
672             if (TabInformations.GetInstance()[id].RetweetedId != null)
673             {
674                 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
675             }
676
677             HttpStatusCode res;
678             var content = "";
679             try
680             {
681                 res = twCon.RetweetStatus(target, ref content);
682             }
683             catch(Exception ex)
684             {
685                 throw new WebApiException("Err:" + ex.Message, ex);
686             }
687
688             this.CheckStatusCode(res, content);
689
690             TwitterStatus status;
691             try
692             {
693                 status = TwitterStatus.ParseJson(content);
694             }
695             catch(SerializationException ex)
696             {
697                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
698                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
699             }
700             catch(Exception ex)
701             {
702                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
703                 throw new WebApiException("Err:Invalid Json!", content, ex);
704             }
705
706             //ReTweetしたものをTLに追加
707             post = CreatePostsFromStatusData(status);
708             if (post == null)
709                 throw new WebApiException("Invalid Json!", content);
710
711             //二重取得回避
712             lock (LockObj)
713             {
714                 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
715                     return;
716             }
717             //Retweet判定
718             if (post.RetweetedId == null)
719                 throw new WebApiException("Invalid Json!", content);
720             //ユーザー情報
721             post.IsMe = true;
722
723             post.IsRead = read;
724             post.IsOwl = false;
725             if (_readOwnPost) post.IsRead = true;
726             post.IsDm = false;
727
728             TabInformations.GetInstance().AddPost(post);
729         }
730
731         public void PostCreateBlock(string screenName)
732         {
733             this.CheckAccountState();
734
735             HttpStatusCode res;
736             var content = "";
737             try
738             {
739                 res = twCon.CreateBlock(screenName, ref content);
740             }
741             catch(Exception ex)
742             {
743                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
744             }
745
746             this.CheckStatusCode(res, content);
747         }
748
749         public void PostDestroyBlock(string screenName)
750         {
751             this.CheckAccountState();
752
753             HttpStatusCode res;
754             var content = "";
755             try
756             {
757                 res = twCon.DestroyBlock(screenName, ref content);
758             }
759             catch(Exception ex)
760             {
761                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
762             }
763
764             this.CheckStatusCode(res, content);
765         }
766
767         public void PostReportSpam(string screenName)
768         {
769             this.CheckAccountState();
770
771             HttpStatusCode res;
772             var content = "";
773             try
774             {
775                 res = twCon.ReportSpam(screenName, ref content);
776             }
777             catch(Exception ex)
778             {
779                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
780             }
781
782             this.CheckStatusCode(res, content);
783         }
784
785         public TwitterUser GetUserInfo(string screenName)
786         {
787             this.CheckAccountState();
788
789             HttpStatusCode res;
790             var content = "";
791             try
792             {
793                 res = twCon.ShowUserInfo(screenName, ref content);
794             }
795             catch(Exception ex)
796             {
797                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
798             }
799
800             this.CheckStatusCode(res, content);
801
802             try
803             {
804                 return TwitterUser.ParseJson(content);
805             }
806             catch (SerializationException ex)
807             {
808                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
809                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
810             }
811             catch (Exception ex)
812             {
813                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
814                 throw new WebApiException("Err:Invalid Json!", content, ex);
815             }
816         }
817
818         public int GetStatus_Retweeted_Count(long StatusId)
819         {
820             this.CheckAccountState();
821
822             HttpStatusCode res;
823             var content = "";
824             try
825             {
826                 res = twCon.ShowStatuses(StatusId, ref content);
827             }
828             catch (Exception ex)
829             {
830                 throw new WebApiException("Err:" + ex.Message, ex);
831             }
832
833             this.CheckStatusCode(res, content);
834
835             try
836             {
837                 var status = TwitterStatus.ParseJson(content);
838                 return status.RetweetCount;
839             }
840             catch (SerializationException ex)
841             {
842                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
843                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
844             }
845             catch (Exception ex)
846             {
847                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
848                 throw new WebApiException("Invalid Json!", content, ex);
849             }
850         }
851
852         public void PostFavAdd(long id)
853         {
854             this.CheckAccountState();
855
856             //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
857
858             //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
859
860             HttpStatusCode res;
861             var content = "";
862             try
863             {
864                 res = twCon.CreateFavorites(id, ref content);
865             }
866             catch(Exception ex)
867             {
868                 //this.favQueue.Add(id)
869                 //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
870                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
871             }
872
873             this.CheckStatusCode(res, content);
874
875             if (!RestrictFavCheck)
876                 return;
877
878             //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
879
880             try
881             {
882                 res = twCon.ShowStatuses(id, ref content);
883             }
884             catch(Exception ex)
885             {
886                 throw new WebApiException("Err:" + ex.Message, ex);
887             }
888
889             this.CheckStatusCode(res, content);
890
891             TwitterStatus status;
892             try
893             {
894                 status = TwitterStatus.ParseJson(content);
895             }
896             catch (SerializationException ex)
897             {
898                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
899                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
900             }
901             catch (Exception ex)
902             {
903                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
904                 throw new WebApiException("Err:Invalid Json!", content, ex);
905             }
906             if (status.Favorited != true)
907                 throw new WebApiException("NG(Restricted?)");
908         }
909
910         public void PostFavRemove(long id)
911         {
912             this.CheckAccountState();
913
914             //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
915
916             //if (this.favQueue.Contains(id))
917             //    this.favQueue.Remove(id)
918             //    return "";
919             //}
920
921             HttpStatusCode res;
922             var content = "";
923             try
924             {
925                 res = twCon.DestroyFavorites(id, ref content);
926             }
927             catch(Exception ex)
928             {
929                 throw new WebApiException("Err:" + ex.Message, ex);
930             }
931
932             this.CheckStatusCode(res, content);
933         }
934
935         public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
936         {
937             this.CheckAccountState();
938
939             HttpStatusCode res;
940             var content = "";
941             try
942             {
943                 res = twCon.UpdateProfile(name, url, location, description, ref content);
944             }
945             catch(Exception ex)
946             {
947                 throw new WebApiException("Err:" + ex.Message, content, ex);
948             }
949
950             this.CheckStatusCode(res, content);
951
952             try
953             {
954                 return TwitterUser.ParseJson(content);
955             }
956             catch (SerializationException e)
957             {
958                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
959                 MyCommon.TraceOut(ex);
960                 throw ex;
961             }
962             catch (Exception e)
963             {
964                 var ex = new WebApiException("Err:Invalid Json!", content, e);
965                 MyCommon.TraceOut(ex);
966                 throw ex;
967             }
968         }
969
970         public void PostUpdateProfileImage(string filename)
971         {
972             this.CheckAccountState();
973
974             HttpStatusCode res;
975             var content = "";
976             try
977             {
978                 res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
979             }
980             catch(Exception ex)
981             {
982                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
983             }
984
985             this.CheckStatusCode(res, content);
986         }
987
988         public string Username
989         {
990             get
991             {
992                 return twCon.AuthenticatedUsername;
993             }
994         }
995
996         public long UserId
997         {
998             get
999             {
1000                 return twCon.AuthenticatedUserId;
1001             }
1002         }
1003
1004         public string Password
1005         {
1006             get
1007             {
1008                 return twCon.Password;
1009             }
1010         }
1011
1012         private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
1013         public static MyCommon.ACCOUNT_STATE AccountState
1014         {
1015             get
1016             {
1017                 return _accountState;
1018             }
1019             set
1020             {
1021                 _accountState = value;
1022             }
1023         }
1024
1025         public bool RestrictFavCheck { get; set; }
1026
1027 #region "バージョンアップ"
1028         public void GetTweenBinary(string strVer)
1029         {
1030             try
1031             {
1032                 //本体
1033                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1034                                                     Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1035                 {
1036                     throw new WebApiException("Err:Download failed");
1037                 }
1038                 //英語リソース
1039                 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1040                 {
1041                     Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1042                 }
1043                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1044                                                     Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1045                 {
1046                     throw new WebApiException("Err:Download failed");
1047                 }
1048                 //その他言語圏のリソース。取得失敗しても継続
1049                 //UIの言語圏のリソース
1050                 var curCul = "";
1051                 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1052                 {
1053                     var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1054                     if (idx > -1)
1055                     {
1056                         curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1057                     }
1058                     else
1059                     {
1060                         curCul = Thread.CurrentThread.CurrentUICulture.Name;
1061                     }
1062                 }
1063                 else
1064                 {
1065                     curCul = Thread.CurrentThread.CurrentUICulture.Name;
1066                 }
1067                 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1068                 {
1069                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1070                     {
1071                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1072                     }
1073                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1074                                                         Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1075                     {
1076                         //return "Err:Download failed";
1077                     }
1078                 }
1079                 //スレッドの言語圏のリソース
1080                 string curCul2;
1081                 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1082                 {
1083                     var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1084                     if (idx > -1)
1085                     {
1086                         curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1087                     }
1088                     else
1089                     {
1090                         curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1091                     }
1092                 }
1093                 else
1094                 {
1095                     curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1096                 }
1097                 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1098                 {
1099                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1100                     {
1101                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1102                     }
1103                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1104                                                     Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1105                     {
1106                         //return "Err:Download failed";
1107                     }
1108                 }
1109
1110                 //アップデータ
1111                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1112                                                     Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1113                 {
1114                     throw new WebApiException("Err:Download failed");
1115                 }
1116                 //シリアライザDLL
1117                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1118                                                     Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1119                 {
1120                     throw new WebApiException("Err:Download failed");
1121                 }
1122             }
1123             catch (Exception ex)
1124             {
1125                 throw new WebApiException("Err:Download failed", ex);
1126             }
1127         }
1128 #endregion
1129
1130         public bool ReadOwnPost
1131         {
1132             get
1133             {
1134                 return _readOwnPost;
1135             }
1136             set
1137             {
1138                 _readOwnPost = value;
1139             }
1140         }
1141
1142         public int FollowersCount { get; private set; }
1143         public int FriendsCount { get; private set; }
1144         public int StatusesCount { get; private set; }
1145         public string Location { get; private set; } = "";
1146         public string Bio { get; private set; } = "";
1147
1148         /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1149         private void UpdateUserStats(TwitterUser self)
1150         {
1151             this.FollowersCount = self.FollowersCount;
1152             this.FriendsCount = self.FriendsCount;
1153             this.StatusesCount = self.StatusesCount;
1154             this.Location = self.Location;
1155             this.Bio = self.Description;
1156         }
1157
1158         /// <summary>
1159         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1160         /// </summary>
1161         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1162         {
1163             return count >= 20 && count <= GetMaxApiResultCount(type);
1164         }
1165
1166         /// <summary>
1167         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1168         /// </summary>
1169         public static bool VerifyMoreApiResultCount(int count)
1170         {
1171             return count >= 20 && count <= 200;
1172         }
1173
1174         /// <summary>
1175         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1176         /// </summary>
1177         public static bool VerifyFirstApiResultCount(int count)
1178         {
1179             return count >= 20 && count <= 200;
1180         }
1181
1182         /// <summary>
1183         /// WORKERTYPEに応じた取得可能な最大件数を取得する
1184         /// </summary>
1185         public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1186         {
1187             // 参照: REST APIs - 各endpointのcountパラメータ
1188             // https://dev.twitter.com/rest/public
1189             switch (type)
1190             {
1191                 case MyCommon.WORKERTYPE.Timeline:
1192                 case MyCommon.WORKERTYPE.Reply:
1193                 case MyCommon.WORKERTYPE.UserTimeline:
1194                 case MyCommon.WORKERTYPE.Favorites:
1195                 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1196                 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1197                 case MyCommon.WORKERTYPE.List:  // 不明
1198                     return 200;
1199
1200                 case MyCommon.WORKERTYPE.PublicSearch:
1201                     return 100;
1202
1203                 default:
1204                     throw new InvalidOperationException("Invalid type: " + type);
1205             }
1206         }
1207
1208         /// <summary>
1209         /// WORKERTYPEに応じた取得件数を取得する
1210         /// </summary>
1211         public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1212         {
1213             if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1214                 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1215             {
1216                 return 20;
1217             }
1218
1219             if (SettingCommon.Instance.UseAdditionalCount)
1220             {
1221                 switch (type)
1222                 {
1223                     case MyCommon.WORKERTYPE.Favorites:
1224                         if (SettingCommon.Instance.FavoritesCountApi != 0)
1225                             return SettingCommon.Instance.FavoritesCountApi;
1226                         break;
1227                     case MyCommon.WORKERTYPE.List:
1228                         if (SettingCommon.Instance.ListCountApi != 0)
1229                             return SettingCommon.Instance.ListCountApi;
1230                         break;
1231                     case MyCommon.WORKERTYPE.PublicSearch:
1232                         if (SettingCommon.Instance.SearchCountApi != 0)
1233                             return SettingCommon.Instance.SearchCountApi;
1234                         break;
1235                     case MyCommon.WORKERTYPE.UserTimeline:
1236                         if (SettingCommon.Instance.UserTimelineCountApi != 0)
1237                             return SettingCommon.Instance.UserTimelineCountApi;
1238                         break;
1239                 }
1240                 if (more && SettingCommon.Instance.MoreCountApi != 0)
1241                 {
1242                     return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1243                 }
1244                 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1245                 {
1246                     return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1247                 }
1248             }
1249
1250             // 上記に当てはまらない場合の共通処理
1251             var count = SettingCommon.Instance.CountApi;
1252
1253             if (type == MyCommon.WORKERTYPE.Reply)
1254                 count = SettingCommon.Instance.CountApiReply;
1255
1256             return Math.Min(count, GetMaxApiResultCount(type));
1257         }
1258
1259         public void GetTimelineApi(bool read,
1260                                 MyCommon.WORKERTYPE gType,
1261                                 bool more,
1262                                 bool startup)
1263         {
1264             this.CheckAccountState();
1265
1266             HttpStatusCode res;
1267             var content = "";
1268             var count = GetApiResultCount(gType, more, startup);
1269
1270             try
1271             {
1272                 if (gType == MyCommon.WORKERTYPE.Timeline)
1273                 {
1274                     if (more)
1275                     {
1276                         res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1277                     }
1278                     else
1279                     {
1280                         res = twCon.HomeTimeline(count, null, null, ref content);
1281                     }
1282                 }
1283                 else
1284                 {
1285                     if (more)
1286                     {
1287                         res = twCon.Mentions(count, this.minMentions, null, ref content);
1288                     }
1289                     else
1290                     {
1291                         res = twCon.Mentions(count, null, null, ref content);
1292                     }
1293                 }
1294             }
1295             catch(Exception ex)
1296             {
1297                 throw new WebApiException("Err:" + ex.Message, ex);
1298             }
1299
1300             this.CheckStatusCode(res, content);
1301
1302             var minimumId = CreatePostsFromJson(content, gType, null, read);
1303
1304             if (minimumId != null)
1305             {
1306                 if (gType == MyCommon.WORKERTYPE.Timeline)
1307                     this.minHomeTimeline = minimumId.Value;
1308                 else
1309                     this.minMentions = minimumId.Value;
1310             }
1311         }
1312
1313         public void GetUserTimelineApi(bool read,
1314                                          string userName,
1315                                          TabClass tab,
1316                                          bool more)
1317         {
1318             this.CheckAccountState();
1319
1320             HttpStatusCode res;
1321             var content = "";
1322             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1323
1324             try
1325             {
1326                 if (string.IsNullOrEmpty(userName))
1327                 {
1328                     var target = tab.User;
1329                     if (string.IsNullOrEmpty(target)) return;
1330                     userName = target;
1331                     res = twCon.UserTimeline(null, target, count, null, null, ref content);
1332                 }
1333                 else
1334                 {
1335                     if (more)
1336                     {
1337                         res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1338                     }
1339                     else
1340                     {
1341                         res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1342                     }
1343                 }
1344             }
1345             catch(Exception ex)
1346             {
1347                 throw new WebApiException("Err:" + ex.Message, ex);
1348             }
1349
1350             if (res == HttpStatusCode.Unauthorized)
1351                 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1352
1353             this.CheckStatusCode(res, content);
1354
1355             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1356
1357             if (minimumId != null)
1358                 tab.OldestId = minimumId.Value;
1359         }
1360
1361         public PostClass GetStatusApi(bool read, long id)
1362         {
1363             this.CheckAccountState();
1364
1365             HttpStatusCode res;
1366             var content = "";
1367             try
1368             {
1369                 res = twCon.ShowStatuses(id, ref content);
1370             }
1371             catch(Exception ex)
1372             {
1373                 throw new WebApiException("Err:" + ex.Message, ex);
1374             }
1375
1376             if (res == HttpStatusCode.Forbidden)
1377                 throw new WebApiException("Err:protected user's tweet", content);
1378
1379             this.CheckStatusCode(res, content);
1380
1381             TwitterStatus status;
1382             try
1383             {
1384                 status = TwitterStatus.ParseJson(content);
1385             }
1386             catch(SerializationException ex)
1387             {
1388                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1389                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1390             }
1391             catch(Exception ex)
1392             {
1393                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1394                 throw new WebApiException("Invalid Json!", content, ex);
1395             }
1396
1397             var item = CreatePostsFromStatusData(status);
1398             if (item == null)
1399                 throw new WebApiException("Err:Can't create post", content);
1400
1401             item.IsRead = read;
1402             if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1403
1404             return item;
1405         }
1406
1407         public void GetStatusApi(bool read, long id, TabClass tab)
1408         {
1409             var post = this.GetStatusApi(read, id);
1410
1411             //非同期アイコン取得&StatusDictionaryに追加
1412             if (tab != null && tab.IsInnerStorageTabType)
1413                 tab.AddPostToInnerStorage(post);
1414             else
1415                 TabInformations.GetInstance().AddPost(post);
1416         }
1417
1418         private PostClass CreatePostsFromStatusData(TwitterStatus status)
1419         {
1420             return CreatePostsFromStatusData(status, false);
1421         }
1422
1423         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1424         {
1425             var post = new PostClass();
1426             TwitterEntities entities;
1427             string sourceHtml;
1428
1429             post.StatusId = status.Id;
1430             if (status.RetweetedStatus != null)
1431             {
1432                 var retweeted = status.RetweetedStatus;
1433
1434                 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1435
1436                 //Id
1437                 post.RetweetedId = retweeted.Id;
1438                 //本文
1439                 post.TextFromApi = retweeted.Text;
1440                 entities = retweeted.MergedEntities;
1441                 sourceHtml = retweeted.Source;
1442                 //Reply先
1443                 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1444                 post.InReplyToUser = retweeted.InReplyToScreenName;
1445                 post.InReplyToUserId = status.InReplyToUserId;
1446
1447                 if (favTweet)
1448                 {
1449                     post.IsFav = true;
1450                 }
1451                 else
1452                 {
1453                     //幻覚fav対策
1454                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1455                     post.IsFav = tc.Contains(retweeted.Id);
1456                 }
1457
1458                 if (retweeted.Coordinates != null)
1459                     post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1460
1461                 //以下、ユーザー情報
1462                 var user = retweeted.User;
1463
1464                 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1465
1466                 post.UserId = user.Id;
1467                 post.ScreenName = user.ScreenName;
1468                 post.Nickname = user.Name.Trim();
1469                 post.ImageUrl = user.ProfileImageUrlHttps;
1470                 post.IsProtect = user.Protected;
1471
1472                 //Retweetした人
1473                 post.RetweetedBy = status.User.ScreenName;
1474                 post.RetweetedByUserId = status.User.Id;
1475                 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
1476             }
1477             else
1478             {
1479                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1480                 //本文
1481                 post.TextFromApi = status.Text;
1482                 entities = status.MergedEntities;
1483                 sourceHtml = status.Source;
1484                 post.InReplyToStatusId = status.InReplyToStatusId;
1485                 post.InReplyToUser = status.InReplyToScreenName;
1486                 post.InReplyToUserId = status.InReplyToUserId;
1487
1488                 if (favTweet)
1489                 {
1490                     post.IsFav = true;
1491                 }
1492                 else
1493                 {
1494                     //幻覚fav対策
1495                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1496                     post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1497                 }
1498
1499                 if (status.Coordinates != null)
1500                     post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1501
1502                 //以下、ユーザー情報
1503                 var user = status.User;
1504
1505                 if (user == null || user.ScreenName == null) return null;
1506
1507                 post.UserId = user.Id;
1508                 post.ScreenName = user.ScreenName;
1509                 post.Nickname = user.Name.Trim();
1510                 post.ImageUrl = user.ProfileImageUrlHttps;
1511                 post.IsProtect = user.Protected;
1512                 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
1513             }
1514             //HTMLに整形
1515             string textFromApi = post.TextFromApi;
1516             post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1517             post.TextFromApi = textFromApi;
1518             post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1519             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1520             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1521
1522             post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1523                 .Where(x => x != post.StatusId && x != post.RetweetedId)
1524                 .Distinct().ToArray();
1525
1526             post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1527                 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1528                 .ToArray();
1529
1530             //Source整形
1531             var source = ParseSource(sourceHtml);
1532             post.Source = source.Item1;
1533             post.SourceUri = source.Item2;
1534
1535             post.IsReply = post.ReplyToList.Contains(_uname);
1536             post.IsExcludeReply = false;
1537
1538             if (post.IsMe)
1539             {
1540                 post.IsOwl = false;
1541             }
1542             else
1543             {
1544                 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1545             }
1546
1547             post.IsDm = false;
1548             return post;
1549         }
1550
1551         /// <summary>
1552         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1553         /// </summary>
1554         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1555         {
1556             var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1557
1558             return GetQuoteTweetStatusIds(urls);
1559         }
1560
1561         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1562         {
1563             foreach (var url in urls)
1564             {
1565                 var match = Twitter.StatusUrlRegex.Match(url);
1566                 if (match.Success)
1567                 {
1568                     long statusId;
1569                     if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1570                         yield return statusId;
1571                 }
1572             }
1573         }
1574
1575         private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1576         {
1577             TwitterStatus[] items;
1578             try
1579             {
1580                 items = TwitterStatus.ParseJsonArray(content);
1581             }
1582             catch(SerializationException ex)
1583             {
1584                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1585                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1586             }
1587             catch(Exception ex)
1588             {
1589                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1590                 throw new WebApiException("Invalid Json!", content, ex);
1591             }
1592
1593             long? minimumId = null;
1594
1595             foreach (var status in items)
1596             {
1597                 PostClass post = null;
1598                 post = CreatePostsFromStatusData(status);
1599                 if (post == null) continue;
1600
1601                 if (minimumId == null || minimumId.Value > post.StatusId)
1602                     minimumId = post.StatusId;
1603
1604                 //二重取得回避
1605                 lock (LockObj)
1606                 {
1607                     if (tab == null)
1608                     {
1609                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1610                     }
1611                     else
1612                     {
1613                         if (tab.Contains(post.StatusId)) continue;
1614                     }
1615                 }
1616
1617                 //RT禁止ユーザーによるもの
1618                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1619                     post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1620
1621                 post.IsRead = read;
1622                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1623
1624                 //非同期アイコン取得&StatusDictionaryに追加
1625                 if (tab != null && tab.IsInnerStorageTabType)
1626                     tab.AddPostToInnerStorage(post);
1627                 else
1628                     TabInformations.GetInstance().AddPost(post);
1629             }
1630
1631             return minimumId;
1632         }
1633
1634         private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1635         {
1636             TwitterSearchResult items;
1637             try
1638             {
1639                 items = TwitterSearchResult.ParseJson(content);
1640             }
1641             catch (SerializationException ex)
1642             {
1643                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1644                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1645             }
1646             catch (Exception ex)
1647             {
1648                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1649                 throw new WebApiException("Invalid Json!", content, ex);
1650             }
1651
1652             long? minimumId = null;
1653
1654             foreach (var result in items.Statuses)
1655             {
1656                 PostClass post = null;
1657                 post = CreatePostsFromStatusData(result);
1658
1659                 if (post == null)
1660                 {
1661                     // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1662                     try
1663                     {
1664                         post = this.GetStatusApi(read, result.Id);
1665                     }
1666                     catch (WebApiException)
1667                     {
1668                         continue;
1669                     }
1670                 }
1671
1672                 if (minimumId == null || minimumId.Value > post.StatusId)
1673                     minimumId = post.StatusId;
1674
1675                 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1676                 //二重取得回避
1677                 lock (LockObj)
1678                 {
1679                     if (tab == null)
1680                     {
1681                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1682                     }
1683                     else
1684                     {
1685                         if (tab.Contains(post.StatusId)) continue;
1686                     }
1687                 }
1688
1689                 post.IsRead = read;
1690                 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1691
1692                 //非同期アイコン取得&StatusDictionaryに追加
1693                 if (tab != null && tab.IsInnerStorageTabType)
1694                     tab.AddPostToInnerStorage(post);
1695                 else
1696                     TabInformations.GetInstance().AddPost(post);
1697             }
1698
1699             return minimumId;
1700         }
1701
1702         private void CreateFavoritePostsFromJson(string content, bool read)
1703         {
1704             TwitterStatus[] item;
1705             try
1706             {
1707                 item = TwitterStatus.ParseJsonArray(content);
1708             }
1709             catch (SerializationException ex)
1710             {
1711                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1712                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1713             }
1714             catch (Exception ex)
1715             {
1716                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1717                 throw new WebApiException("Invalid Json!", content, ex);
1718             }
1719
1720             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1721
1722             foreach (var status in item)
1723             {
1724                 //二重取得回避
1725                 lock (LockObj)
1726                 {
1727                     if (favTab.Contains(status.Id)) continue;
1728                 }
1729
1730                 var post = CreatePostsFromStatusData(status, true);
1731                 if (post == null) continue;
1732
1733                 post.IsRead = read;
1734
1735                 TabInformations.GetInstance().AddPost(post);
1736             }
1737         }
1738
1739         public void GetListStatus(bool read,
1740                                 TabClass tab,
1741                                 bool more,
1742                                 bool startup)
1743         {
1744             HttpStatusCode res;
1745             var content = "";
1746             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1747
1748             try
1749             {
1750                 if (more)
1751                 {
1752                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1753                 }
1754                 else
1755                 {
1756                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1757                 }
1758             }
1759             catch(Exception ex)
1760             {
1761                 throw new WebApiException("Err:" + ex.Message, ex);
1762             }
1763
1764             this.CheckStatusCode(res, content);
1765
1766             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1767
1768             if (minimumId != null)
1769                 tab.OldestId = minimumId.Value;
1770         }
1771
1772         /// <summary>
1773         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1774         /// </summary>
1775         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1776         internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1777         {
1778             if (!posts.ContainsKey(startStatusId))
1779                 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1780
1781             var nextPost = posts[startStatusId];
1782             while (nextPost.InReplyToStatusId != null)
1783             {
1784                 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1785                     break;
1786                 nextPost = posts[nextPost.InReplyToStatusId.Value];
1787             }
1788
1789             return nextPost;
1790         }
1791
1792         public void GetRelatedResult(bool read, TabClass tab)
1793         {
1794             var relPosts = new Dictionary<Int64, PostClass>();
1795             if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1796             {
1797                 //検索結果対応
1798                 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1799                 if (p != null && p.InReplyToStatusId != null)
1800                 {
1801                     tab.RelationTargetPost = p;
1802                 }
1803                 else
1804                 {
1805                     p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1806                     tab.RelationTargetPost = p;
1807                 }
1808             }
1809             relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1810
1811             Exception lastException = null;
1812
1813             // in_reply_to_status_id を使用してリプライチェインを辿る
1814             var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1815             var loopCount = 1;
1816             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1817             {
1818                 var inReplyToId = nextPost.InReplyToStatusId.Value;
1819
1820                 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1821                 if (inReplyToPost == null)
1822                 {
1823                     try
1824                     {
1825                         inReplyToPost = this.GetStatusApi(read, inReplyToId);
1826                     }
1827                     catch (WebApiException ex)
1828                     {
1829                         lastException = ex;
1830                         break;
1831                     }
1832                 }
1833
1834                 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1835
1836                 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1837             }
1838
1839             //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1840             var text = tab.RelationTargetPost.Text;
1841             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1842                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1843             foreach (var _match in ma)
1844             {
1845                 Int64 _statusId;
1846                 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1847                 {
1848                     if (relPosts.ContainsKey(_statusId))
1849                         continue;
1850
1851                     var p = TabInformations.GetInstance()[_statusId];
1852                     if (p == null)
1853                     {
1854                         try
1855                         {
1856                             p = this.GetStatusApi(read, _statusId);
1857                         }
1858                         catch (WebApiException ex)
1859                         {
1860                             lastException = ex;
1861                             break;
1862                         }
1863                     }
1864
1865                     if (p != null)
1866                         relPosts.Add(p.StatusId, p);
1867                 }
1868             }
1869
1870             relPosts.Values.ToList().ForEach(p =>
1871             {
1872                 if (p.IsMe && !read && this._readOwnPost)
1873                     p.IsRead = true;
1874                 else
1875                     p.IsRead = read;
1876
1877                 tab.AddPostToInnerStorage(p);
1878             });
1879
1880             if (lastException != null)
1881                 throw new WebApiException(lastException.Message, lastException);
1882         }
1883
1884         public void GetSearch(bool read,
1885                             TabClass tab,
1886                             bool more)
1887         {
1888             HttpStatusCode res;
1889             var content = "";
1890             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1891             long? maxId = null;
1892             long? sinceId = null;
1893             if (more)
1894             {
1895                 maxId = tab.OldestId - 1;
1896             }
1897             else
1898             {
1899                 sinceId = tab.SinceId;
1900             }
1901
1902             try
1903             {
1904                 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
1905                 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
1906             }
1907             catch(Exception ex)
1908             {
1909                 throw new WebApiException("Err:" + ex.Message, ex);
1910             }
1911             switch (res)
1912             {
1913                 case HttpStatusCode.BadRequest:
1914                     throw new WebApiException("Invalid query", content);
1915                 case HttpStatusCode.NotFound:
1916                     throw new WebApiException("Invalid query", content);
1917                 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
1918                     throw new WebApiException("Search API Limit?", content);
1919                 case HttpStatusCode.OK:
1920                     break;
1921                 default:
1922                     throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
1923             }
1924
1925             if (!TabInformations.GetInstance().ContainsTab(tab))
1926                 return;
1927
1928             var minimumId =  this.CreatePostsFromSearchJson(content, tab, read, count, more);
1929
1930             if (minimumId != null)
1931                 tab.OldestId = minimumId.Value;
1932         }
1933
1934         private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
1935         {
1936             TwitterDirectMessage[] item;
1937             try
1938             {
1939                 if (gType == MyCommon.WORKERTYPE.UserStream)
1940                 {
1941                     item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
1942                 }
1943                 else
1944                 {
1945                     item = TwitterDirectMessage.ParseJsonArray(content);
1946                 }
1947             }
1948             catch(SerializationException ex)
1949             {
1950                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1951                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1952             }
1953             catch(Exception ex)
1954             {
1955                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1956                 throw new WebApiException("Invalid Json!", content, ex);
1957             }
1958
1959             foreach (var message in item)
1960             {
1961                 var post = new PostClass();
1962                 try
1963                 {
1964                     post.StatusId = message.Id;
1965                     if (gType != MyCommon.WORKERTYPE.UserStream)
1966                     {
1967                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
1968                         {
1969                             if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
1970                         }
1971                         else
1972                         {
1973                             if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
1974                         }
1975                     }
1976
1977                     //二重取得回避
1978                     lock (LockObj)
1979                     {
1980                         if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
1981                     }
1982                     //sender_id
1983                     //recipient_id
1984                     post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
1985                     //本文
1986                     var textFromApi = message.Text;
1987                     //HTMLに整形
1988                     post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
1989                     post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
1990                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1991                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1992                     post.IsFav = false;
1993
1994                     post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
1995
1996                     post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
1997                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1998                         .ToArray();
1999
2000                     //以下、ユーザー情報
2001                     TwitterUser user;
2002                     if (gType == MyCommon.WORKERTYPE.UserStream)
2003                     {
2004                         if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2005                         {
2006                             user = message.Sender;
2007                             post.IsMe = false;
2008                             post.IsOwl = true;
2009                         }
2010                         else
2011                         {
2012                             user = message.Recipient;
2013                             post.IsMe = true;
2014                             post.IsOwl = false;
2015                         }
2016                     }
2017                     else
2018                     {
2019                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2020                         {
2021                             user = message.Sender;
2022                             post.IsMe = false;
2023                             post.IsOwl = true;
2024                         }
2025                         else
2026                         {
2027                             user = message.Recipient;
2028                             post.IsMe = true;
2029                             post.IsOwl = false;
2030                         }
2031                     }
2032
2033                     post.UserId = user.Id;
2034                     post.ScreenName = user.ScreenName;
2035                     post.Nickname = user.Name.Trim();
2036                     post.ImageUrl = user.ProfileImageUrlHttps;
2037                     post.IsProtect = user.Protected;
2038                 }
2039                 catch(Exception ex)
2040                 {
2041                     MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2042                     MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2043                     continue;
2044                 }
2045
2046                 post.IsRead = read;
2047                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2048                 post.IsReply = false;
2049                 post.IsExcludeReply = false;
2050                 post.IsDm = true;
2051
2052                 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2053                 dmTab.AddPostToInnerStorage(post);
2054             }
2055         }
2056
2057         public void GetDirectMessageApi(bool read,
2058                                 MyCommon.WORKERTYPE gType,
2059                                 bool more)
2060         {
2061             this.CheckAccountState();
2062             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2063
2064             HttpStatusCode res;
2065             var content = "";
2066             var count = GetApiResultCount(gType, more, false);
2067
2068             try
2069             {
2070                 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2071                 {
2072                     if (more)
2073                     {
2074                         res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2075                     }
2076                     else
2077                     {
2078                         res = twCon.DirectMessages(count, null, null, ref content);
2079                     }
2080                 }
2081                 else
2082                 {
2083                     if (more)
2084                     {
2085                         res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2086                     }
2087                     else
2088                     {
2089                         res = twCon.DirectMessagesSent(count, null, null, ref content);
2090                     }
2091                 }
2092             }
2093             catch(Exception ex)
2094             {
2095                 throw new WebApiException("Err:" + ex.Message, ex);
2096             }
2097
2098             this.CheckStatusCode(res, content);
2099
2100             CreateDirectMessagesFromJson(content, gType, read);
2101         }
2102
2103         public void GetFavoritesApi(bool read,
2104                             bool more)
2105         {
2106             this.CheckAccountState();
2107
2108             HttpStatusCode res;
2109             var content = "";
2110             var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2111
2112             try
2113             {
2114                 res = twCon.Favorites(count, ref content);
2115             }
2116             catch(Exception ex)
2117             {
2118                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2119             }
2120
2121             this.CheckStatusCode(res, content);
2122
2123             CreateFavoritePostsFromJson(content, read);
2124         }
2125
2126         private string ReplaceTextFromApi(string text, TwitterEntities entities)
2127         {
2128             if (entities != null)
2129             {
2130                 if (entities.Urls != null)
2131                 {
2132                     foreach (var m in entities.Urls)
2133                     {
2134                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2135                     }
2136                 }
2137                 if (entities.Media != null)
2138                 {
2139                     foreach (var m in entities.Media)
2140                     {
2141                         if (m.AltText != null)
2142                         {
2143                             text = text.Replace(m.Url, string.Format(Properties.Resources.ImageAltText, m.AltText));
2144                         }
2145                         else
2146                         {
2147                             if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2148                         }
2149                     }
2150                 }
2151             }
2152             return text;
2153         }
2154
2155         /// <summary>
2156         /// フォロワーIDを更新します
2157         /// </summary>
2158         /// <exception cref="WebApiException"/>
2159         public void RefreshFollowerIds()
2160         {
2161             if (MyCommon._endingFlag) return;
2162
2163             var cursor = -1L;
2164             var newFollowerIds = new HashSet<long>();
2165             do
2166             {
2167                 var ret = this.GetFollowerIdsApi(ref cursor);
2168                 newFollowerIds.UnionWith(ret.Ids);
2169                 cursor = ret.NextCursor;
2170             } while (cursor != 0);
2171
2172             this.followerId = newFollowerIds;
2173             TabInformations.GetInstance().RefreshOwl(this.followerId);
2174
2175             this._GetFollowerResult = true;
2176         }
2177
2178         public bool GetFollowersSuccess
2179         {
2180             get
2181             {
2182                 return _GetFollowerResult;
2183             }
2184         }
2185
2186         private TwitterIds GetFollowerIdsApi(ref long cursor)
2187         {
2188             this.CheckAccountState();
2189
2190             HttpStatusCode res;
2191             var content = "";
2192             try
2193             {
2194                 res = twCon.FollowerIds(cursor, ref content);
2195             }
2196             catch(Exception e)
2197             {
2198                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2199             }
2200
2201             this.CheckStatusCode(res, content);
2202
2203             try
2204             {
2205                 var ret = TwitterIds.ParseJson(content);
2206
2207                 if (ret.Ids == null)
2208                 {
2209                     var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2210                     MyCommon.ExceptionOut(ex);
2211                     throw ex;
2212                 }
2213
2214                 return ret;
2215             }
2216             catch(SerializationException e)
2217             {
2218                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2219                 MyCommon.TraceOut(ex);
2220                 throw ex;
2221             }
2222             catch(Exception e)
2223             {
2224                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2225                 MyCommon.TraceOut(ex);
2226                 throw ex;
2227             }
2228         }
2229
2230         /// <summary>
2231         /// RT 非表示ユーザーを更新します
2232         /// </summary>
2233         /// <exception cref="WebApiException"/>
2234         public void RefreshNoRetweetIds()
2235         {
2236             if (MyCommon._endingFlag) return;
2237
2238             this.noRTId = this.NoRetweetIdsApi();
2239
2240             this._GetNoRetweetResult = true;
2241         }
2242
2243         private long[] NoRetweetIdsApi()
2244         {
2245             this.CheckAccountState();
2246
2247             HttpStatusCode res;
2248             var content = "";
2249             try
2250             {
2251                 res = twCon.NoRetweetIds(ref content);
2252             }
2253             catch(Exception e)
2254             {
2255                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2256             }
2257
2258             this.CheckStatusCode(res, content);
2259
2260             try
2261             {
2262                 return MyCommon.CreateDataFromJson<long[]>(content);
2263             }
2264             catch(SerializationException e)
2265             {
2266                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2267                 MyCommon.TraceOut(ex);
2268                 throw ex;
2269             }
2270             catch(Exception e)
2271             {
2272                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2273                 MyCommon.TraceOut(ex);
2274                 throw ex;
2275             }
2276         }
2277
2278         public bool GetNoRetweetSuccess
2279         {
2280             get
2281             {
2282                 return _GetNoRetweetResult;
2283             }
2284         }
2285
2286         /// <summary>
2287         /// t.co の文字列長などの設定情報を更新します
2288         /// </summary>
2289         /// <exception cref="WebApiException"/>
2290         public void RefreshConfiguration()
2291         {
2292             this.Configuration = this.ConfigurationApi();
2293         }
2294
2295         private TwitterConfiguration ConfigurationApi()
2296         {
2297             HttpStatusCode res;
2298             var content = "";
2299             try
2300             {
2301                 res = twCon.GetConfiguration(ref content);
2302             }
2303             catch(Exception e)
2304             {
2305                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2306             }
2307
2308             this.CheckStatusCode(res, content);
2309
2310             try
2311             {
2312                 return TwitterConfiguration.ParseJson(content);
2313             }
2314             catch(SerializationException e)
2315             {
2316                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2317                 MyCommon.TraceOut(ex);
2318                 throw ex;
2319             }
2320             catch(Exception e)
2321             {
2322                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2323                 MyCommon.TraceOut(ex);
2324                 throw ex;
2325             }
2326         }
2327
2328         public void GetListsApi()
2329         {
2330             this.CheckAccountState();
2331
2332             HttpStatusCode res;
2333             IEnumerable<ListElement> lists;
2334             var content = "";
2335
2336             try
2337             {
2338                 res = twCon.GetLists(this.Username, ref content);
2339             }
2340             catch (Exception ex)
2341             {
2342                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2343             }
2344
2345             this.CheckStatusCode(res, content);
2346
2347             try
2348             {
2349                 lists = TwitterList.ParseJsonArray(content)
2350                     .Select(x => new ListElement(x, this));
2351             }
2352             catch (SerializationException ex)
2353             {
2354                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2355                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2356             }
2357             catch (Exception ex)
2358             {
2359                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2360                 throw new WebApiException("Err:Invalid Json!", content, ex);
2361             }
2362
2363             try
2364             {
2365                 res = twCon.GetListsSubscriptions(this.Username, ref content);
2366             }
2367             catch (Exception ex)
2368             {
2369                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2370             }
2371
2372             this.CheckStatusCode(res, content);
2373
2374             try
2375             {
2376                 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2377                     .Select(x => new ListElement(x, this)));
2378             }
2379             catch (SerializationException ex)
2380             {
2381                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2382                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2383             }
2384             catch (Exception ex)
2385             {
2386                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2387                 throw new WebApiException("Err:Invalid Json!", content, ex);
2388             }
2389
2390             TabInformations.GetInstance().SubscribableLists = lists.ToList();
2391         }
2392
2393         public void DeleteList(string list_id)
2394         {
2395             HttpStatusCode res;
2396             var content = "";
2397
2398             try
2399             {
2400                 res = twCon.DeleteListID(this.Username, list_id, ref content);
2401             }
2402             catch(Exception ex)
2403             {
2404                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2405             }
2406
2407             this.CheckStatusCode(res, content);
2408         }
2409
2410         public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2411         {
2412             HttpStatusCode res;
2413             var content = "";
2414
2415             try
2416             {
2417                 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2418             }
2419             catch(Exception ex)
2420             {
2421                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2422             }
2423
2424             this.CheckStatusCode(res, content);
2425
2426             try
2427             {
2428                 var le = TwitterList.ParseJson(content);
2429                 return  new ListElement(le, this);
2430             }
2431             catch(SerializationException ex)
2432             {
2433                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2434                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2435             }
2436             catch(Exception ex)
2437             {
2438                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2439                 throw new WebApiException("Err:Invalid Json!", content, ex);
2440             }
2441         }
2442
2443         public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2444         {
2445             this.CheckAccountState();
2446
2447             HttpStatusCode res;
2448             var content = "";
2449             try
2450             {
2451                 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2452             }
2453             catch(Exception ex)
2454             {
2455                 throw new WebApiException("Err:" + ex.Message);
2456             }
2457
2458             this.CheckStatusCode(res, content);
2459
2460             try
2461             {
2462                 var users = TwitterUsers.ParseJson(content);
2463                 Array.ForEach<TwitterUser>(
2464                     users.Users,
2465                     u => lists.Add(new UserInfo(u)));
2466
2467                 return users.NextCursor;
2468             }
2469             catch(SerializationException ex)
2470             {
2471                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2472                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2473             }
2474             catch(Exception ex)
2475             {
2476                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2477                 throw new WebApiException("Err:Invalid Json!", content, ex);
2478             }
2479         }
2480
2481         public void CreateListApi(string listName, bool isPrivate, string description)
2482         {
2483             this.CheckAccountState();
2484
2485             HttpStatusCode res;
2486             var content = "";
2487             try
2488             {
2489                 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2490             }
2491             catch(Exception ex)
2492             {
2493                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2494             }
2495
2496             this.CheckStatusCode(res, content);
2497
2498             try
2499             {
2500                 var le = TwitterList.ParseJson(content);
2501                 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2502             }
2503             catch(SerializationException ex)
2504             {
2505                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2506                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2507             }
2508             catch(Exception ex)
2509             {
2510                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2511                 throw new WebApiException("Err:Invalid Json!", content, ex);
2512             }
2513         }
2514
2515         public bool ContainsUserAtList(string listId, string user)
2516         {
2517             this.CheckAccountState();
2518
2519             HttpStatusCode res;
2520             var content = "";
2521
2522             try
2523             {
2524                 res = this.twCon.ShowListMember(listId, user, ref content);
2525             }
2526             catch(Exception ex)
2527             {
2528                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2529             }
2530
2531             if (res == HttpStatusCode.NotFound)
2532             {
2533                 return false;
2534             }
2535
2536             this.CheckStatusCode(res, content);
2537
2538             try
2539             {
2540                 TwitterUser.ParseJson(content);
2541                 return true;
2542             }
2543             catch(Exception)
2544             {
2545                 return false;
2546             }
2547         }
2548
2549         public void AddUserToList(string listId, string user)
2550         {
2551             HttpStatusCode res;
2552             var content = "";
2553
2554             try
2555             {
2556                 res = twCon.CreateListMembers(listId, user, ref content);
2557             }
2558             catch(Exception ex)
2559             {
2560                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2561             }
2562
2563             this.CheckStatusCode(res, content);
2564         }
2565
2566         public void RemoveUserToList(string listId, string user)
2567         {
2568             HttpStatusCode res;
2569             var content = "";
2570
2571             try
2572             {
2573                 res = twCon.DeleteListMembers(listId, user, ref content);
2574             }
2575             catch(Exception ex)
2576             {
2577                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2578             }
2579
2580             this.CheckStatusCode(res, content);
2581         }
2582
2583         public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2584         {
2585             if (entities != null)
2586             {
2587                 if (entities.Hashtags != null)
2588                 {
2589                     lock (this.LockObj)
2590                     {
2591                         this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2592                     }
2593                 }
2594                 if (entities.UserMentions != null)
2595                 {
2596                     foreach (var ent in entities.UserMentions)
2597                     {
2598                         var screenName = ent.ScreenName.ToLowerInvariant();
2599                         if (!AtList.Contains(screenName))
2600                             AtList.Add(screenName);
2601                     }
2602                 }
2603                 if (entities.Media != null)
2604                 {
2605                     if (media != null)
2606                     {
2607                         foreach (var ent in entities.Media)
2608                         {
2609                             if (!media.Any(x => x.Url == ent.MediaUrl))
2610                             {
2611                                 if (ent.VideoInfo != null &&
2612                                     ent.Type == "animated_gif" || ent.Type == "video")
2613                                 {
2614                                     //var videoUrl = ent.VideoInfo.Variants
2615                                     //    .Where(v => v.ContentType == "video/mp4")
2616                                     //    .OrderByDescending(v => v.Bitrate)
2617                                     //    .Select(v => v.Url).FirstOrDefault();
2618                                     media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
2619                                 }
2620                                 else
2621                                     media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
2622                             }
2623                         }
2624                     }
2625                 }
2626             }
2627
2628             // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2629             text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2630
2631             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>");
2632             text = PreProcessUrl(text); //IDN置換
2633
2634             return text;
2635         }
2636
2637         private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2638
2639         /// <summary>
2640         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2641         /// </summary>
2642         public static Tuple<string, Uri> ParseSource(string sourceHtml)
2643         {
2644             if (string.IsNullOrEmpty(sourceHtml))
2645                 return Tuple.Create<string, Uri>("", null);
2646
2647             string sourceText;
2648             Uri sourceUri;
2649
2650             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2651
2652             var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2653             if (match.Success)
2654             {
2655                 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2656                 try
2657                 {
2658                     var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2659                     sourceUri = new Uri(SourceUriBase, uriStr);
2660                 }
2661                 catch (UriFormatException)
2662                 {
2663                     sourceUri = null;
2664                 }
2665             }
2666             else
2667             {
2668                 sourceText = WebUtility.HtmlDecode(sourceHtml);
2669                 sourceUri = null;
2670             }
2671
2672             return Tuple.Create(sourceText, sourceUri);
2673         }
2674
2675         public TwitterApiStatus GetInfoApi()
2676         {
2677             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2678
2679             if (MyCommon._endingFlag) return null;
2680
2681             HttpStatusCode res;
2682             var content = "";
2683             try
2684             {
2685                 res = twCon.RateLimitStatus(ref content);
2686             }
2687             catch (Exception)
2688             {
2689                 this.ResetApiStatus();
2690                 return null;
2691             }
2692
2693             this.CheckStatusCode(res, content);
2694
2695             try
2696             {
2697                 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2698                 return MyCommon.TwitterApiInfo;
2699             }
2700             catch (Exception ex)
2701             {
2702                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2703                 MyCommon.TwitterApiInfo.Reset();
2704                 return null;
2705             }
2706         }
2707
2708         /// <summary>
2709         /// ブロック中のユーザーを更新します
2710         /// </summary>
2711         /// <exception cref="WebApiException"/>
2712         public void RefreshBlockIds()
2713         {
2714             if (MyCommon._endingFlag) return;
2715
2716             var cursor = -1L;
2717             var newBlockIds = new HashSet<long>();
2718             do
2719             {
2720                 var ret = this.GetBlockIdsApi(cursor);
2721                 newBlockIds.UnionWith(ret.Ids);
2722                 cursor = ret.NextCursor;
2723             } while (cursor != 0);
2724
2725             newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2726
2727             TabInformations.GetInstance().BlockIds = newBlockIds;
2728         }
2729
2730         public TwitterIds GetBlockIdsApi(long cursor)
2731         {
2732             this.CheckAccountState();
2733
2734             HttpStatusCode res;
2735             var content = "";
2736             try
2737             {
2738                 res = twCon.GetBlockUserIds(ref content, cursor);
2739             }
2740             catch(Exception e)
2741             {
2742                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2743             }
2744
2745             this.CheckStatusCode(res, content);
2746
2747             try
2748             {
2749                 return TwitterIds.ParseJson(content);
2750             }
2751             catch(SerializationException e)
2752             {
2753                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2754                 MyCommon.TraceOut(ex);
2755                 throw ex;
2756             }
2757             catch(Exception e)
2758             {
2759                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2760                 MyCommon.TraceOut(ex);
2761                 throw ex;
2762             }
2763         }
2764
2765         /// <summary>
2766         /// ミュート中のユーザーIDを更新します
2767         /// </summary>
2768         /// <exception cref="WebApiException"/>
2769         public async Task RefreshMuteUserIdsAsync()
2770         {
2771             if (MyCommon._endingFlag) return;
2772
2773             var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2774                 .ConfigureAwait(false);
2775
2776             TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2777         }
2778
2779         public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2780         {
2781             var content = "";
2782
2783             try
2784             {
2785                 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2786                     .ConfigureAwait(false);
2787
2788                 this.CheckStatusCode(res, content);
2789
2790                 return TwitterIds.ParseJson(content);
2791             }
2792             catch (WebException ex)
2793             {
2794                 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2795                 MyCommon.TraceOut(ex2);
2796                 throw ex2;
2797             }
2798             catch (SerializationException ex)
2799             {
2800                 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2801                 MyCommon.TraceOut(ex2);
2802                 throw ex2;
2803             }
2804         }
2805
2806         public string[] GetHashList()
2807         {
2808             string[] hashArray;
2809             lock (LockObj)
2810             {
2811                 hashArray = _hashList.ToArray();
2812                 _hashList.Clear();
2813             }
2814             return hashArray;
2815         }
2816
2817         public string AccessToken
2818         {
2819             get
2820             {
2821                 return twCon.AccessToken;
2822             }
2823         }
2824
2825         public string AccessTokenSecret
2826         {
2827             get
2828             {
2829                 return twCon.AccessTokenSecret;
2830             }
2831         }
2832
2833         private void CheckAccountState()
2834         {
2835             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2836                 throw new WebApiException("Auth error. Check your account");
2837         }
2838
2839         private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2840         {
2841             if (!this.AccessLevel.HasFlag(accessLevelFlags))
2842                 throw new WebApiException("Auth Err:try to re-authorization.");
2843         }
2844
2845         private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2846             [CallerMemberName] string callerMethodName = "")
2847         {
2848             if (httpStatus == HttpStatusCode.OK)
2849             {
2850                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2851                 return;
2852             }
2853
2854             if (string.IsNullOrWhiteSpace(responseText))
2855             {
2856                 if (httpStatus == HttpStatusCode.Unauthorized)
2857                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2858
2859                 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2860             }
2861
2862             try
2863             {
2864                 var errors = TwitterError.ParseJson(responseText).Errors;
2865                 if (errors == null || !errors.Any())
2866                 {
2867                     throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2868                 }
2869
2870                 foreach (var error in errors)
2871                 {
2872                     if (error.Code == TwitterErrorCode.InvalidToken ||
2873                         error.Code == TwitterErrorCode.SuspendedAccount)
2874                     {
2875                         Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2876                     }
2877                 }
2878
2879                 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2880             }
2881             catch (SerializationException) { }
2882
2883             throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2884         }
2885
2886         public int GetTextLengthRemain(string postText)
2887         {
2888             var matchDm = Twitter.DMSendTextRegex.Match(postText);
2889             if (matchDm.Success)
2890                 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2891
2892             return this.GetTextLengthRemainInternal(postText, isDm: false);
2893         }
2894
2895         private int GetTextLengthRemainInternal(string postText, bool isDm)
2896         {
2897             var textLength = 0;
2898
2899             var pos = 0;
2900             while (pos < postText.Length)
2901             {
2902                 textLength++;
2903
2904                 if (char.IsSurrogatePair(postText, pos))
2905                     pos += 2; // サロゲートペアの場合は2文字分進める
2906                 else
2907                     pos++;
2908             }
2909
2910             var urls = TweetExtractor.ExtractUrls(postText);
2911             foreach (var url in urls)
2912             {
2913                 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
2914                     ? this.Configuration.ShortUrlLengthHttps
2915                     : this.Configuration.ShortUrlLength;
2916
2917                 textLength += shortUrlLength - url.Length;
2918             }
2919
2920             if (isDm)
2921                 return this.Configuration.DmTextCharacterLimit - textLength;
2922             else
2923                 return 140 - textLength;
2924         }
2925
2926
2927 #region "UserStream"
2928         private string trackWord_ = "";
2929         public string TrackWord
2930         {
2931             get
2932             {
2933                 return trackWord_;
2934             }
2935             set
2936             {
2937                 trackWord_ = value;
2938             }
2939         }
2940         private bool allAtReply_ = false;
2941         public bool AllAtReply
2942         {
2943             get
2944             {
2945                 return allAtReply_;
2946             }
2947             set
2948             {
2949                 allAtReply_ = value;
2950             }
2951         }
2952
2953         public event EventHandler NewPostFromStream;
2954         public event EventHandler UserStreamStarted;
2955         public event EventHandler UserStreamStopped;
2956         public event EventHandler<PostDeletedEventArgs> PostDeleted;
2957         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
2958         private DateTime _lastUserstreamDataReceived;
2959         private TwitterUserstream userStream;
2960
2961         public class FormattedEvent
2962         {
2963             public MyCommon.EVENTTYPE Eventtype { get; set; }
2964             public DateTime CreatedAt { get; set; }
2965             public string Event { get; set; }
2966             public string Username { get; set; }
2967             public string Target { get; set; }
2968             public Int64 Id { get; set; }
2969             public bool IsMe { get; set; }
2970         }
2971
2972         public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
2973         public List<FormattedEvent> StoredEvent
2974         {
2975             get
2976             {
2977                 return storedEvent_;
2978             }
2979             set
2980             {
2981                 storedEvent_ = value;
2982             }
2983         }
2984
2985         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
2986         {
2987             ["favorite"] = MyCommon.EVENTTYPE.Favorite,
2988             ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
2989             ["follow"] = MyCommon.EVENTTYPE.Follow,
2990             ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
2991             ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
2992             ["block"] = MyCommon.EVENTTYPE.Block,
2993             ["unblock"] = MyCommon.EVENTTYPE.Unblock,
2994             ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
2995             ["deleted"] = MyCommon.EVENTTYPE.Deleted,
2996             ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
2997             ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
2998             ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
2999             ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3000             ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3001             ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3002             ["mute"] = MyCommon.EVENTTYPE.Mute,
3003             ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3004             ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3005         };
3006
3007         public bool IsUserstreamDataReceived
3008         {
3009             get
3010             {
3011                 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3012             }
3013         }
3014
3015         private void userStream_StatusArrived(string line)
3016         {
3017             this._lastUserstreamDataReceived = DateTime.Now;
3018             if (string.IsNullOrEmpty(line)) return;
3019
3020             if (line.First() != '{' || line.Last() != '}')
3021             {
3022                 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3023                 return;
3024             }
3025
3026             var isDm = false;
3027
3028             try
3029             {
3030                 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3031                 {
3032                     var xElm = XElement.Load(jsonReader);
3033                     if (xElm.Element("friends") != null)
3034                     {
3035                         Debug.WriteLine("friends");
3036                         return;
3037                     }
3038                     else if (xElm.Element("delete") != null)
3039                     {
3040                         Debug.WriteLine("delete");
3041                         Int64 id;
3042                         XElement idElm;
3043                         if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3044                         {
3045                             id = 0;
3046                             long.TryParse(idElm.Value, out id);
3047
3048                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3049                         }
3050                         else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3051                         {
3052                             id = 0;
3053                             long.TryParse(idElm.Value, out id);
3054
3055                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3056                         }
3057                         else
3058                         {
3059                             MyCommon.TraceOut("delete:" + line);
3060                             return;
3061                         }
3062                         for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3063                         {
3064                             var sEvt = this.StoredEvent[i];
3065                             if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3066                             {
3067                                 this.StoredEvent.RemoveAt(i);
3068                             }
3069                         }
3070                         return;
3071                     }
3072                     else if (xElm.Element("limit") != null)
3073                     {
3074                         Debug.WriteLine(line);
3075                         return;
3076                     }
3077                     else if (xElm.Element("event") != null)
3078                     {
3079                         Debug.WriteLine("event: " + xElm.Element("event").Value);
3080                         CreateEventFromJson(line);
3081                         return;
3082                     }
3083                     else if (xElm.Element("direct_message") != null)
3084                     {
3085                         Debug.WriteLine("direct_message");
3086                         isDm = true;
3087                     }
3088                     else if (xElm.Element("retweeted_status") != null)
3089                     {
3090                         var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3091                         var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3092
3093                         // 自分に関係しないリツイートの場合は無視する
3094                         var selfUserId = this.UserId.ToString();
3095                         if (sourceUserId == selfUserId || targetUserId == selfUserId)
3096                         {
3097                             // 公式 RT をイベントとしても扱う
3098                             var evt = CreateEventFromRetweet(xElm);
3099                             if (evt != null)
3100                             {
3101                                 this.StoredEvent.Insert(0, evt);
3102
3103                                 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3104                             }
3105                         }
3106
3107                         // 従来通り公式 RT の表示も行うため return しない
3108                     }
3109                     else if (xElm.Element("scrub_geo") != null)
3110                     {
3111                         try
3112                         {
3113                             TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3114                                                                         long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3115                         }
3116                         catch(Exception)
3117                         {
3118                             MyCommon.TraceOut("scrub_geo:" + line);
3119                         }
3120                         return;
3121                     }
3122                 }
3123
3124                 if (isDm)
3125                 {
3126                     CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3127                 }
3128                 else
3129                 {
3130                     CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3131                 }
3132             }
3133             catch (WebApiException ex)
3134             {
3135                 MyCommon.TraceOut(ex);
3136                 return;
3137             }
3138             catch(NullReferenceException)
3139             {
3140                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3141             }
3142
3143             this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3144         }
3145
3146         /// <summary>
3147         /// UserStreamsから受信した公式RTをイベントに変換します
3148         /// </summary>
3149         private FormattedEvent CreateEventFromRetweet(XElement xElm)
3150         {
3151             return new FormattedEvent
3152             {
3153                 Eventtype = MyCommon.EVENTTYPE.Retweet,
3154                 Event = "retweet",
3155                 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3156                 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3157                 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3158                 Target = string.Format("@{0}:{1}", new[]
3159                 {
3160                     xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3161                     WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3162                 }),
3163                 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3164             };
3165         }
3166
3167         private void CreateEventFromJson(string content)
3168         {
3169             TwitterStreamEvent eventData = null;
3170             try
3171             {
3172                 eventData = TwitterStreamEvent.ParseJson(content);
3173             }
3174             catch(SerializationException ex)
3175             {
3176                 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3177             }
3178             catch(Exception ex)
3179             {
3180                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3181             }
3182
3183             var evt = new FormattedEvent();
3184             evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3185             evt.Event = eventData.Event;
3186             evt.Username = eventData.Source.ScreenName;
3187             evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3188
3189             MyCommon.EVENTTYPE eventType;
3190             eventTable.TryGetValue(eventData.Event, out eventType);
3191             evt.Eventtype = eventType;
3192
3193             TwitterStreamEvent<TwitterStatus> tweetEvent;
3194
3195             switch (eventData.Event)
3196             {
3197                 case "access_revoked":
3198                 case "access_unrevoked":
3199                 case "user_delete":
3200                 case "user_suspend":
3201                     return;
3202                 case "follow":
3203                     if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3204                     {
3205                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3206                     }
3207                     else
3208                     {
3209                         return;    //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3210                     }
3211                     evt.Target = "";
3212                     break;
3213                 case "unfollow":
3214                     evt.Target = "@" + eventData.Target.ScreenName;
3215                     break;
3216                 case "favorited_retweet":
3217                 case "retweeted_retweet":
3218                     return;
3219                 case "favorite":
3220                 case "unfavorite":
3221                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3222                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3223                     evt.Id = tweetEvent.TargetObject.Id;
3224
3225                     if (SettingCommon.Instance.IsRemoveSameEvent)
3226                     {
3227                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3228                             return;
3229                     }
3230
3231                     var tabinfo = TabInformations.GetInstance();
3232
3233                     PostClass post;
3234                     var statusId = tweetEvent.TargetObject.Id;
3235                     if (!tabinfo.Posts.TryGetValue(statusId, out post))
3236                         break;
3237
3238                     if (eventData.Event == "favorite")
3239                     {
3240                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3241                         if (!favTab.Contains(post.StatusId))
3242                             favTab.AddPostImmediately(post.StatusId, post.IsRead);
3243
3244                         if (tweetEvent.Source.Id == this.UserId)
3245                         {
3246                             post.IsFav = true;
3247                         }
3248                         else if (tweetEvent.Target.Id == this.UserId)
3249                         {
3250                             post.FavoritedCount++;
3251
3252                             if (SettingCommon.Instance.FavEventUnread)
3253                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
3254                         }
3255                     }
3256                     else // unfavorite
3257                     {
3258                         if (tweetEvent.Source.Id == this.UserId)
3259                         {
3260                             post.IsFav = false;
3261                         }
3262                         else if (tweetEvent.Target.Id == this.UserId)
3263                         {
3264                             post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3265                         }
3266                     }
3267                     break;
3268                 case "quoted_tweet":
3269                     if (evt.IsMe) return;
3270
3271                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3272                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3273                     evt.Id = tweetEvent.TargetObject.Id;
3274
3275                     if (SettingCommon.Instance.IsRemoveSameEvent)
3276                     {
3277                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3278                             return;
3279                     }
3280                     break;
3281                 case "list_member_added":
3282                 case "list_member_removed":
3283                 case "list_created":
3284                 case "list_destroyed":
3285                 case "list_updated":
3286                 case "list_user_subscribed":
3287                 case "list_user_unsubscribed":
3288                     var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3289                     evt.Target = listEvent.TargetObject.FullName;
3290                     break;
3291                 case "block":
3292                     if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3293                     evt.Target = "";
3294                     break;
3295                 case "unblock":
3296                     if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3297                     evt.Target = "";
3298                     break;
3299                 case "user_update":
3300                     evt.Target = "";
3301                     break;
3302                 
3303                 // Mute / Unmute
3304                 case "mute":
3305                     evt.Target = "@" + eventData.Target.ScreenName;
3306                     if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3307                     {
3308                         TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3309                     }
3310                     break;
3311                 case "unmute":
3312                     evt.Target = "@" + eventData.Target.ScreenName;
3313                     if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3314                     {
3315                         TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3316                     }
3317                     break;
3318
3319                 default:
3320                     MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3321                     break;
3322             }
3323             this.StoredEvent.Insert(0, evt);
3324
3325             this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3326         }
3327
3328         private void userStream_Started()
3329         {
3330             this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3331         }
3332
3333         private void userStream_Stopped()
3334         {
3335             this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3336         }
3337
3338         public bool UserStreamEnabled
3339         {
3340             get
3341             {
3342                 return userStream == null ? false : userStream.Enabled;
3343             }
3344         }
3345
3346         public void StartUserStream()
3347         {
3348             if (userStream != null)
3349             {
3350                 StopUserStream();
3351             }
3352             userStream = new TwitterUserstream(twCon);
3353             userStream.StatusArrived += userStream_StatusArrived;
3354             userStream.Started += userStream_Started;
3355             userStream.Stopped += userStream_Stopped;
3356             userStream.Start(this.AllAtReply, this.TrackWord);
3357         }
3358
3359         public void StopUserStream()
3360         {
3361             userStream?.Dispose();
3362             userStream = null;
3363             if (!MyCommon._endingFlag)
3364             {
3365                 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3366             }
3367         }
3368
3369         public void ReconnectUserStream()
3370         {
3371             if (userStream != null)
3372             {
3373                 this.StartUserStream();
3374             }
3375         }
3376
3377         private class TwitterUserstream : IDisposable
3378         {
3379             public event Action<string> StatusArrived;
3380             public event Action Stopped;
3381             public event Action Started;
3382             private HttpTwitter twCon;
3383
3384             private Thread _streamThread;
3385             private bool _streamActive;
3386
3387             private bool _allAtreplies = false;
3388             private string _trackwords = "";
3389
3390             public TwitterUserstream(HttpTwitter twitterConnection)
3391             {
3392                 twCon = (HttpTwitter)twitterConnection.Clone();
3393             }
3394
3395             public void Start(bool allAtReplies, string trackwords)
3396             {
3397                 this.AllAtReplies = allAtReplies;
3398                 this.TrackWords = trackwords;
3399                 _streamActive = true;
3400                 if (_streamThread != null && _streamThread.IsAlive) return;
3401                 _streamThread = new Thread(UserStreamLoop);
3402                 _streamThread.Name = "UserStreamReceiver";
3403                 _streamThread.IsBackground = true;
3404                 _streamThread.Start();
3405             }
3406
3407             public bool Enabled
3408             {
3409                 get
3410                 {
3411                     return _streamActive;
3412                 }
3413             }
3414
3415             public bool AllAtReplies
3416             {
3417                 get
3418                 {
3419                     return _allAtreplies;
3420                 }
3421                 set
3422                 {
3423                     _allAtreplies = value;
3424                 }
3425             }
3426
3427             public string TrackWords
3428             {
3429                 get
3430                 {
3431                     return _trackwords;
3432                 }
3433                 set
3434                 {
3435                     _trackwords = value;
3436                 }
3437             }
3438
3439             private void UserStreamLoop()
3440             {
3441                 var sleepSec = 0;
3442                 do
3443                 {
3444                     Stream st = null;
3445                     StreamReader sr = null;
3446                     try
3447                     {
3448                         if (!MyCommon.IsNetworkAvailable())
3449                         {
3450                             sleepSec = 30;
3451                             continue;
3452                         }
3453
3454                         Started?.Invoke();
3455
3456                         var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3457
3458                         switch (res)
3459                         {
3460                             case HttpStatusCode.OK:
3461                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3462                                 break;
3463                             case HttpStatusCode.Unauthorized:
3464                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3465                                 sleepSec = 120;
3466                                 continue;
3467                         }
3468
3469                         if (st == null)
3470                         {
3471                             sleepSec = 30;
3472                             //MyCommon.TraceOut("Stop:stream is null")
3473                             continue;
3474                         }
3475
3476                         sr = new StreamReader(st);
3477
3478                         while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3479                         {
3480                             StatusArrived?.Invoke(sr.ReadLine());
3481                             //this.LastTime = Now;
3482                         }
3483
3484                         if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3485                         {
3486                             sleepSec = 30;
3487                             //MyCommon.TraceOut("Stop:EndOfStream")
3488                             continue;
3489                         }
3490                         break;
3491                     }
3492                     catch(WebException ex)
3493                     {
3494                         if (ex.Status == WebExceptionStatus.Timeout)
3495                         {
3496                             sleepSec = 30;                        //MyCommon.TraceOut("Stop:Timeout")
3497                         }
3498                         else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3499                         {
3500                             //MyCommon.TraceOut("Stop:Connection Limit")
3501                             break;
3502                         }
3503                         else
3504                         {
3505                             sleepSec = 30;
3506                             //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3507                         }
3508                     }
3509                     catch(ThreadAbortException)
3510                     {
3511                         break;
3512                     }
3513                     catch(IOException)
3514                     {
3515                         sleepSec = 30;
3516                         //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3517                     }
3518                     catch(ArgumentException ex)
3519                     {
3520                         //System.ArgumentException: ストリームを読み取れませんでした。
3521                         //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3522                         sleepSec = 30;
3523                         MyCommon.TraceOut(ex, "Stop:ArgumentException");
3524                     }
3525                     catch(Exception ex)
3526                     {
3527                         MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3528                         MyCommon.ExceptionOut(ex);
3529                         sleepSec = 30;
3530                     }
3531                     finally
3532                     {
3533                         if (_streamActive)
3534                         {
3535                             Stopped?.Invoke();
3536                         }
3537                         twCon.RequestAbort();
3538                         sr?.Close();
3539                         if (sleepSec > 0)
3540                         {
3541                             var ms = 0;
3542                             while (_streamActive && ms < sleepSec * 1000)
3543                             {
3544                                 Thread.Sleep(500);
3545                                 ms += 500;
3546                             }
3547                         }
3548                         sleepSec = 0;
3549                     }
3550                 } while (this._streamActive);
3551
3552                 if (_streamActive)
3553                 {
3554                     Stopped?.Invoke();
3555                 }
3556                 MyCommon.TraceOut("Stop:EndLoop");
3557             }
3558
3559 #region "IDisposable Support"
3560             private bool disposedValue; // 重複する呼び出しを検出するには
3561
3562             // IDisposable
3563             protected virtual void Dispose(bool disposing)
3564             {
3565                 if (!this.disposedValue)
3566                 {
3567                     if (disposing)
3568                     {
3569                         _streamActive = false;
3570                         if (_streamThread != null && _streamThread.IsAlive)
3571                         {
3572                             _streamThread.Abort();
3573                         }
3574                     }
3575                 }
3576                 this.disposedValue = true;
3577             }
3578
3579             //protected Overrides void Finalize()
3580             //{
3581             //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3582             //    Dispose(false)
3583             //    MyBase.Finalize()
3584             //}
3585
3586             // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3587             public void Dispose()
3588             {
3589                 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3590                 Dispose(true);
3591                 GC.SuppressFinalize(this);
3592             }
3593 #endregion
3594
3595         }
3596 #endregion
3597
3598 #region "IDisposable Support"
3599         private bool disposedValue; // 重複する呼び出しを検出するには
3600
3601         // IDisposable
3602         protected virtual void Dispose(bool disposing)
3603         {
3604             if (!this.disposedValue)
3605             {
3606                 if (disposing)
3607                 {
3608                     this.StopUserStream();
3609                 }
3610             }
3611             this.disposedValue = true;
3612         }
3613
3614         //protected Overrides void Finalize()
3615         //{
3616         //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3617         //    Dispose(false)
3618         //    MyBase.Finalize()
3619         //}
3620
3621         // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3622         public void Dispose()
3623         {
3624             // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3625             Dispose(true);
3626             GC.SuppressFinalize(this);
3627         }
3628 #endregion
3629     }
3630
3631     public class PostDeletedEventArgs : EventArgs
3632     {
3633         public long StatusId { get; }
3634
3635         public PostDeletedEventArgs(long statusId)
3636         {
3637             this.StatusId = statusId;
3638         }
3639     }
3640
3641     public class UserStreamEventReceivedEventArgs : EventArgs
3642     {
3643         public Twitter.FormattedEvent EventData { get; }
3644
3645         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3646         {
3647             this.EventData = eventData;
3648         }
3649     }
3650 }