OSDN Git Service

ツイートに添付された画像の ext_alt_text の受信に対応
[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 RemoveStatus(long id)
662         {
663             this.CheckAccountState();
664
665             HttpStatusCode res;
666             try
667             {
668                 res = twCon.DestroyStatus(id);
669             }
670             catch(Exception ex)
671             {
672                 throw new WebApiException("Err:" + ex.Message, ex);
673             }
674
675             this.CheckStatusCode(res, null);
676         }
677
678         public void PostRetweet(long id, bool read)
679         {
680             this.CheckAccountState();
681
682             //データ部分の生成
683             var target = id;
684             var post = TabInformations.GetInstance()[id];
685             if (post == null)
686             {
687                 throw new WebApiException("Err:Target isn't found.");
688             }
689             if (TabInformations.GetInstance()[id].RetweetedId != null)
690             {
691                 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
692             }
693
694             HttpStatusCode res;
695             var content = "";
696             try
697             {
698                 res = twCon.RetweetStatus(target, ref content);
699             }
700             catch(Exception ex)
701             {
702                 throw new WebApiException("Err:" + ex.Message, ex);
703             }
704
705             this.CheckStatusCode(res, content);
706
707             TwitterStatus status;
708             try
709             {
710                 status = TwitterStatus.ParseJson(content);
711             }
712             catch(SerializationException ex)
713             {
714                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
715                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
716             }
717             catch(Exception ex)
718             {
719                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
720                 throw new WebApiException("Err:Invalid Json!", content, ex);
721             }
722
723             //ReTweetしたものをTLに追加
724             post = CreatePostsFromStatusData(status);
725             if (post == null)
726                 throw new WebApiException("Invalid Json!", content);
727
728             //二重取得回避
729             lock (LockObj)
730             {
731                 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
732                     return;
733             }
734             //Retweet判定
735             if (post.RetweetedId == null)
736                 throw new WebApiException("Invalid Json!", content);
737             //ユーザー情報
738             post.IsMe = true;
739
740             post.IsRead = read;
741             post.IsOwl = false;
742             if (_readOwnPost) post.IsRead = true;
743             post.IsDm = false;
744
745             TabInformations.GetInstance().AddPost(post);
746         }
747
748         public void RemoveDirectMessage(long id, PostClass post)
749         {
750             this.CheckAccountState();
751             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
752
753             //if (post.IsMe)
754             //    _deletemessages.Add(post)
755             //}
756
757             HttpStatusCode res;
758             try
759             {
760                 res = twCon.DestroyDirectMessage(id);
761             }
762             catch(Exception ex)
763             {
764                 throw new WebApiException("Err:" + ex.Message, ex);
765             }
766
767             this.CheckStatusCode(res, null);
768         }
769
770         public void PostFollowCommand(string screenName)
771         {
772             this.CheckAccountState();
773
774             HttpStatusCode res;
775             var content = "";
776             try
777             {
778                 res = twCon.CreateFriendships(screenName, ref content);
779             }
780             catch(Exception ex)
781             {
782                 throw new WebApiException("Err:" + ex.Message, ex);
783             }
784
785             this.CheckStatusCode(res, content);
786         }
787
788         public void PostRemoveCommand(string screenName)
789         {
790             this.CheckAccountState();
791
792             HttpStatusCode res;
793             var content = "";
794             try
795             {
796                 res = twCon.DestroyFriendships(screenName, ref content);
797             }
798             catch(Exception ex)
799             {
800                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
801             }
802
803             this.CheckStatusCode(res, content);
804         }
805
806         public void PostCreateBlock(string screenName)
807         {
808             this.CheckAccountState();
809
810             HttpStatusCode res;
811             var content = "";
812             try
813             {
814                 res = twCon.CreateBlock(screenName, ref content);
815             }
816             catch(Exception ex)
817             {
818                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
819             }
820
821             this.CheckStatusCode(res, content);
822         }
823
824         public void PostDestroyBlock(string screenName)
825         {
826             this.CheckAccountState();
827
828             HttpStatusCode res;
829             var content = "";
830             try
831             {
832                 res = twCon.DestroyBlock(screenName, ref content);
833             }
834             catch(Exception ex)
835             {
836                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
837             }
838
839             this.CheckStatusCode(res, content);
840         }
841
842         public void PostReportSpam(string screenName)
843         {
844             this.CheckAccountState();
845
846             HttpStatusCode res;
847             var content = "";
848             try
849             {
850                 res = twCon.ReportSpam(screenName, ref content);
851             }
852             catch(Exception ex)
853             {
854                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
855             }
856
857             this.CheckStatusCode(res, content);
858         }
859
860         public TwitterFriendship GetFriendshipInfo(string screenName)
861         {
862             this.CheckAccountState();
863
864             HttpStatusCode res;
865             var content = "";
866             try
867             {
868                 res = twCon.ShowFriendships(_uname, screenName, ref content);
869             }
870             catch(Exception ex)
871             {
872                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
873             }
874
875             this.CheckStatusCode(res, content);
876
877             try
878             {
879                 return TwitterFriendship.ParseJson(content);
880             }
881             catch(SerializationException ex)
882             {
883                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
884                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
885             }
886             catch(Exception ex)
887             {
888                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
889                 throw new WebApiException("Err:Invalid Json!", content, ex);
890             }
891         }
892
893         public TwitterUser GetUserInfo(string screenName)
894         {
895             this.CheckAccountState();
896
897             HttpStatusCode res;
898             var content = "";
899             try
900             {
901                 res = twCon.ShowUserInfo(screenName, ref content);
902             }
903             catch(Exception ex)
904             {
905                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
906             }
907
908             this.CheckStatusCode(res, content);
909
910             try
911             {
912                 return TwitterUser.ParseJson(content);
913             }
914             catch (SerializationException ex)
915             {
916                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
917                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
918             }
919             catch (Exception ex)
920             {
921                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
922                 throw new WebApiException("Err:Invalid Json!", content, ex);
923             }
924         }
925
926         public int GetStatus_Retweeted_Count(long StatusId)
927         {
928             this.CheckAccountState();
929
930             HttpStatusCode res;
931             var content = "";
932             try
933             {
934                 res = twCon.ShowStatuses(StatusId, ref content);
935             }
936             catch (Exception ex)
937             {
938                 throw new WebApiException("Err:" + ex.Message, ex);
939             }
940
941             this.CheckStatusCode(res, content);
942
943             try
944             {
945                 var status = TwitterStatus.ParseJson(content);
946                 return status.RetweetCount;
947             }
948             catch (SerializationException ex)
949             {
950                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
951                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
952             }
953             catch (Exception ex)
954             {
955                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
956                 throw new WebApiException("Invalid Json!", content, ex);
957             }
958         }
959
960         public void PostFavAdd(long id)
961         {
962             this.CheckAccountState();
963
964             //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
965
966             //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
967
968             HttpStatusCode res;
969             var content = "";
970             try
971             {
972                 res = twCon.CreateFavorites(id, ref content);
973             }
974             catch(Exception ex)
975             {
976                 //this.favQueue.Add(id)
977                 //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
978                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
979             }
980
981             this.CheckStatusCode(res, content);
982
983             if (!RestrictFavCheck)
984                 return;
985
986             //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
987
988             try
989             {
990                 res = twCon.ShowStatuses(id, ref content);
991             }
992             catch(Exception ex)
993             {
994                 throw new WebApiException("Err:" + ex.Message, ex);
995             }
996
997             this.CheckStatusCode(res, content);
998
999             TwitterStatus status;
1000             try
1001             {
1002                 status = TwitterStatus.ParseJson(content);
1003             }
1004             catch (SerializationException ex)
1005             {
1006                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1007                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
1008             }
1009             catch (Exception ex)
1010             {
1011                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1012                 throw new WebApiException("Err:Invalid Json!", content, ex);
1013             }
1014             if (status.Favorited != true)
1015                 throw new WebApiException("NG(Restricted?)");
1016         }
1017
1018         public void PostFavRemove(long id)
1019         {
1020             this.CheckAccountState();
1021
1022             //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
1023
1024             //if (this.favQueue.Contains(id))
1025             //    this.favQueue.Remove(id)
1026             //    return "";
1027             //}
1028
1029             HttpStatusCode res;
1030             var content = "";
1031             try
1032             {
1033                 res = twCon.DestroyFavorites(id, ref content);
1034             }
1035             catch(Exception ex)
1036             {
1037                 throw new WebApiException("Err:" + ex.Message, ex);
1038             }
1039
1040             this.CheckStatusCode(res, content);
1041         }
1042
1043         public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
1044         {
1045             this.CheckAccountState();
1046
1047             HttpStatusCode res;
1048             var content = "";
1049             try
1050             {
1051                 res = twCon.UpdateProfile(name, url, location, description, ref content);
1052             }
1053             catch(Exception ex)
1054             {
1055                 throw new WebApiException("Err:" + ex.Message, content, ex);
1056             }
1057
1058             this.CheckStatusCode(res, content);
1059
1060             try
1061             {
1062                 return TwitterUser.ParseJson(content);
1063             }
1064             catch (SerializationException e)
1065             {
1066                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
1067                 MyCommon.TraceOut(ex);
1068                 throw ex;
1069             }
1070             catch (Exception e)
1071             {
1072                 var ex = new WebApiException("Err:Invalid Json!", content, e);
1073                 MyCommon.TraceOut(ex);
1074                 throw ex;
1075             }
1076         }
1077
1078         public void PostUpdateProfileImage(string filename)
1079         {
1080             this.CheckAccountState();
1081
1082             HttpStatusCode res;
1083             var content = "";
1084             try
1085             {
1086                 res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
1087             }
1088             catch(Exception ex)
1089             {
1090                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
1091             }
1092
1093             this.CheckStatusCode(res, content);
1094         }
1095
1096         public string Username
1097         {
1098             get
1099             {
1100                 return twCon.AuthenticatedUsername;
1101             }
1102         }
1103
1104         public long UserId
1105         {
1106             get
1107             {
1108                 return twCon.AuthenticatedUserId;
1109             }
1110         }
1111
1112         public string Password
1113         {
1114             get
1115             {
1116                 return twCon.Password;
1117             }
1118         }
1119
1120         private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
1121         public static MyCommon.ACCOUNT_STATE AccountState
1122         {
1123             get
1124             {
1125                 return _accountState;
1126             }
1127             set
1128             {
1129                 _accountState = value;
1130             }
1131         }
1132
1133         public bool RestrictFavCheck { get; set; }
1134
1135 #region "バージョンアップ"
1136         public void GetTweenBinary(string strVer)
1137         {
1138             try
1139             {
1140                 //本体
1141                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1142                                                     Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1143                 {
1144                     throw new WebApiException("Err:Download failed");
1145                 }
1146                 //英語リソース
1147                 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1148                 {
1149                     Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1150                 }
1151                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1152                                                     Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1153                 {
1154                     throw new WebApiException("Err:Download failed");
1155                 }
1156                 //その他言語圏のリソース。取得失敗しても継続
1157                 //UIの言語圏のリソース
1158                 var curCul = "";
1159                 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1160                 {
1161                     var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1162                     if (idx > -1)
1163                     {
1164                         curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1165                     }
1166                     else
1167                     {
1168                         curCul = Thread.CurrentThread.CurrentUICulture.Name;
1169                     }
1170                 }
1171                 else
1172                 {
1173                     curCul = Thread.CurrentThread.CurrentUICulture.Name;
1174                 }
1175                 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1176                 {
1177                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1178                     {
1179                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1180                     }
1181                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1182                                                         Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1183                     {
1184                         //return "Err:Download failed";
1185                     }
1186                 }
1187                 //スレッドの言語圏のリソース
1188                 string curCul2;
1189                 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1190                 {
1191                     var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1192                     if (idx > -1)
1193                     {
1194                         curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1195                     }
1196                     else
1197                     {
1198                         curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1199                     }
1200                 }
1201                 else
1202                 {
1203                     curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1204                 }
1205                 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1206                 {
1207                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1208                     {
1209                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1210                     }
1211                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1212                                                     Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1213                     {
1214                         //return "Err:Download failed";
1215                     }
1216                 }
1217
1218                 //アップデータ
1219                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1220                                                     Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1221                 {
1222                     throw new WebApiException("Err:Download failed");
1223                 }
1224                 //シリアライザDLL
1225                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1226                                                     Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1227                 {
1228                     throw new WebApiException("Err:Download failed");
1229                 }
1230             }
1231             catch (Exception ex)
1232             {
1233                 throw new WebApiException("Err:Download failed", ex);
1234             }
1235         }
1236 #endregion
1237
1238         public bool ReadOwnPost
1239         {
1240             get
1241             {
1242                 return _readOwnPost;
1243             }
1244             set
1245             {
1246                 _readOwnPost = value;
1247             }
1248         }
1249
1250         public int FollowersCount { get; private set; }
1251         public int FriendsCount { get; private set; }
1252         public int StatusesCount { get; private set; }
1253         public string Location { get; private set; } = "";
1254         public string Bio { get; private set; } = "";
1255
1256         /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1257         private void UpdateUserStats(TwitterUser self)
1258         {
1259             this.FollowersCount = self.FollowersCount;
1260             this.FriendsCount = self.FriendsCount;
1261             this.StatusesCount = self.StatusesCount;
1262             this.Location = self.Location;
1263             this.Bio = self.Description;
1264         }
1265
1266         /// <summary>
1267         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1268         /// </summary>
1269         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1270         {
1271             return count >= 20 && count <= GetMaxApiResultCount(type);
1272         }
1273
1274         /// <summary>
1275         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1276         /// </summary>
1277         public static bool VerifyMoreApiResultCount(int count)
1278         {
1279             return count >= 20 && count <= 200;
1280         }
1281
1282         /// <summary>
1283         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1284         /// </summary>
1285         public static bool VerifyFirstApiResultCount(int count)
1286         {
1287             return count >= 20 && count <= 200;
1288         }
1289
1290         /// <summary>
1291         /// WORKERTYPEに応じた取得可能な最大件数を取得する
1292         /// </summary>
1293         public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1294         {
1295             // 参照: REST APIs - 各endpointのcountパラメータ
1296             // https://dev.twitter.com/rest/public
1297             switch (type)
1298             {
1299                 case MyCommon.WORKERTYPE.Timeline:
1300                 case MyCommon.WORKERTYPE.Reply:
1301                 case MyCommon.WORKERTYPE.UserTimeline:
1302                 case MyCommon.WORKERTYPE.Favorites:
1303                 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1304                 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1305                 case MyCommon.WORKERTYPE.List:  // 不明
1306                     return 200;
1307
1308                 case MyCommon.WORKERTYPE.PublicSearch:
1309                     return 100;
1310
1311                 default:
1312                     throw new InvalidOperationException("Invalid type: " + type);
1313             }
1314         }
1315
1316         /// <summary>
1317         /// WORKERTYPEに応じた取得件数を取得する
1318         /// </summary>
1319         public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1320         {
1321             if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1322                 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1323             {
1324                 return 20;
1325             }
1326
1327             if (SettingCommon.Instance.UseAdditionalCount)
1328             {
1329                 switch (type)
1330                 {
1331                     case MyCommon.WORKERTYPE.Favorites:
1332                         if (SettingCommon.Instance.FavoritesCountApi != 0)
1333                             return SettingCommon.Instance.FavoritesCountApi;
1334                         break;
1335                     case MyCommon.WORKERTYPE.List:
1336                         if (SettingCommon.Instance.ListCountApi != 0)
1337                             return SettingCommon.Instance.ListCountApi;
1338                         break;
1339                     case MyCommon.WORKERTYPE.PublicSearch:
1340                         if (SettingCommon.Instance.SearchCountApi != 0)
1341                             return SettingCommon.Instance.SearchCountApi;
1342                         break;
1343                     case MyCommon.WORKERTYPE.UserTimeline:
1344                         if (SettingCommon.Instance.UserTimelineCountApi != 0)
1345                             return SettingCommon.Instance.UserTimelineCountApi;
1346                         break;
1347                 }
1348                 if (more && SettingCommon.Instance.MoreCountApi != 0)
1349                 {
1350                     return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1351                 }
1352                 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1353                 {
1354                     return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1355                 }
1356             }
1357
1358             // 上記に当てはまらない場合の共通処理
1359             var count = SettingCommon.Instance.CountApi;
1360
1361             if (type == MyCommon.WORKERTYPE.Reply)
1362                 count = SettingCommon.Instance.CountApiReply;
1363
1364             return Math.Min(count, GetMaxApiResultCount(type));
1365         }
1366
1367         public void GetTimelineApi(bool read,
1368                                 MyCommon.WORKERTYPE gType,
1369                                 bool more,
1370                                 bool startup)
1371         {
1372             this.CheckAccountState();
1373
1374             HttpStatusCode res;
1375             var content = "";
1376             var count = GetApiResultCount(gType, more, startup);
1377
1378             try
1379             {
1380                 if (gType == MyCommon.WORKERTYPE.Timeline)
1381                 {
1382                     if (more)
1383                     {
1384                         res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1385                     }
1386                     else
1387                     {
1388                         res = twCon.HomeTimeline(count, null, null, ref content);
1389                     }
1390                 }
1391                 else
1392                 {
1393                     if (more)
1394                     {
1395                         res = twCon.Mentions(count, this.minMentions, null, ref content);
1396                     }
1397                     else
1398                     {
1399                         res = twCon.Mentions(count, null, null, ref content);
1400                     }
1401                 }
1402             }
1403             catch(Exception ex)
1404             {
1405                 throw new WebApiException("Err:" + ex.Message, ex);
1406             }
1407
1408             this.CheckStatusCode(res, content);
1409
1410             var minimumId = CreatePostsFromJson(content, gType, null, read);
1411
1412             if (minimumId != null)
1413             {
1414                 if (gType == MyCommon.WORKERTYPE.Timeline)
1415                     this.minHomeTimeline = minimumId.Value;
1416                 else
1417                     this.minMentions = minimumId.Value;
1418             }
1419         }
1420
1421         public void GetUserTimelineApi(bool read,
1422                                          string userName,
1423                                          TabClass tab,
1424                                          bool more)
1425         {
1426             this.CheckAccountState();
1427
1428             HttpStatusCode res;
1429             var content = "";
1430             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1431
1432             try
1433             {
1434                 if (string.IsNullOrEmpty(userName))
1435                 {
1436                     var target = tab.User;
1437                     if (string.IsNullOrEmpty(target)) return;
1438                     userName = target;
1439                     res = twCon.UserTimeline(null, target, count, null, null, ref content);
1440                 }
1441                 else
1442                 {
1443                     if (more)
1444                     {
1445                         res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1446                     }
1447                     else
1448                     {
1449                         res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1450                     }
1451                 }
1452             }
1453             catch(Exception ex)
1454             {
1455                 throw new WebApiException("Err:" + ex.Message, ex);
1456             }
1457
1458             if (res == HttpStatusCode.Unauthorized)
1459                 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1460
1461             this.CheckStatusCode(res, content);
1462
1463             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1464
1465             if (minimumId != null)
1466                 tab.OldestId = minimumId.Value;
1467         }
1468
1469         public PostClass GetStatusApi(bool read, long id)
1470         {
1471             this.CheckAccountState();
1472
1473             HttpStatusCode res;
1474             var content = "";
1475             try
1476             {
1477                 res = twCon.ShowStatuses(id, ref content);
1478             }
1479             catch(Exception ex)
1480             {
1481                 throw new WebApiException("Err:" + ex.Message, ex);
1482             }
1483
1484             if (res == HttpStatusCode.Forbidden)
1485                 throw new WebApiException("Err:protected user's tweet", content);
1486
1487             this.CheckStatusCode(res, content);
1488
1489             TwitterStatus status;
1490             try
1491             {
1492                 status = TwitterStatus.ParseJson(content);
1493             }
1494             catch(SerializationException ex)
1495             {
1496                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1497                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1498             }
1499             catch(Exception ex)
1500             {
1501                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1502                 throw new WebApiException("Invalid Json!", content, ex);
1503             }
1504
1505             var item = CreatePostsFromStatusData(status);
1506             if (item == null)
1507                 throw new WebApiException("Err:Can't create post", content);
1508
1509             item.IsRead = read;
1510             if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1511
1512             return item;
1513         }
1514
1515         public void GetStatusApi(bool read, long id, TabClass tab)
1516         {
1517             var post = this.GetStatusApi(read, id);
1518
1519             //非同期アイコン取得&StatusDictionaryに追加
1520             if (tab != null && tab.IsInnerStorageTabType)
1521                 tab.AddPostToInnerStorage(post);
1522             else
1523                 TabInformations.GetInstance().AddPost(post);
1524         }
1525
1526         private PostClass CreatePostsFromStatusData(TwitterStatus status)
1527         {
1528             return CreatePostsFromStatusData(status, false);
1529         }
1530
1531         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1532         {
1533             var post = new PostClass();
1534             TwitterEntities entities;
1535             string sourceHtml;
1536
1537             post.StatusId = status.Id;
1538             if (status.RetweetedStatus != null)
1539             {
1540                 var retweeted = status.RetweetedStatus;
1541
1542                 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1543
1544                 //Id
1545                 post.RetweetedId = retweeted.Id;
1546                 //本文
1547                 post.TextFromApi = retweeted.Text;
1548                 entities = retweeted.MergedEntities;
1549                 sourceHtml = retweeted.Source;
1550                 //Reply先
1551                 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1552                 post.InReplyToUser = retweeted.InReplyToScreenName;
1553                 post.InReplyToUserId = status.InReplyToUserId;
1554
1555                 if (favTweet)
1556                 {
1557                     post.IsFav = true;
1558                 }
1559                 else
1560                 {
1561                     //幻覚fav対策
1562                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1563                     post.IsFav = tc.Contains(retweeted.Id);
1564                 }
1565
1566                 if (retweeted.Coordinates != null)
1567                     post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1568
1569                 //以下、ユーザー情報
1570                 var user = retweeted.User;
1571
1572                 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1573
1574                 post.UserId = user.Id;
1575                 post.ScreenName = user.ScreenName;
1576                 post.Nickname = user.Name.Trim();
1577                 post.ImageUrl = user.ProfileImageUrlHttps;
1578                 post.IsProtect = user.Protected;
1579
1580                 //Retweetした人
1581                 post.RetweetedBy = status.User.ScreenName;
1582                 post.RetweetedByUserId = status.User.Id;
1583                 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
1584             }
1585             else
1586             {
1587                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1588                 //本文
1589                 post.TextFromApi = status.Text;
1590                 entities = status.MergedEntities;
1591                 sourceHtml = status.Source;
1592                 post.InReplyToStatusId = status.InReplyToStatusId;
1593                 post.InReplyToUser = status.InReplyToScreenName;
1594                 post.InReplyToUserId = status.InReplyToUserId;
1595
1596                 if (favTweet)
1597                 {
1598                     post.IsFav = true;
1599                 }
1600                 else
1601                 {
1602                     //幻覚fav対策
1603                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1604                     post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1605                 }
1606
1607                 if (status.Coordinates != null)
1608                     post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1609
1610                 //以下、ユーザー情報
1611                 var user = status.User;
1612
1613                 if (user == null || user.ScreenName == null) return null;
1614
1615                 post.UserId = user.Id;
1616                 post.ScreenName = user.ScreenName;
1617                 post.Nickname = user.Name.Trim();
1618                 post.ImageUrl = user.ProfileImageUrlHttps;
1619                 post.IsProtect = user.Protected;
1620                 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
1621             }
1622             //HTMLに整形
1623             string textFromApi = post.TextFromApi;
1624             post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1625             post.TextFromApi = textFromApi;
1626             post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1627             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1628             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1629
1630             post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1631                 .Where(x => x != post.StatusId && x != post.RetweetedId)
1632                 .Distinct().ToArray();
1633
1634             post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1635                 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1636                 .ToArray();
1637
1638             //Source整形
1639             var source = ParseSource(sourceHtml);
1640             post.Source = source.Item1;
1641             post.SourceUri = source.Item2;
1642
1643             post.IsReply = post.ReplyToList.Contains(_uname);
1644             post.IsExcludeReply = false;
1645
1646             if (post.IsMe)
1647             {
1648                 post.IsOwl = false;
1649             }
1650             else
1651             {
1652                 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1653             }
1654
1655             post.IsDm = false;
1656             return post;
1657         }
1658
1659         /// <summary>
1660         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1661         /// </summary>
1662         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1663         {
1664             var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1665
1666             return GetQuoteTweetStatusIds(urls);
1667         }
1668
1669         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1670         {
1671             foreach (var url in urls)
1672             {
1673                 var match = Twitter.StatusUrlRegex.Match(url);
1674                 if (match.Success)
1675                 {
1676                     long statusId;
1677                     if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1678                         yield return statusId;
1679                 }
1680             }
1681         }
1682
1683         private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1684         {
1685             TwitterStatus[] items;
1686             try
1687             {
1688                 items = TwitterStatus.ParseJsonArray(content);
1689             }
1690             catch(SerializationException ex)
1691             {
1692                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1693                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1694             }
1695             catch(Exception ex)
1696             {
1697                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1698                 throw new WebApiException("Invalid Json!", content, ex);
1699             }
1700
1701             long? minimumId = null;
1702
1703             foreach (var status in items)
1704             {
1705                 PostClass post = null;
1706                 post = CreatePostsFromStatusData(status);
1707                 if (post == null) continue;
1708
1709                 if (minimumId == null || minimumId.Value > post.StatusId)
1710                     minimumId = post.StatusId;
1711
1712                 //二重取得回避
1713                 lock (LockObj)
1714                 {
1715                     if (tab == null)
1716                     {
1717                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1718                     }
1719                     else
1720                     {
1721                         if (tab.Contains(post.StatusId)) continue;
1722                     }
1723                 }
1724
1725                 //RT禁止ユーザーによるもの
1726                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1727                     post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1728
1729                 post.IsRead = read;
1730                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1731
1732                 //非同期アイコン取得&StatusDictionaryに追加
1733                 if (tab != null && tab.IsInnerStorageTabType)
1734                     tab.AddPostToInnerStorage(post);
1735                 else
1736                     TabInformations.GetInstance().AddPost(post);
1737             }
1738
1739             return minimumId;
1740         }
1741
1742         private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1743         {
1744             TwitterSearchResult items;
1745             try
1746             {
1747                 items = TwitterSearchResult.ParseJson(content);
1748             }
1749             catch (SerializationException ex)
1750             {
1751                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1752                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1753             }
1754             catch (Exception ex)
1755             {
1756                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1757                 throw new WebApiException("Invalid Json!", content, ex);
1758             }
1759
1760             long? minimumId = null;
1761
1762             foreach (var result in items.Statuses)
1763             {
1764                 PostClass post = null;
1765                 post = CreatePostsFromStatusData(result);
1766
1767                 if (post == null)
1768                 {
1769                     // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1770                     try
1771                     {
1772                         post = this.GetStatusApi(read, result.Id);
1773                     }
1774                     catch (WebApiException)
1775                     {
1776                         continue;
1777                     }
1778                 }
1779
1780                 if (minimumId == null || minimumId.Value > post.StatusId)
1781                     minimumId = post.StatusId;
1782
1783                 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1784                 //二重取得回避
1785                 lock (LockObj)
1786                 {
1787                     if (tab == null)
1788                     {
1789                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1790                     }
1791                     else
1792                     {
1793                         if (tab.Contains(post.StatusId)) continue;
1794                     }
1795                 }
1796
1797                 post.IsRead = read;
1798                 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1799
1800                 //非同期アイコン取得&StatusDictionaryに追加
1801                 if (tab != null && tab.IsInnerStorageTabType)
1802                     tab.AddPostToInnerStorage(post);
1803                 else
1804                     TabInformations.GetInstance().AddPost(post);
1805             }
1806
1807             return minimumId;
1808         }
1809
1810         private void CreateFavoritePostsFromJson(string content, bool read)
1811         {
1812             TwitterStatus[] item;
1813             try
1814             {
1815                 item = TwitterStatus.ParseJsonArray(content);
1816             }
1817             catch (SerializationException ex)
1818             {
1819                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1820                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1821             }
1822             catch (Exception ex)
1823             {
1824                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1825                 throw new WebApiException("Invalid Json!", content, ex);
1826             }
1827
1828             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1829
1830             foreach (var status in item)
1831             {
1832                 //二重取得回避
1833                 lock (LockObj)
1834                 {
1835                     if (favTab.Contains(status.Id)) continue;
1836                 }
1837
1838                 var post = CreatePostsFromStatusData(status, true);
1839                 if (post == null) continue;
1840
1841                 post.IsRead = read;
1842
1843                 TabInformations.GetInstance().AddPost(post);
1844             }
1845         }
1846
1847         public void GetListStatus(bool read,
1848                                 TabClass tab,
1849                                 bool more,
1850                                 bool startup)
1851         {
1852             HttpStatusCode res;
1853             var content = "";
1854             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1855
1856             try
1857             {
1858                 if (more)
1859                 {
1860                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1861                 }
1862                 else
1863                 {
1864                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1865                 }
1866             }
1867             catch(Exception ex)
1868             {
1869                 throw new WebApiException("Err:" + ex.Message, ex);
1870             }
1871
1872             this.CheckStatusCode(res, content);
1873
1874             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1875
1876             if (minimumId != null)
1877                 tab.OldestId = minimumId.Value;
1878         }
1879
1880         /// <summary>
1881         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1882         /// </summary>
1883         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1884         internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1885         {
1886             if (!posts.ContainsKey(startStatusId))
1887                 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1888
1889             var nextPost = posts[startStatusId];
1890             while (nextPost.InReplyToStatusId != null)
1891             {
1892                 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1893                     break;
1894                 nextPost = posts[nextPost.InReplyToStatusId.Value];
1895             }
1896
1897             return nextPost;
1898         }
1899
1900         public void GetRelatedResult(bool read, TabClass tab)
1901         {
1902             var relPosts = new Dictionary<Int64, PostClass>();
1903             if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1904             {
1905                 //検索結果対応
1906                 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1907                 if (p != null && p.InReplyToStatusId != null)
1908                 {
1909                     tab.RelationTargetPost = p;
1910                 }
1911                 else
1912                 {
1913                     p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1914                     tab.RelationTargetPost = p;
1915                 }
1916             }
1917             relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1918
1919             Exception lastException = null;
1920
1921             // in_reply_to_status_id を使用してリプライチェインを辿る
1922             var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1923             var loopCount = 1;
1924             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1925             {
1926                 var inReplyToId = nextPost.InReplyToStatusId.Value;
1927
1928                 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1929                 if (inReplyToPost == null)
1930                 {
1931                     try
1932                     {
1933                         inReplyToPost = this.GetStatusApi(read, inReplyToId);
1934                     }
1935                     catch (WebApiException ex)
1936                     {
1937                         lastException = ex;
1938                         break;
1939                     }
1940                 }
1941
1942                 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1943
1944                 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1945             }
1946
1947             //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1948             var text = tab.RelationTargetPost.Text;
1949             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1950                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1951             foreach (var _match in ma)
1952             {
1953                 Int64 _statusId;
1954                 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1955                 {
1956                     if (relPosts.ContainsKey(_statusId))
1957                         continue;
1958
1959                     var p = TabInformations.GetInstance()[_statusId];
1960                     if (p == null)
1961                     {
1962                         try
1963                         {
1964                             p = this.GetStatusApi(read, _statusId);
1965                         }
1966                         catch (WebApiException ex)
1967                         {
1968                             lastException = ex;
1969                             break;
1970                         }
1971                     }
1972
1973                     if (p != null)
1974                         relPosts.Add(p.StatusId, p);
1975                 }
1976             }
1977
1978             relPosts.Values.ToList().ForEach(p =>
1979             {
1980                 if (p.IsMe && !read && this._readOwnPost)
1981                     p.IsRead = true;
1982                 else
1983                     p.IsRead = read;
1984
1985                 tab.AddPostToInnerStorage(p);
1986             });
1987
1988             if (lastException != null)
1989                 throw new WebApiException(lastException.Message, lastException);
1990         }
1991
1992         public void GetSearch(bool read,
1993                             TabClass tab,
1994                             bool more)
1995         {
1996             HttpStatusCode res;
1997             var content = "";
1998             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1999             long? maxId = null;
2000             long? sinceId = null;
2001             if (more)
2002             {
2003                 maxId = tab.OldestId - 1;
2004             }
2005             else
2006             {
2007                 sinceId = tab.SinceId;
2008             }
2009
2010             try
2011             {
2012                 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2013                 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2014             }
2015             catch(Exception ex)
2016             {
2017                 throw new WebApiException("Err:" + ex.Message, ex);
2018             }
2019             switch (res)
2020             {
2021                 case HttpStatusCode.BadRequest:
2022                     throw new WebApiException("Invalid query", content);
2023                 case HttpStatusCode.NotFound:
2024                     throw new WebApiException("Invalid query", content);
2025                 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2026                     throw new WebApiException("Search API Limit?", content);
2027                 case HttpStatusCode.OK:
2028                     break;
2029                 default:
2030                     throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2031             }
2032
2033             if (!TabInformations.GetInstance().ContainsTab(tab))
2034                 return;
2035
2036             var minimumId =  this.CreatePostsFromSearchJson(content, tab, read, count, more);
2037
2038             if (minimumId != null)
2039                 tab.OldestId = minimumId.Value;
2040         }
2041
2042         private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2043         {
2044             TwitterDirectMessage[] item;
2045             try
2046             {
2047                 if (gType == MyCommon.WORKERTYPE.UserStream)
2048                 {
2049                     item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2050                 }
2051                 else
2052                 {
2053                     item = TwitterDirectMessage.ParseJsonArray(content);
2054                 }
2055             }
2056             catch(SerializationException ex)
2057             {
2058                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2059                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2060             }
2061             catch(Exception ex)
2062             {
2063                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2064                 throw new WebApiException("Invalid Json!", content, ex);
2065             }
2066
2067             foreach (var message in item)
2068             {
2069                 var post = new PostClass();
2070                 try
2071                 {
2072                     post.StatusId = message.Id;
2073                     if (gType != MyCommon.WORKERTYPE.UserStream)
2074                     {
2075                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2076                         {
2077                             if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2078                         }
2079                         else
2080                         {
2081                             if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2082                         }
2083                     }
2084
2085                     //二重取得回避
2086                     lock (LockObj)
2087                     {
2088                         if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2089                     }
2090                     //sender_id
2091                     //recipient_id
2092                     post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2093                     //本文
2094                     var textFromApi = message.Text;
2095                     //HTMLに整形
2096                     post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2097                     post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2098                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2099                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2100                     post.IsFav = false;
2101
2102                     post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2103
2104                     post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
2105                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
2106                         .ToArray();
2107
2108                     //以下、ユーザー情報
2109                     TwitterUser user;
2110                     if (gType == MyCommon.WORKERTYPE.UserStream)
2111                     {
2112                         if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2113                         {
2114                             user = message.Sender;
2115                             post.IsMe = false;
2116                             post.IsOwl = true;
2117                         }
2118                         else
2119                         {
2120                             user = message.Recipient;
2121                             post.IsMe = true;
2122                             post.IsOwl = false;
2123                         }
2124                     }
2125                     else
2126                     {
2127                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2128                         {
2129                             user = message.Sender;
2130                             post.IsMe = false;
2131                             post.IsOwl = true;
2132                         }
2133                         else
2134                         {
2135                             user = message.Recipient;
2136                             post.IsMe = true;
2137                             post.IsOwl = false;
2138                         }
2139                     }
2140
2141                     post.UserId = user.Id;
2142                     post.ScreenName = user.ScreenName;
2143                     post.Nickname = user.Name.Trim();
2144                     post.ImageUrl = user.ProfileImageUrlHttps;
2145                     post.IsProtect = user.Protected;
2146                 }
2147                 catch(Exception ex)
2148                 {
2149                     MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2150                     MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2151                     continue;
2152                 }
2153
2154                 post.IsRead = read;
2155                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2156                 post.IsReply = false;
2157                 post.IsExcludeReply = false;
2158                 post.IsDm = true;
2159
2160                 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2161                 dmTab.AddPostToInnerStorage(post);
2162             }
2163         }
2164
2165         public void GetDirectMessageApi(bool read,
2166                                 MyCommon.WORKERTYPE gType,
2167                                 bool more)
2168         {
2169             this.CheckAccountState();
2170             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2171
2172             HttpStatusCode res;
2173             var content = "";
2174             var count = GetApiResultCount(gType, more, false);
2175
2176             try
2177             {
2178                 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2179                 {
2180                     if (more)
2181                     {
2182                         res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2183                     }
2184                     else
2185                     {
2186                         res = twCon.DirectMessages(count, null, null, ref content);
2187                     }
2188                 }
2189                 else
2190                 {
2191                     if (more)
2192                     {
2193                         res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2194                     }
2195                     else
2196                     {
2197                         res = twCon.DirectMessagesSent(count, null, null, ref content);
2198                     }
2199                 }
2200             }
2201             catch(Exception ex)
2202             {
2203                 throw new WebApiException("Err:" + ex.Message, ex);
2204             }
2205
2206             this.CheckStatusCode(res, content);
2207
2208             CreateDirectMessagesFromJson(content, gType, read);
2209         }
2210
2211         public void GetFavoritesApi(bool read,
2212                             bool more)
2213         {
2214             this.CheckAccountState();
2215
2216             HttpStatusCode res;
2217             var content = "";
2218             var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2219
2220             try
2221             {
2222                 res = twCon.Favorites(count, ref content);
2223             }
2224             catch(Exception ex)
2225             {
2226                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2227             }
2228
2229             this.CheckStatusCode(res, content);
2230
2231             CreateFavoritePostsFromJson(content, read);
2232         }
2233
2234         private string ReplaceTextFromApi(string text, TwitterEntities entities)
2235         {
2236             if (entities != null)
2237             {
2238                 if (entities.Urls != null)
2239                 {
2240                     foreach (var m in entities.Urls)
2241                     {
2242                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2243                     }
2244                 }
2245                 if (entities.Media != null)
2246                 {
2247                     foreach (var m in entities.Media)
2248                     {
2249                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2250                     }
2251                 }
2252             }
2253             return text;
2254         }
2255
2256         /// <summary>
2257         /// フォロワーIDを更新します
2258         /// </summary>
2259         /// <exception cref="WebApiException"/>
2260         public void RefreshFollowerIds()
2261         {
2262             if (MyCommon._endingFlag) return;
2263
2264             var cursor = -1L;
2265             var newFollowerIds = new HashSet<long>();
2266             do
2267             {
2268                 var ret = this.GetFollowerIdsApi(ref cursor);
2269                 newFollowerIds.UnionWith(ret.Ids);
2270                 cursor = ret.NextCursor;
2271             } while (cursor != 0);
2272
2273             this.followerId = newFollowerIds;
2274             TabInformations.GetInstance().RefreshOwl(this.followerId);
2275
2276             this._GetFollowerResult = true;
2277         }
2278
2279         public bool GetFollowersSuccess
2280         {
2281             get
2282             {
2283                 return _GetFollowerResult;
2284             }
2285         }
2286
2287         private TwitterIds GetFollowerIdsApi(ref long cursor)
2288         {
2289             this.CheckAccountState();
2290
2291             HttpStatusCode res;
2292             var content = "";
2293             try
2294             {
2295                 res = twCon.FollowerIds(cursor, ref content);
2296             }
2297             catch(Exception e)
2298             {
2299                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2300             }
2301
2302             this.CheckStatusCode(res, content);
2303
2304             try
2305             {
2306                 var ret = TwitterIds.ParseJson(content);
2307
2308                 if (ret.Ids == null)
2309                 {
2310                     var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2311                     MyCommon.ExceptionOut(ex);
2312                     throw ex;
2313                 }
2314
2315                 return ret;
2316             }
2317             catch(SerializationException e)
2318             {
2319                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2320                 MyCommon.TraceOut(ex);
2321                 throw ex;
2322             }
2323             catch(Exception e)
2324             {
2325                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2326                 MyCommon.TraceOut(ex);
2327                 throw ex;
2328             }
2329         }
2330
2331         /// <summary>
2332         /// RT 非表示ユーザーを更新します
2333         /// </summary>
2334         /// <exception cref="WebApiException"/>
2335         public void RefreshNoRetweetIds()
2336         {
2337             if (MyCommon._endingFlag) return;
2338
2339             this.noRTId = this.NoRetweetIdsApi();
2340
2341             this._GetNoRetweetResult = true;
2342         }
2343
2344         private long[] NoRetweetIdsApi()
2345         {
2346             this.CheckAccountState();
2347
2348             HttpStatusCode res;
2349             var content = "";
2350             try
2351             {
2352                 res = twCon.NoRetweetIds(ref content);
2353             }
2354             catch(Exception e)
2355             {
2356                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2357             }
2358
2359             this.CheckStatusCode(res, content);
2360
2361             try
2362             {
2363                 return MyCommon.CreateDataFromJson<long[]>(content);
2364             }
2365             catch(SerializationException e)
2366             {
2367                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2368                 MyCommon.TraceOut(ex);
2369                 throw ex;
2370             }
2371             catch(Exception e)
2372             {
2373                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2374                 MyCommon.TraceOut(ex);
2375                 throw ex;
2376             }
2377         }
2378
2379         public bool GetNoRetweetSuccess
2380         {
2381             get
2382             {
2383                 return _GetNoRetweetResult;
2384             }
2385         }
2386
2387         /// <summary>
2388         /// t.co の文字列長などの設定情報を更新します
2389         /// </summary>
2390         /// <exception cref="WebApiException"/>
2391         public void RefreshConfiguration()
2392         {
2393             this.Configuration = this.ConfigurationApi();
2394         }
2395
2396         private TwitterConfiguration ConfigurationApi()
2397         {
2398             HttpStatusCode res;
2399             var content = "";
2400             try
2401             {
2402                 res = twCon.GetConfiguration(ref content);
2403             }
2404             catch(Exception e)
2405             {
2406                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2407             }
2408
2409             this.CheckStatusCode(res, content);
2410
2411             try
2412             {
2413                 return TwitterConfiguration.ParseJson(content);
2414             }
2415             catch(SerializationException e)
2416             {
2417                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2418                 MyCommon.TraceOut(ex);
2419                 throw ex;
2420             }
2421             catch(Exception e)
2422             {
2423                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2424                 MyCommon.TraceOut(ex);
2425                 throw ex;
2426             }
2427         }
2428
2429         public void GetListsApi()
2430         {
2431             this.CheckAccountState();
2432
2433             HttpStatusCode res;
2434             IEnumerable<ListElement> lists;
2435             var content = "";
2436
2437             try
2438             {
2439                 res = twCon.GetLists(this.Username, ref content);
2440             }
2441             catch (Exception ex)
2442             {
2443                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2444             }
2445
2446             this.CheckStatusCode(res, content);
2447
2448             try
2449             {
2450                 lists = TwitterList.ParseJsonArray(content)
2451                     .Select(x => new ListElement(x, this));
2452             }
2453             catch (SerializationException ex)
2454             {
2455                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2456                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2457             }
2458             catch (Exception ex)
2459             {
2460                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2461                 throw new WebApiException("Err:Invalid Json!", content, ex);
2462             }
2463
2464             try
2465             {
2466                 res = twCon.GetListsSubscriptions(this.Username, ref content);
2467             }
2468             catch (Exception ex)
2469             {
2470                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2471             }
2472
2473             this.CheckStatusCode(res, content);
2474
2475             try
2476             {
2477                 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2478                     .Select(x => new ListElement(x, this)));
2479             }
2480             catch (SerializationException ex)
2481             {
2482                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2483                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2484             }
2485             catch (Exception ex)
2486             {
2487                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2488                 throw new WebApiException("Err:Invalid Json!", content, ex);
2489             }
2490
2491             TabInformations.GetInstance().SubscribableLists = lists.ToList();
2492         }
2493
2494         public void DeleteList(string list_id)
2495         {
2496             HttpStatusCode res;
2497             var content = "";
2498
2499             try
2500             {
2501                 res = twCon.DeleteListID(this.Username, list_id, ref content);
2502             }
2503             catch(Exception ex)
2504             {
2505                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2506             }
2507
2508             this.CheckStatusCode(res, content);
2509         }
2510
2511         public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2512         {
2513             HttpStatusCode res;
2514             var content = "";
2515
2516             try
2517             {
2518                 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2519             }
2520             catch(Exception ex)
2521             {
2522                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2523             }
2524
2525             this.CheckStatusCode(res, content);
2526
2527             try
2528             {
2529                 var le = TwitterList.ParseJson(content);
2530                 return  new ListElement(le, this);
2531             }
2532             catch(SerializationException ex)
2533             {
2534                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2535                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2536             }
2537             catch(Exception ex)
2538             {
2539                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2540                 throw new WebApiException("Err:Invalid Json!", content, ex);
2541             }
2542         }
2543
2544         public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2545         {
2546             this.CheckAccountState();
2547
2548             HttpStatusCode res;
2549             var content = "";
2550             try
2551             {
2552                 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2553             }
2554             catch(Exception ex)
2555             {
2556                 throw new WebApiException("Err:" + ex.Message);
2557             }
2558
2559             this.CheckStatusCode(res, content);
2560
2561             try
2562             {
2563                 var users = TwitterUsers.ParseJson(content);
2564                 Array.ForEach<TwitterUser>(
2565                     users.Users,
2566                     u => lists.Add(new UserInfo(u)));
2567
2568                 return users.NextCursor;
2569             }
2570             catch(SerializationException ex)
2571             {
2572                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2573                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2574             }
2575             catch(Exception ex)
2576             {
2577                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2578                 throw new WebApiException("Err:Invalid Json!", content, ex);
2579             }
2580         }
2581
2582         public void CreateListApi(string listName, bool isPrivate, string description)
2583         {
2584             this.CheckAccountState();
2585
2586             HttpStatusCode res;
2587             var content = "";
2588             try
2589             {
2590                 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2591             }
2592             catch(Exception ex)
2593             {
2594                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2595             }
2596
2597             this.CheckStatusCode(res, content);
2598
2599             try
2600             {
2601                 var le = TwitterList.ParseJson(content);
2602                 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2603             }
2604             catch(SerializationException ex)
2605             {
2606                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2607                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2608             }
2609             catch(Exception ex)
2610             {
2611                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2612                 throw new WebApiException("Err:Invalid Json!", content, ex);
2613             }
2614         }
2615
2616         public bool ContainsUserAtList(string listId, string user)
2617         {
2618             this.CheckAccountState();
2619
2620             HttpStatusCode res;
2621             var content = "";
2622
2623             try
2624             {
2625                 res = this.twCon.ShowListMember(listId, user, ref content);
2626             }
2627             catch(Exception ex)
2628             {
2629                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2630             }
2631
2632             if (res == HttpStatusCode.NotFound)
2633             {
2634                 return false;
2635             }
2636
2637             this.CheckStatusCode(res, content);
2638
2639             try
2640             {
2641                 TwitterUser.ParseJson(content);
2642                 return true;
2643             }
2644             catch(Exception)
2645             {
2646                 return false;
2647             }
2648         }
2649
2650         public void AddUserToList(string listId, string user)
2651         {
2652             HttpStatusCode res;
2653             var content = "";
2654
2655             try
2656             {
2657                 res = twCon.CreateListMembers(listId, user, ref content);
2658             }
2659             catch(Exception ex)
2660             {
2661                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2662             }
2663
2664             this.CheckStatusCode(res, content);
2665         }
2666
2667         public void RemoveUserToList(string listId, string user)
2668         {
2669             HttpStatusCode res;
2670             var content = "";
2671
2672             try
2673             {
2674                 res = twCon.DeleteListMembers(listId, user, ref content);
2675             }
2676             catch(Exception ex)
2677             {
2678                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2679             }
2680
2681             this.CheckStatusCode(res, content);
2682         }
2683
2684         public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2685         {
2686             if (entities != null)
2687             {
2688                 if (entities.Hashtags != null)
2689                 {
2690                     lock (this.LockObj)
2691                     {
2692                         this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2693                     }
2694                 }
2695                 if (entities.UserMentions != null)
2696                 {
2697                     foreach (var ent in entities.UserMentions)
2698                     {
2699                         var screenName = ent.ScreenName.ToLowerInvariant();
2700                         if (!AtList.Contains(screenName))
2701                             AtList.Add(screenName);
2702                     }
2703                 }
2704                 if (entities.Media != null)
2705                 {
2706                     if (media != null)
2707                     {
2708                         foreach (var ent in entities.Media)
2709                         {
2710                             if (!media.Any(x => x.Url == ent.MediaUrl))
2711                             {
2712                                 if (ent.VideoInfo != null &&
2713                                     ent.Type == "animated_gif" || ent.Type == "video")
2714                                 {
2715                                     //var videoUrl = ent.VideoInfo.Variants
2716                                     //    .Where(v => v.ContentType == "video/mp4")
2717                                     //    .OrderByDescending(v => v.Bitrate)
2718                                     //    .Select(v => v.Url).FirstOrDefault();
2719                                     media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
2720                                 }
2721                                 else
2722                                     media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
2723                             }
2724                         }
2725                     }
2726                 }
2727             }
2728
2729             // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2730             text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2731
2732             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>");
2733             text = PreProcessUrl(text); //IDN置換
2734
2735             return text;
2736         }
2737
2738         private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2739
2740         /// <summary>
2741         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2742         /// </summary>
2743         public static Tuple<string, Uri> ParseSource(string sourceHtml)
2744         {
2745             if (string.IsNullOrEmpty(sourceHtml))
2746                 return Tuple.Create<string, Uri>("", null);
2747
2748             string sourceText;
2749             Uri sourceUri;
2750
2751             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2752
2753             var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2754             if (match.Success)
2755             {
2756                 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2757                 try
2758                 {
2759                     var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2760                     sourceUri = new Uri(SourceUriBase, uriStr);
2761                 }
2762                 catch (UriFormatException)
2763                 {
2764                     sourceUri = null;
2765                 }
2766             }
2767             else
2768             {
2769                 sourceText = WebUtility.HtmlDecode(sourceHtml);
2770                 sourceUri = null;
2771             }
2772
2773             return Tuple.Create(sourceText, sourceUri);
2774         }
2775
2776         public TwitterApiStatus GetInfoApi()
2777         {
2778             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2779
2780             if (MyCommon._endingFlag) return null;
2781
2782             HttpStatusCode res;
2783             var content = "";
2784             try
2785             {
2786                 res = twCon.RateLimitStatus(ref content);
2787             }
2788             catch (Exception)
2789             {
2790                 this.ResetApiStatus();
2791                 return null;
2792             }
2793
2794             this.CheckStatusCode(res, content);
2795
2796             try
2797             {
2798                 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2799                 return MyCommon.TwitterApiInfo;
2800             }
2801             catch (Exception ex)
2802             {
2803                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2804                 MyCommon.TwitterApiInfo.Reset();
2805                 return null;
2806             }
2807         }
2808
2809         /// <summary>
2810         /// ブロック中のユーザーを更新します
2811         /// </summary>
2812         /// <exception cref="WebApiException"/>
2813         public void RefreshBlockIds()
2814         {
2815             if (MyCommon._endingFlag) return;
2816
2817             var cursor = -1L;
2818             var newBlockIds = new HashSet<long>();
2819             do
2820             {
2821                 var ret = this.GetBlockIdsApi(cursor);
2822                 newBlockIds.UnionWith(ret.Ids);
2823                 cursor = ret.NextCursor;
2824             } while (cursor != 0);
2825
2826             newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2827
2828             TabInformations.GetInstance().BlockIds = newBlockIds;
2829         }
2830
2831         public TwitterIds GetBlockIdsApi(long cursor)
2832         {
2833             this.CheckAccountState();
2834
2835             HttpStatusCode res;
2836             var content = "";
2837             try
2838             {
2839                 res = twCon.GetBlockUserIds(ref content, cursor);
2840             }
2841             catch(Exception e)
2842             {
2843                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2844             }
2845
2846             this.CheckStatusCode(res, content);
2847
2848             try
2849             {
2850                 return TwitterIds.ParseJson(content);
2851             }
2852             catch(SerializationException e)
2853             {
2854                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2855                 MyCommon.TraceOut(ex);
2856                 throw ex;
2857             }
2858             catch(Exception e)
2859             {
2860                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2861                 MyCommon.TraceOut(ex);
2862                 throw ex;
2863             }
2864         }
2865
2866         /// <summary>
2867         /// ミュート中のユーザーIDを更新します
2868         /// </summary>
2869         /// <exception cref="WebApiException"/>
2870         public async Task RefreshMuteUserIdsAsync()
2871         {
2872             if (MyCommon._endingFlag) return;
2873
2874             var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2875                 .ConfigureAwait(false);
2876
2877             TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2878         }
2879
2880         public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2881         {
2882             var content = "";
2883
2884             try
2885             {
2886                 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2887                     .ConfigureAwait(false);
2888
2889                 this.CheckStatusCode(res, content);
2890
2891                 return TwitterIds.ParseJson(content);
2892             }
2893             catch (WebException ex)
2894             {
2895                 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2896                 MyCommon.TraceOut(ex2);
2897                 throw ex2;
2898             }
2899             catch (SerializationException ex)
2900             {
2901                 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2902                 MyCommon.TraceOut(ex2);
2903                 throw ex2;
2904             }
2905         }
2906
2907         public string[] GetHashList()
2908         {
2909             string[] hashArray;
2910             lock (LockObj)
2911             {
2912                 hashArray = _hashList.ToArray();
2913                 _hashList.Clear();
2914             }
2915             return hashArray;
2916         }
2917
2918         public string AccessToken
2919         {
2920             get
2921             {
2922                 return twCon.AccessToken;
2923             }
2924         }
2925
2926         public string AccessTokenSecret
2927         {
2928             get
2929             {
2930                 return twCon.AccessTokenSecret;
2931             }
2932         }
2933
2934         private void CheckAccountState()
2935         {
2936             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2937                 throw new WebApiException("Auth error. Check your account");
2938         }
2939
2940         private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2941         {
2942             if (!this.AccessLevel.HasFlag(accessLevelFlags))
2943                 throw new WebApiException("Auth Err:try to re-authorization.");
2944         }
2945
2946         private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2947             [CallerMemberName] string callerMethodName = "")
2948         {
2949             if (httpStatus == HttpStatusCode.OK)
2950             {
2951                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2952                 return;
2953             }
2954
2955             if (string.IsNullOrWhiteSpace(responseText))
2956             {
2957                 if (httpStatus == HttpStatusCode.Unauthorized)
2958                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2959
2960                 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2961             }
2962
2963             try
2964             {
2965                 var errors = TwitterError.ParseJson(responseText).Errors;
2966                 if (errors == null || !errors.Any())
2967                 {
2968                     throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2969                 }
2970
2971                 foreach (var error in errors)
2972                 {
2973                     if (error.Code == TwitterErrorCode.InvalidToken ||
2974                         error.Code == TwitterErrorCode.SuspendedAccount)
2975                     {
2976                         Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2977                     }
2978                 }
2979
2980                 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2981             }
2982             catch (SerializationException) { }
2983
2984             throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2985         }
2986
2987         public int GetTextLengthRemain(string postText)
2988         {
2989             var matchDm = Twitter.DMSendTextRegex.Match(postText);
2990             if (matchDm.Success)
2991                 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2992
2993             return this.GetTextLengthRemainInternal(postText, isDm: false);
2994         }
2995
2996         private int GetTextLengthRemainInternal(string postText, bool isDm)
2997         {
2998             var textLength = 0;
2999
3000             var pos = 0;
3001             while (pos < postText.Length)
3002             {
3003                 textLength++;
3004
3005                 if (char.IsSurrogatePair(postText, pos))
3006                     pos += 2; // サロゲートペアの場合は2文字分進める
3007                 else
3008                     pos++;
3009             }
3010
3011             var urls = TweetExtractor.ExtractUrls(postText);
3012             foreach (var url in urls)
3013             {
3014                 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
3015                     ? this.Configuration.ShortUrlLengthHttps
3016                     : this.Configuration.ShortUrlLength;
3017
3018                 textLength += shortUrlLength - url.Length;
3019             }
3020
3021             if (isDm)
3022                 return this.Configuration.DmTextCharacterLimit - textLength;
3023             else
3024                 return 140 - textLength;
3025         }
3026
3027
3028 #region "UserStream"
3029         private string trackWord_ = "";
3030         public string TrackWord
3031         {
3032             get
3033             {
3034                 return trackWord_;
3035             }
3036             set
3037             {
3038                 trackWord_ = value;
3039             }
3040         }
3041         private bool allAtReply_ = false;
3042         public bool AllAtReply
3043         {
3044             get
3045             {
3046                 return allAtReply_;
3047             }
3048             set
3049             {
3050                 allAtReply_ = value;
3051             }
3052         }
3053
3054         public event EventHandler NewPostFromStream;
3055         public event EventHandler UserStreamStarted;
3056         public event EventHandler UserStreamStopped;
3057         public event EventHandler<PostDeletedEventArgs> PostDeleted;
3058         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3059         private DateTime _lastUserstreamDataReceived;
3060         private TwitterUserstream userStream;
3061
3062         public class FormattedEvent
3063         {
3064             public MyCommon.EVENTTYPE Eventtype { get; set; }
3065             public DateTime CreatedAt { get; set; }
3066             public string Event { get; set; }
3067             public string Username { get; set; }
3068             public string Target { get; set; }
3069             public Int64 Id { get; set; }
3070             public bool IsMe { get; set; }
3071         }
3072
3073         public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3074         public List<FormattedEvent> StoredEvent
3075         {
3076             get
3077             {
3078                 return storedEvent_;
3079             }
3080             set
3081             {
3082                 storedEvent_ = value;
3083             }
3084         }
3085
3086         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3087         {
3088             ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3089             ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3090             ["follow"] = MyCommon.EVENTTYPE.Follow,
3091             ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3092             ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3093             ["block"] = MyCommon.EVENTTYPE.Block,
3094             ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3095             ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3096             ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3097             ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3098             ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3099             ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3100             ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3101             ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3102             ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3103             ["mute"] = MyCommon.EVENTTYPE.Mute,
3104             ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3105             ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3106         };
3107
3108         public bool IsUserstreamDataReceived
3109         {
3110             get
3111             {
3112                 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3113             }
3114         }
3115
3116         private void userStream_StatusArrived(string line)
3117         {
3118             this._lastUserstreamDataReceived = DateTime.Now;
3119             if (string.IsNullOrEmpty(line)) return;
3120
3121             if (line.First() != '{' || line.Last() != '}')
3122             {
3123                 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3124                 return;
3125             }
3126
3127             var isDm = false;
3128
3129             try
3130             {
3131                 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3132                 {
3133                     var xElm = XElement.Load(jsonReader);
3134                     if (xElm.Element("friends") != null)
3135                     {
3136                         Debug.WriteLine("friends");
3137                         return;
3138                     }
3139                     else if (xElm.Element("delete") != null)
3140                     {
3141                         Debug.WriteLine("delete");
3142                         Int64 id;
3143                         XElement idElm;
3144                         if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3145                         {
3146                             id = 0;
3147                             long.TryParse(idElm.Value, out id);
3148
3149                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3150                         }
3151                         else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3152                         {
3153                             id = 0;
3154                             long.TryParse(idElm.Value, out id);
3155
3156                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3157                         }
3158                         else
3159                         {
3160                             MyCommon.TraceOut("delete:" + line);
3161                             return;
3162                         }
3163                         for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3164                         {
3165                             var sEvt = this.StoredEvent[i];
3166                             if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3167                             {
3168                                 this.StoredEvent.RemoveAt(i);
3169                             }
3170                         }
3171                         return;
3172                     }
3173                     else if (xElm.Element("limit") != null)
3174                     {
3175                         Debug.WriteLine(line);
3176                         return;
3177                     }
3178                     else if (xElm.Element("event") != null)
3179                     {
3180                         Debug.WriteLine("event: " + xElm.Element("event").Value);
3181                         CreateEventFromJson(line);
3182                         return;
3183                     }
3184                     else if (xElm.Element("direct_message") != null)
3185                     {
3186                         Debug.WriteLine("direct_message");
3187                         isDm = true;
3188                     }
3189                     else if (xElm.Element("retweeted_status") != null)
3190                     {
3191                         var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3192                         var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3193
3194                         // 自分に関係しないリツイートの場合は無視する
3195                         var selfUserId = this.UserId.ToString();
3196                         if (sourceUserId == selfUserId || targetUserId == selfUserId)
3197                         {
3198                             // 公式 RT をイベントとしても扱う
3199                             var evt = CreateEventFromRetweet(xElm);
3200                             if (evt != null)
3201                             {
3202                                 this.StoredEvent.Insert(0, evt);
3203
3204                                 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3205                             }
3206                         }
3207
3208                         // 従来通り公式 RT の表示も行うため return しない
3209                     }
3210                     else if (xElm.Element("scrub_geo") != null)
3211                     {
3212                         try
3213                         {
3214                             TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3215                                                                         long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3216                         }
3217                         catch(Exception)
3218                         {
3219                             MyCommon.TraceOut("scrub_geo:" + line);
3220                         }
3221                         return;
3222                     }
3223                 }
3224
3225                 if (isDm)
3226                 {
3227                     CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3228                 }
3229                 else
3230                 {
3231                     CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3232                 }
3233             }
3234             catch (WebApiException ex)
3235             {
3236                 MyCommon.TraceOut(ex);
3237                 return;
3238             }
3239             catch(NullReferenceException)
3240             {
3241                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3242             }
3243
3244             this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3245         }
3246
3247         /// <summary>
3248         /// UserStreamsから受信した公式RTをイベントに変換します
3249         /// </summary>
3250         private FormattedEvent CreateEventFromRetweet(XElement xElm)
3251         {
3252             return new FormattedEvent
3253             {
3254                 Eventtype = MyCommon.EVENTTYPE.Retweet,
3255                 Event = "retweet",
3256                 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3257                 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3258                 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3259                 Target = string.Format("@{0}:{1}", new[]
3260                 {
3261                     xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3262                     WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3263                 }),
3264                 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3265             };
3266         }
3267
3268         private void CreateEventFromJson(string content)
3269         {
3270             TwitterStreamEvent eventData = null;
3271             try
3272             {
3273                 eventData = TwitterStreamEvent.ParseJson(content);
3274             }
3275             catch(SerializationException ex)
3276             {
3277                 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3278             }
3279             catch(Exception ex)
3280             {
3281                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3282             }
3283
3284             var evt = new FormattedEvent();
3285             evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3286             evt.Event = eventData.Event;
3287             evt.Username = eventData.Source.ScreenName;
3288             evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3289
3290             MyCommon.EVENTTYPE eventType;
3291             eventTable.TryGetValue(eventData.Event, out eventType);
3292             evt.Eventtype = eventType;
3293
3294             TwitterStreamEvent<TwitterStatus> tweetEvent;
3295
3296             switch (eventData.Event)
3297             {
3298                 case "access_revoked":
3299                 case "access_unrevoked":
3300                 case "user_delete":
3301                 case "user_suspend":
3302                     return;
3303                 case "follow":
3304                     if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3305                     {
3306                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3307                     }
3308                     else
3309                     {
3310                         return;    //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3311                     }
3312                     evt.Target = "";
3313                     break;
3314                 case "unfollow":
3315                     evt.Target = "@" + eventData.Target.ScreenName;
3316                     break;
3317                 case "favorited_retweet":
3318                 case "retweeted_retweet":
3319                     return;
3320                 case "favorite":
3321                 case "unfavorite":
3322                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3323                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3324                     evt.Id = tweetEvent.TargetObject.Id;
3325
3326                     if (SettingCommon.Instance.IsRemoveSameEvent)
3327                     {
3328                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3329                             return;
3330                     }
3331
3332                     var tabinfo = TabInformations.GetInstance();
3333
3334                     PostClass post;
3335                     var statusId = tweetEvent.TargetObject.Id;
3336                     if (!tabinfo.Posts.TryGetValue(statusId, out post))
3337                         break;
3338
3339                     if (eventData.Event == "favorite")
3340                     {
3341                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3342                         if (!favTab.Contains(post.StatusId))
3343                             favTab.AddPostImmediately(post.StatusId, post.IsRead);
3344
3345                         if (tweetEvent.Source.Id == this.UserId)
3346                         {
3347                             post.IsFav = true;
3348                         }
3349                         else if (tweetEvent.Target.Id == this.UserId)
3350                         {
3351                             post.FavoritedCount++;
3352
3353                             if (SettingCommon.Instance.FavEventUnread)
3354                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
3355                         }
3356                     }
3357                     else // unfavorite
3358                     {
3359                         if (tweetEvent.Source.Id == this.UserId)
3360                         {
3361                             post.IsFav = false;
3362                         }
3363                         else if (tweetEvent.Target.Id == this.UserId)
3364                         {
3365                             post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3366                         }
3367                     }
3368                     break;
3369                 case "quoted_tweet":
3370                     if (evt.IsMe) return;
3371
3372                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3373                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3374                     evt.Id = tweetEvent.TargetObject.Id;
3375
3376                     if (SettingCommon.Instance.IsRemoveSameEvent)
3377                     {
3378                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3379                             return;
3380                     }
3381                     break;
3382                 case "list_member_added":
3383                 case "list_member_removed":
3384                 case "list_created":
3385                 case "list_destroyed":
3386                 case "list_updated":
3387                 case "list_user_subscribed":
3388                 case "list_user_unsubscribed":
3389                     var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3390                     evt.Target = listEvent.TargetObject.FullName;
3391                     break;
3392                 case "block":
3393                     if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3394                     evt.Target = "";
3395                     break;
3396                 case "unblock":
3397                     if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3398                     evt.Target = "";
3399                     break;
3400                 case "user_update":
3401                     evt.Target = "";
3402                     break;
3403                 
3404                 // Mute / Unmute
3405                 case "mute":
3406                     evt.Target = "@" + eventData.Target.ScreenName;
3407                     if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3408                     {
3409                         TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3410                     }
3411                     break;
3412                 case "unmute":
3413                     evt.Target = "@" + eventData.Target.ScreenName;
3414                     if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3415                     {
3416                         TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3417                     }
3418                     break;
3419
3420                 default:
3421                     MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3422                     break;
3423             }
3424             this.StoredEvent.Insert(0, evt);
3425
3426             this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3427         }
3428
3429         private void userStream_Started()
3430         {
3431             this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3432         }
3433
3434         private void userStream_Stopped()
3435         {
3436             this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3437         }
3438
3439         public bool UserStreamEnabled
3440         {
3441             get
3442             {
3443                 return userStream == null ? false : userStream.Enabled;
3444             }
3445         }
3446
3447         public void StartUserStream()
3448         {
3449             if (userStream != null)
3450             {
3451                 StopUserStream();
3452             }
3453             userStream = new TwitterUserstream(twCon);
3454             userStream.StatusArrived += userStream_StatusArrived;
3455             userStream.Started += userStream_Started;
3456             userStream.Stopped += userStream_Stopped;
3457             userStream.Start(this.AllAtReply, this.TrackWord);
3458         }
3459
3460         public void StopUserStream()
3461         {
3462             userStream?.Dispose();
3463             userStream = null;
3464             if (!MyCommon._endingFlag)
3465             {
3466                 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3467             }
3468         }
3469
3470         public void ReconnectUserStream()
3471         {
3472             if (userStream != null)
3473             {
3474                 this.StartUserStream();
3475             }
3476         }
3477
3478         private class TwitterUserstream : IDisposable
3479         {
3480             public event Action<string> StatusArrived;
3481             public event Action Stopped;
3482             public event Action Started;
3483             private HttpTwitter twCon;
3484
3485             private Thread _streamThread;
3486             private bool _streamActive;
3487
3488             private bool _allAtreplies = false;
3489             private string _trackwords = "";
3490
3491             public TwitterUserstream(HttpTwitter twitterConnection)
3492             {
3493                 twCon = (HttpTwitter)twitterConnection.Clone();
3494             }
3495
3496             public void Start(bool allAtReplies, string trackwords)
3497             {
3498                 this.AllAtReplies = allAtReplies;
3499                 this.TrackWords = trackwords;
3500                 _streamActive = true;
3501                 if (_streamThread != null && _streamThread.IsAlive) return;
3502                 _streamThread = new Thread(UserStreamLoop);
3503                 _streamThread.Name = "UserStreamReceiver";
3504                 _streamThread.IsBackground = true;
3505                 _streamThread.Start();
3506             }
3507
3508             public bool Enabled
3509             {
3510                 get
3511                 {
3512                     return _streamActive;
3513                 }
3514             }
3515
3516             public bool AllAtReplies
3517             {
3518                 get
3519                 {
3520                     return _allAtreplies;
3521                 }
3522                 set
3523                 {
3524                     _allAtreplies = value;
3525                 }
3526             }
3527
3528             public string TrackWords
3529             {
3530                 get
3531                 {
3532                     return _trackwords;
3533                 }
3534                 set
3535                 {
3536                     _trackwords = value;
3537                 }
3538             }
3539
3540             private void UserStreamLoop()
3541             {
3542                 var sleepSec = 0;
3543                 do
3544                 {
3545                     Stream st = null;
3546                     StreamReader sr = null;
3547                     try
3548                     {
3549                         if (!MyCommon.IsNetworkAvailable())
3550                         {
3551                             sleepSec = 30;
3552                             continue;
3553                         }
3554
3555                         Started?.Invoke();
3556
3557                         var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3558
3559                         switch (res)
3560                         {
3561                             case HttpStatusCode.OK:
3562                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3563                                 break;
3564                             case HttpStatusCode.Unauthorized:
3565                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3566                                 sleepSec = 120;
3567                                 continue;
3568                         }
3569
3570                         if (st == null)
3571                         {
3572                             sleepSec = 30;
3573                             //MyCommon.TraceOut("Stop:stream is null")
3574                             continue;
3575                         }
3576
3577                         sr = new StreamReader(st);
3578
3579                         while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3580                         {
3581                             StatusArrived?.Invoke(sr.ReadLine());
3582                             //this.LastTime = Now;
3583                         }
3584
3585                         if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3586                         {
3587                             sleepSec = 30;
3588                             //MyCommon.TraceOut("Stop:EndOfStream")
3589                             continue;
3590                         }
3591                         break;
3592                     }
3593                     catch(WebException ex)
3594                     {
3595                         if (ex.Status == WebExceptionStatus.Timeout)
3596                         {
3597                             sleepSec = 30;                        //MyCommon.TraceOut("Stop:Timeout")
3598                         }
3599                         else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3600                         {
3601                             //MyCommon.TraceOut("Stop:Connection Limit")
3602                             break;
3603                         }
3604                         else
3605                         {
3606                             sleepSec = 30;
3607                             //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3608                         }
3609                     }
3610                     catch(ThreadAbortException)
3611                     {
3612                         break;
3613                     }
3614                     catch(IOException)
3615                     {
3616                         sleepSec = 30;
3617                         //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3618                     }
3619                     catch(ArgumentException ex)
3620                     {
3621                         //System.ArgumentException: ストリームを読み取れませんでした。
3622                         //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3623                         sleepSec = 30;
3624                         MyCommon.TraceOut(ex, "Stop:ArgumentException");
3625                     }
3626                     catch(Exception ex)
3627                     {
3628                         MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3629                         MyCommon.ExceptionOut(ex);
3630                         sleepSec = 30;
3631                     }
3632                     finally
3633                     {
3634                         if (_streamActive)
3635                         {
3636                             Stopped?.Invoke();
3637                         }
3638                         twCon.RequestAbort();
3639                         sr?.Close();
3640                         if (sleepSec > 0)
3641                         {
3642                             var ms = 0;
3643                             while (_streamActive && ms < sleepSec * 1000)
3644                             {
3645                                 Thread.Sleep(500);
3646                                 ms += 500;
3647                             }
3648                         }
3649                         sleepSec = 0;
3650                     }
3651                 } while (this._streamActive);
3652
3653                 if (_streamActive)
3654                 {
3655                     Stopped?.Invoke();
3656                 }
3657                 MyCommon.TraceOut("Stop:EndLoop");
3658             }
3659
3660 #region "IDisposable Support"
3661             private bool disposedValue; // 重複する呼び出しを検出するには
3662
3663             // IDisposable
3664             protected virtual void Dispose(bool disposing)
3665             {
3666                 if (!this.disposedValue)
3667                 {
3668                     if (disposing)
3669                     {
3670                         _streamActive = false;
3671                         if (_streamThread != null && _streamThread.IsAlive)
3672                         {
3673                             _streamThread.Abort();
3674                         }
3675                     }
3676                 }
3677                 this.disposedValue = true;
3678             }
3679
3680             //protected Overrides void Finalize()
3681             //{
3682             //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3683             //    Dispose(false)
3684             //    MyBase.Finalize()
3685             //}
3686
3687             // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3688             public void Dispose()
3689             {
3690                 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3691                 Dispose(true);
3692                 GC.SuppressFinalize(this);
3693             }
3694 #endregion
3695
3696         }
3697 #endregion
3698
3699 #region "IDisposable Support"
3700         private bool disposedValue; // 重複する呼び出しを検出するには
3701
3702         // IDisposable
3703         protected virtual void Dispose(bool disposing)
3704         {
3705             if (!this.disposedValue)
3706             {
3707                 if (disposing)
3708                 {
3709                     this.StopUserStream();
3710                 }
3711             }
3712             this.disposedValue = true;
3713         }
3714
3715         //protected Overrides void Finalize()
3716         //{
3717         //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3718         //    Dispose(false)
3719         //    MyBase.Finalize()
3720         //}
3721
3722         // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3723         public void Dispose()
3724         {
3725             // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3726             Dispose(true);
3727             GC.SuppressFinalize(this);
3728         }
3729 #endregion
3730     }
3731
3732     public class PostDeletedEventArgs : EventArgs
3733     {
3734         public long StatusId { get; }
3735
3736         public PostDeletedEventArgs(long statusId)
3737         {
3738             this.StatusId = statusId;
3739         }
3740     }
3741
3742     public class UserStreamEventReceivedEventArgs : EventArgs
3743     {
3744         public Twitter.FormattedEvent EventData { get; }
3745
3746         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3747         {
3748             this.EventData = eventData;
3749         }
3750     }
3751 }