OSDN Git Service

IApiConnection, IHttpRequest, ApiResponseで構成する新しいTwitterApiConnectionを実装
[opentween/open-tween.git] / OpenTween / MyCommon.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) 2011      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 #nullable enable
29
30 using System;
31 using System.Collections;
32 using System.Collections.Generic;
33 using System.ComponentModel;
34 using System.Diagnostics;
35 using System.Diagnostics.CodeAnalysis;
36 using System.Drawing;
37 using System.Drawing.Imaging;
38 using System.Globalization;
39 using System.IO;
40 using System.Linq;
41 using System.Net;
42 using System.Net.Http;
43 using System.Net.NetworkInformation;
44 using System.Reflection;
45 using System.Runtime.InteropServices;
46 using System.Runtime.Serialization.Json;
47 using System.Security.Cryptography;
48 using System.Security.Principal;
49 using System.Text;
50 using System.Text.RegularExpressions;
51 using System.Threading.Tasks;
52 using System.Web;
53 using System.Windows.Forms;
54 using OpenTween.Api;
55 using OpenTween.Models;
56 using OpenTween.Setting;
57
58 namespace OpenTween
59 {
60     public static class MyCommon
61     {
62         private static readonly object LockObj = new();
63
64         public static bool EndingFlag { get; set; } // 終了フラグ
65
66         public enum IconSizes
67         {
68             IconNone = 0,
69             Icon16 = 1,
70             Icon24 = 2,
71             Icon48 = 3,
72             Icon48_2 = 4,
73         }
74
75         public enum NameBalloonEnum
76         {
77             None,
78             UserID,
79             NickName,
80         }
81
82         public enum DispTitleEnum
83         {
84             None,
85             Ver,
86             Post,
87             UnreadRepCount,
88             UnreadAllCount,
89             UnreadAllRepCount,
90             UnreadCountAllCount,
91             OwnStatus,
92         }
93
94         public enum LogUnitEnum
95         {
96             Minute,
97             Hour,
98             Day,
99         }
100
101         public enum UploadFileType
102         {
103             Invalid,
104             Picture,
105             MultiMedia,
106         }
107
108         public enum UrlConverter
109         {
110             TinyUrl,
111             Isgd,
112             Bitly,
113             Jmp,
114             Uxnu,
115             // 特殊
116             Nicoms,
117             // 廃止
118             Unu = -1,
119             Twurl = -1,
120         }
121
122         public enum ListItemDoubleClickActionType
123         {
124             // 設定ファイルの互換性を保つため新規の項目は途中に追加しないこと
125             Reply,
126             Favorite,
127             ShowProfile,
128             ShowTimeline,
129             ShowRelated,
130             OpenHomeInBrowser,
131             OpenStatusInBrowser,
132             None,
133             ReplyAll,
134         }
135
136         public enum HITRESULT
137         {
138             None,
139             Copy,
140             CopyAndMark,
141             Move,
142             Exclude,
143         }
144
145         public enum HttpTimeOut
146         {
147             MinValue = 10,
148             MaxValue = 1000,
149             DefaultValue = 20,
150         }
151
152         // Backgroundworkerへ処理種別を通知するための引数用enum
153         public enum WORKERTYPE
154         {
155             Timeline, // タイムライン取得
156             Reply, // 返信取得
157             DirectMessegeRcv, // 受信DM取得
158             DirectMessegeSnt, // 送信DM取得
159             PostMessage, // 発言POST
160             FavAdd, // Fav追加
161             FavRemove, // Fav削除
162             Follower, // Followerリスト取得
163             Favorites, // Fav取得
164             Retweet, // Retweetする
165             PublicSearch, // 公式検索
166             List, // Lists
167             Related, // 関連発言
168             UserTimeline, // UserTimeline
169             BlockIds, // Blocking/ids
170             Configuration, // Twitter Configuration読み込み
171             NoRetweetIds, // RT非表示ユーザー取得
172             //////
173             ErrorState, // エラー表示のみで後処理終了(認証エラー時など)
174         }
175
176         public static class DEFAULTTAB
177         {
178             public const string RECENT = "Recent";
179             public const string REPLY = "Reply";
180             public const string DM = "Direct";
181             public const string FAV = "Favorites";
182             public static readonly string MUTE = Properties.Resources.MuteTabName;
183         }
184
185         public static readonly object? Block = null;
186         public static bool TraceFlag = false;
187
188 #if DEBUG
189         public static bool DebugBuild = true;
190 #else
191         public static bool DebugBuild = false;
192 #endif
193
194         public enum ACCOUNT_STATE
195         {
196             Valid,
197             Invalid,
198         }
199
200         public enum REPLY_ICONSTATE
201         {
202             None,
203             StaticIcon,
204             BlinkIcon,
205         }
206
207         public static _Assembly EntryAssembly { get; internal set; }
208
209         public static string FileVersion { get; internal set; }
210
211         static MyCommon()
212         {
213             var assembly = Assembly.GetExecutingAssembly();
214             MyCommon.EntryAssembly = assembly;
215
216             var assemblyVersion = assembly.GetName().Version;
217             MyCommon.FileVersion = assemblyVersion.ToString();
218         }
219
220         public static string GetErrorLogPath()
221             => Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
222
223         public static void TraceOut(WebApiException ex)
224         {
225             var message = ExceptionOutMessage(ex);
226
227             if (ex.ResponseText != null)
228                 message += Environment.NewLine + "------- Response Data -------" + Environment.NewLine + ex.ResponseText;
229
230             TraceOut(TraceFlag, message);
231         }
232
233         public static void TraceOut(Exception ex, string message)
234         {
235             var buf = ExceptionOutMessage(ex);
236             TraceOut(TraceFlag, message + Environment.NewLine + buf);
237         }
238
239         public static void TraceOut(string message)
240             => TraceOut(TraceFlag, message);
241
242         public static void TraceOut(bool outputFlag, string message)
243         {
244             lock (LockObj)
245             {
246                 if (!outputFlag) return;
247
248                 var logPath = MyCommon.GetErrorLogPath();
249                 if (!Directory.Exists(logPath))
250                     Directory.CreateDirectory(logPath);
251
252                 var now = DateTimeUtc.Now;
253                 var fileName = $"{ApplicationSettings.AssemblyName}Trace-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
254                 fileName = Path.Combine(logPath, fileName);
255
256                 using var writer = new StreamWriter(fileName);
257
258                 writer.WriteLine("**** TraceOut: {0} ****", now.ToLocalTimeString());
259                 writer.WriteLine(Properties.Resources.TraceOutText1, ApplicationSettings.FeedbackEmailAddress);
260                 writer.WriteLine(Properties.Resources.TraceOutText2, ApplicationSettings.FeedbackTwitterName);
261                 writer.WriteLine();
262                 writer.WriteLine(Properties.Resources.TraceOutText3);
263                 writer.WriteLine(Properties.Resources.TraceOutText4, Environment.OSVersion.VersionString);
264                 writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version);
265                 writer.WriteLine(Properties.Resources.TraceOutText6, ApplicationSettings.AssemblyName, FileVersion);
266                 writer.WriteLine(message);
267                 writer.WriteLine();
268             }
269         }
270
271         // エラー内容をバッファに書き出し
272         // 注意:最終的にファイル出力されるエラーログに記録されるため次の情報は書き出さない
273         // 文頭メッセージ、権限、動作環境
274         // Dataプロパティにある終了許可フラグのパースもここで行う
275
276         public static string ExceptionOutMessage(Exception ex)
277         {
278             var isTerminatePermission = true;
279             return ExceptionOutMessage(ex, ref isTerminatePermission);
280         }
281
282         public static string ExceptionOutMessage(Exception ex, ref bool isTerminatePermission)
283         {
284             if (ex == null) return "";
285
286             var buf = new StringBuilder();
287
288             buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, ex.GetType().FullName, ex.Message);
289             buf.AppendLine();
290             if (ex.Data != null)
291             {
292                 var needHeader = true;
293                 foreach (DictionaryEntry dt in ex.Data)
294                 {
295                     if (needHeader)
296                     {
297                         buf.AppendLine();
298                         buf.AppendLine("-------Extra Information-------");
299                         needHeader = false;
300                     }
301                     buf.AppendFormat("{0}  :  {1}", dt.Key, dt.Value);
302                     buf.AppendLine();
303                     if (dt.Key.Equals("IsTerminatePermission"))
304                     {
305                         isTerminatePermission = (bool)dt.Value;
306                     }
307                 }
308                 if (!needHeader)
309                 {
310                     buf.AppendLine("-----End Extra Information-----");
311                 }
312             }
313             buf.AppendLine(ex.StackTrace);
314             buf.AppendLine();
315
316             // InnerExceptionが存在する場合書き出す
317             var innerException = ex.InnerException;
318             var nesting = 0;
319             while (innerException != null)
320             {
321                 buf.AppendFormat("-----InnerException[{0}]-----\r\n", nesting);
322                 buf.AppendLine();
323                 buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, innerException.GetType().FullName, innerException.Message);
324                 buf.AppendLine();
325                 if (innerException.Data != null)
326                 {
327                     var needHeader = true;
328
329                     foreach (DictionaryEntry dt in innerException.Data)
330                     {
331                         if (needHeader)
332                         {
333                             buf.AppendLine();
334                             buf.AppendLine("-------Extra Information-------");
335                             needHeader = false;
336                         }
337                         buf.AppendFormat("{0}  :  {1}", dt.Key, dt.Value);
338                         if (dt.Key.Equals("IsTerminatePermission"))
339                         {
340                             isTerminatePermission = (bool)dt.Value;
341                         }
342                     }
343                     if (!needHeader)
344                     {
345                         buf.AppendLine("-----End Extra Information-----");
346                     }
347                 }
348                 buf.AppendLine(innerException.StackTrace);
349                 buf.AppendLine();
350                 nesting++;
351                 innerException = innerException.InnerException;
352             }
353             return buf.ToString();
354         }
355
356         public static bool ExceptionOut(Exception ex)
357         {
358             lock (LockObj)
359             {
360                 var isTerminatePermission = true;
361
362                 var ident = WindowsIdentity.GetCurrent();
363                 var princ = new WindowsPrincipal(ident);
364                 var now = DateTimeUtc.Now;
365
366                 var errorReport = string.Join(Environment.NewLine,
367                     string.Format(Properties.Resources.UnhandledExceptionText1, now.ToLocalTimeString()),
368
369                     // 権限書き出し
370                     string.Format(Properties.Resources.UnhandledExceptionText11 + princ.IsInRole(WindowsBuiltInRole.Administrator)),
371                     string.Format(Properties.Resources.UnhandledExceptionText12 + princ.IsInRole(WindowsBuiltInRole.User)),
372                     "",
373
374                     // OSVersion,AppVersion書き出し
375                     string.Format(Properties.Resources.UnhandledExceptionText4),
376                     string.Format(Properties.Resources.UnhandledExceptionText5, Environment.OSVersion.VersionString),
377                     string.Format(Properties.Resources.UnhandledExceptionText6, Environment.Version),
378                     string.Format(Properties.Resources.UnhandledExceptionText7, ApplicationSettings.AssemblyName, FileVersion),
379
380                     ExceptionOutMessage(ex, ref isTerminatePermission));
381
382                 var logPath = MyCommon.GetErrorLogPath();
383                 if (!Directory.Exists(logPath))
384                     Directory.CreateDirectory(logPath);
385
386                 var fileName = $"{ApplicationSettings.AssemblyName}-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
387                 using (var writer = new StreamWriter(Path.Combine(logPath, fileName)))
388                 {
389                     writer.Write(errorReport);
390                 }
391
392                 var settings = SettingManager.Instance;
393                 var mainForm = Application.OpenForms.OfType<TweenMain>().FirstOrDefault();
394
395                 ErrorReport report;
396                 if (mainForm != null && !mainForm.IsDisposed)
397                     report = new ErrorReport(mainForm.TwitterInstance, errorReport);
398                 else
399                     report = new ErrorReport(errorReport);
400
401                 report.AnonymousReport = settings.Common.ErrorReportAnonymous;
402
403                 OpenErrorReportDialog(mainForm, report);
404
405                 // ダイアログ内で設定が変更されていれば保存する
406                 if (settings.Common.ErrorReportAnonymous != report.AnonymousReport)
407                 {
408                     settings.Common.ErrorReportAnonymous = report.AnonymousReport;
409                     settings.SaveCommon();
410                 }
411
412                 return false;
413             }
414         }
415
416         private static void OpenErrorReportDialog(Form? owner, ErrorReport report)
417         {
418             if (owner != null && owner.InvokeRequired)
419             {
420                 owner.Invoke((Action)(() => OpenErrorReportDialog(owner, report)));
421                 return;
422             }
423
424             using var dialog = new SendErrorReportForm();
425             dialog.ErrorReport = report;
426             dialog.ShowDialog(owner);
427         }
428
429         /// <summary>
430         /// URLのドメイン名をPunycode展開します。
431         /// <para>
432         /// ドメイン名がIDNでない場合はそのまま返します。
433         /// ドメインラベルの区切り文字はFULLSTOP(.、U002E)に置き換えられます。
434         /// </para>
435         /// </summary>
436         /// <param name="inputUrl">展開対象のURL</param>
437         /// <returns>IDNが含まれていた場合はPunycodeに展開したURLをを返します。Punycode展開時にエラーが発生した場合はnullを返します。</returns>
438         public static string? IDNEncode(string inputUrl)
439         {
440             try
441             {
442                 var uriBuilder = new UriBuilder(inputUrl);
443
444                 var idnConverter = new IdnMapping();
445                 uriBuilder.Host = idnConverter.GetAscii(uriBuilder.Host);
446
447                 return uriBuilder.Uri.AbsoluteUri;
448             }
449             catch (Exception)
450             {
451                 return null;
452             }
453         }
454
455         public static string IDNDecode(string inputUrl)
456         {
457             try
458             {
459                 var uriBuilder = new UriBuilder(inputUrl);
460
461                 if (uriBuilder.Host != null)
462                 {
463                     var idnConverter = new IdnMapping();
464                     uriBuilder.Host = idnConverter.GetUnicode(uriBuilder.Host);
465                 }
466
467                 return uriBuilder.Uri.AbsoluteUri;
468             }
469             catch (Exception)
470             {
471                 return inputUrl;
472             }
473         }
474
475         /// <summary>
476         /// URL を画面上で人間に読みやすい文字列に変換する(エスケープ解除など)
477         /// </summary>
478         public static string ConvertToReadableUrl(string inputUrl)
479         {
480             try
481             {
482                 var outputUrl = inputUrl;
483
484                 // Punycodeをデコードする
485                 outputUrl = MyCommon.IDNDecode(outputUrl);
486
487                 // URL内で特殊な意味を持つ記号は元の文字に変換されることを避けるために二重エスケープする
488                 // 参考: Firefoxの losslessDecodeURI() 関数
489                 //   http://hg.mozilla.org/mozilla-central/annotate/FIREFOX_AURORA_27_BASE/browser/base/content/browser.js#l2128
490                 outputUrl = Regex.Replace(outputUrl, @"%(2[3456BCF]|3[ABDF]|40)", @"%25$1", RegexOptions.IgnoreCase);
491
492                 // エスケープを解除する
493                 outputUrl = Uri.UnescapeDataString(outputUrl);
494
495                 return outputUrl;
496             }
497             catch (UriFormatException)
498             {
499                 return inputUrl;
500             }
501         }
502
503         public static void MoveArrayItem(int[] values, int idx_fr, int idx_to)
504         {
505             var moved_value = values[idx_fr];
506             var num_moved = Math.Abs(idx_fr - idx_to);
507
508             if (idx_to < idx_fr)
509             {
510                 Array.Copy(values, idx_to, values, idx_to + 1, num_moved);
511             }
512             else
513             {
514                 Array.Copy(values, idx_fr + 1, values, idx_fr, num_moved);
515             }
516
517             values[idx_to] = moved_value;
518         }
519
520         public static string EncryptString(string str)
521         {
522             if (MyCommon.IsNullOrEmpty(str)) return "";
523
524             // 文字列をバイト型配列にする
525             var bytesIn = Encoding.UTF8.GetBytes(str);
526
527             // DESCryptoServiceProviderオブジェクトの作成
528             using var des = new DESCryptoServiceProvider();
529
530             // 共有キーと初期化ベクタを決定
531             // パスワードをバイト配列にする
532             var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
533             // 共有キーと初期化ベクタを設定
534             des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
535             des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
536
537             MemoryStream? msOut = null;
538             ICryptoTransform? desdecrypt = null;
539
540             try
541             {
542                 // 暗号化されたデータを書き出すためのMemoryStream
543                 msOut = new MemoryStream();
544
545                 // DES暗号化オブジェクトの作成
546                 desdecrypt = des.CreateEncryptor();
547
548                 // 書き込むためのCryptoStreamの作成
549                 using var cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write);
550
551                 // Disposeが重複して呼ばれないようにする
552                 var msTmp = msOut;
553                 msOut = null;
554                 desdecrypt = null;
555
556                 // 書き込む
557                 cryptStream.Write(bytesIn, 0, bytesIn.Length);
558                 cryptStream.FlushFinalBlock();
559                 // 暗号化されたデータを取得
560                 var bytesOut = msTmp.ToArray();
561
562                 // Base64で文字列に変更して結果を返す
563                 return Convert.ToBase64String(bytesOut);
564             }
565             finally
566             {
567                 msOut?.Dispose();
568                 desdecrypt?.Dispose();
569             }
570         }
571
572         public static string DecryptString(string str)
573         {
574             if (MyCommon.IsNullOrEmpty(str)) return "";
575
576             // DESCryptoServiceProviderオブジェクトの作成
577             using var des = new DESCryptoServiceProvider();
578
579             // 共有キーと初期化ベクタを決定
580             // パスワードをバイト配列にする
581             var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
582             // 共有キーと初期化ベクタを設定
583             des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
584             des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
585
586             // Base64で文字列をバイト配列に戻す
587             var bytesIn = Convert.FromBase64String(str);
588
589             MemoryStream? msIn = null;
590             ICryptoTransform? desdecrypt = null;
591             CryptoStream? cryptStreem = null;
592
593             try
594             {
595                 // 暗号化されたデータを読み込むためのMemoryStream
596                 msIn = new MemoryStream(bytesIn);
597                 // DES復号化オブジェクトの作成
598                 desdecrypt = des.CreateDecryptor();
599                 // 読み込むためのCryptoStreamの作成
600                 cryptStreem = new CryptoStream(msIn, desdecrypt, CryptoStreamMode.Read);
601
602                 // Disposeが重複して呼ばれないようにする
603                 msIn = null;
604                 desdecrypt = null;
605
606                 // 復号化されたデータを取得するためのStreamReader
607                 using var srOut = new StreamReader(cryptStreem, Encoding.UTF8);
608
609                 // Disposeが重複して呼ばれないようにする
610                 cryptStreem = null;
611
612                 // 復号化されたデータを取得する
613                 var result = srOut.ReadToEnd();
614
615                 return result;
616             }
617             finally
618             {
619                 msIn?.Dispose();
620                 desdecrypt?.Dispose();
621                 cryptStreem?.Dispose();
622             }
623         }
624
625         public static byte[] ResizeBytesArray(byte[] bytes,
626                                     int newSize)
627         {
628             var newBytes = new byte[newSize];
629             if (bytes.Length <= newSize)
630             {
631                 foreach (var i in Enumerable.Range(0, bytes.Length))
632                 {
633                     newBytes[i] = bytes[i];
634                 }
635             }
636             else
637             {
638                 var pos = 0;
639                 foreach (var i in Enumerable.Range(0, bytes.Length))
640                 {
641                     newBytes[pos] = unchecked((byte)(newBytes[pos] ^ bytes[i]));
642                     pos++;
643                     if (pos >= newBytes.Length)
644                     {
645                         pos = 0;
646                     }
647                 }
648             }
649             return newBytes;
650         }
651
652         [Flags]
653         public enum TabUsageType
654         {
655             Undefined = 0,
656             Home = 1, // Unique
657             Mentions = 2, // Unique
658             DirectMessage = 4, // Unique
659             Favorites = 8, // Unique
660             UserDefined = 16,
661             LocalQuery = 32, // Pin(no save/no save query/distribute/no update(normal update))
662             Profile = 64, // Pin(save/no distribute/manual update)
663             PublicSearch = 128, // Pin(save/no distribute/auto update)
664             Lists = 256,
665             Related = 512,
666             UserTimeline = 1024,
667             Mute = 2048,
668             SearchResults = 4096,
669         }
670
671         public static TwitterApiStatus TwitterApiInfo = new();
672
673         public static bool IsAnimatedGif(string filename)
674         {
675             Image? img = null;
676             try
677             {
678                 img = Image.FromFile(filename);
679                 if (img == null) return false;
680                 if (img.RawFormat.Guid == ImageFormat.Gif.Guid)
681                 {
682                     var fd = new FrameDimension(img.FrameDimensionsList[0]);
683                     var fd_count = img.GetFrameCount(fd);
684                     if (fd_count > 1)
685                     {
686                         return true;
687                     }
688                     else
689                     {
690                         return false;
691                     }
692                 }
693                 return false;
694             }
695             catch (Exception)
696             {
697                 return false;
698             }
699             finally
700             {
701                 img?.Dispose();
702             }
703         }
704
705         public static DateTimeUtc DateTimeParse(string input)
706         {
707             var formats = new[]
708             {
709                 "ddd MMM dd HH:mm:ss zzzz yyyy",
710                 "ddd, d MMM yyyy HH:mm:ss zzzz",
711             };
712
713             if (DateTimeUtc.TryParseExact(input, formats, DateTimeFormatInfo.InvariantInfo, out var result))
714                 return result;
715
716             TraceOut("Parse Error(DateTimeFormat) : " + input);
717
718             return DateTimeUtc.Now;
719         }
720
721         public static T CreateDataFromJson<T>(string content)
722             => MyCommon.CreateDataFromJson<T>(Encoding.UTF8.GetBytes(content));
723
724         public static T CreateDataFromJson<T>(byte[] bytes)
725         {
726             using var stream = new MemoryStream(bytes);
727             var settings = new DataContractJsonSerializerSettings
728             {
729                 UseSimpleDictionaryFormat = true,
730             };
731             return (T)new DataContractJsonSerializer(typeof(T), settings).ReadObject(stream);
732         }
733
734         public static bool IsNetworkAvailable()
735         {
736             try
737             {
738                 return NetworkInterface.GetIsNetworkAvailable();
739             }
740             catch (Exception)
741             {
742                 return false;
743             }
744         }
745
746         public static bool IsValidEmail(string strIn)
747         {
748             var pattern = @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" +
749                 @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$";
750
751             // Return true if strIn is in valid e-mail format.
752             return Regex.IsMatch(strIn, pattern);
753         }
754
755         /// <summary>
756         /// 指定された修飾キーが押されている状態かを取得します。
757         /// </summary>
758         /// <param name="keys">状態を調べるキー</param>
759         /// <returns><paramref name="keys"/> で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。</returns>
760         public static bool IsKeyDown(params Keys[] keys)
761             => MyCommon.IsKeyDownInternal(Control.ModifierKeys, keys);
762
763         internal static bool IsKeyDownInternal(Keys modifierKeys, Keys[] targetKeys)
764         {
765             foreach (var key in targetKeys)
766             {
767                 if ((modifierKeys & key) != key)
768                 {
769                     return false;
770                 }
771             }
772             return true;
773         }
774
775         /// <summary>
776         /// アプリケーションのアセンブリ名を取得します。
777         /// </summary>
778         /// <remarks>
779         /// VB.NETの<code>My.Application.Info.AssemblyName</code>と(ほぼ)同じ動作をします。
780         /// </remarks>
781         /// <returns>アプリケーションのアセンブリ名</returns>
782         public static string GetAssemblyName()
783             => MyCommon.EntryAssembly.GetName().Name;
784
785         /// <summary>
786         /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
787         /// </summary>
788         /// <param name="orig">対象となる文字列</param>
789         /// <returns>置換後の文字列</returns>
790         public static string ReplaceAppName(string orig)
791             => MyCommon.ReplaceAppName(orig, ApplicationSettings.ApplicationName);
792
793         /// <summary>
794         /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
795         /// </summary>
796         /// <param name="orig">対象となる文字列</param>
797         /// <param name="appname">アプリケーション名</param>
798         /// <returns>置換後の文字列</returns>
799         public static string ReplaceAppName(string orig, string appname)
800             => orig.Replace("%AppName%", appname);
801
802         /// <summary>
803         /// 表示用のバージョン番号の文字列を生成する
804         /// </summary>
805         /// <remarks>
806         /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-beta1」が出力される
807         /// </remarks>
808         /// <returns>
809         /// 生成されたバージョン番号の文字列
810         /// </returns>
811         public static string GetReadableVersion(string? versionStr = null)
812         {
813             var version = Version.Parse(versionStr ?? MyCommon.FileVersion);
814
815             return GetReadableVersion(version);
816         }
817
818         /// <summary>
819         /// 表示用のバージョン番号の文字列を生成する
820         /// </summary>
821         /// <remarks>
822         /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-dev」のように出力される
823         /// </remarks>
824         /// <returns>
825         /// 生成されたバージョン番号の文字列
826         /// </returns>
827         public static string GetReadableVersion(Version version)
828         {
829             var versionNum = new[] { version.Major, version.Minor, version.Build, version.Revision };
830
831             if (versionNum[3] == 0)
832             {
833                 return string.Format("{0}.{1}.{2}", versionNum[0], versionNum[1], versionNum[2]);
834             }
835             else
836             {
837                 versionNum[2] = versionNum[2] + 1;
838
839                 if (versionNum[3] == 1)
840                     return string.Format("{0}.{1}.{2}-dev", versionNum[0], versionNum[1], versionNum[2]);
841                 else
842                     return string.Format("{0}.{1}.{2}-dev+build.{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
843             }
844         }
845
846         public const string TwitterUrl = "https://twitter.com/";
847
848         public static string GetStatusUrl(PostClass post)
849         {
850             var statusId = post.RetweetedId ?? post.StatusId;
851             return GetStatusUrl(post.ScreenName, statusId.ToTwitterStatusId());
852         }
853
854         public static string GetStatusUrl(string screenName, TwitterStatusId statusId)
855             => TwitterUrl + screenName + "/status/" + statusId.Id;
856
857         /// <summary>
858         /// 指定された IDictionary を元にクエリ文字列を生成します
859         /// </summary>
860         /// <param name="param">生成するクエリの key-value コレクション</param>
861         public static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> param)
862         {
863             if (param == null)
864                 return string.Empty;
865
866             var query = param
867                 .Where(x => x.Value != null)
868                 .Select(x => EscapeQueryString(x.Key) + '=' + EscapeQueryString(x.Value));
869
870             return string.Join("&", query);
871         }
872
873         // .NET 4.5+: Reserved characters のうち、Uriクラスによってエスケープ強制解除されてしまうものも最初から Unreserved として扱う
874         private static readonly HashSet<char> UnreservedChars =
875             new("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:");
876
877         /// <summary>
878         /// 2バイト文字も考慮したクエリ用エンコード
879         /// </summary>
880         /// <param name="stringToEncode">エンコードする文字列</param>
881         /// <returns>エンコード結果文字列</returns>
882         public static string EscapeQueryString(string stringToEncode)
883         {
884             var sb = new StringBuilder(stringToEncode.Length * 2);
885
886             foreach (var b in Encoding.UTF8.GetBytes(stringToEncode))
887             {
888                 if (UnreservedChars.Contains((char)b))
889                     sb.Append((char)b);
890                 else
891                     sb.AppendFormat("%{0:X2}", b);
892             }
893
894             return sb.ToString();
895         }
896
897         /// <summary>
898         /// 指定された範囲の整数を昇順に列挙します
899         /// </summary>
900         /// <remarks>
901         /// start, start + 1, start + 2, ..., end の範囲の数列を生成します
902         /// </remarks>
903         /// <param name="from">数列の先頭の値 (最小値)</param>
904         /// <param name="to">数列の末尾の値 (最大値)</param>
905         /// <returns>整数を列挙する IEnumerable インスタンス</returns>
906         public static IEnumerable<int> CountUp(int from, int to)
907         {
908             if (from > to)
909                 return Enumerable.Empty<int>();
910
911             return Enumerable.Range(from, to - from + 1);
912         }
913
914         /// <summary>
915         /// 指定された範囲の整数を降順に列挙します
916         /// </summary>
917         /// <remarks>
918         /// start, start - 1, start - 2, ..., end の範囲の数列を生成します
919         /// </remarks>
920         /// <param name="from">数列の先頭の値 (最大値)</param>
921         /// <param name="to">数列の末尾の値 (最小値)</param>
922         /// <returns>整数を列挙する IEnumerable インスタンス</returns>
923         public static IEnumerable<int> CountDown(int from, int to)
924         {
925             for (var i = from; i >= to; i--)
926                 yield return i;
927         }
928
929         public static IEnumerable<int> CircularCountUp(int length, int startIndex)
930         {
931             if (length < 1)
932                 throw new ArgumentOutOfRangeException(nameof(length));
933             if (startIndex < 0 || startIndex >= length)
934                 throw new ArgumentOutOfRangeException(nameof(startIndex));
935
936             // startindex ... 末尾
937             var indices = MyCommon.CountUp(startIndex, length - 1);
938
939             // 先頭 ... (startIndex - 1)
940             if (startIndex != 0)
941                 indices = indices.Concat(MyCommon.CountUp(0, startIndex - 1));
942
943             return indices;
944         }
945
946         public static IEnumerable<int> CircularCountDown(int length, int startIndex)
947         {
948             if (length < 1)
949                 throw new ArgumentOutOfRangeException(nameof(length));
950             if (startIndex < 0 || startIndex >= length)
951                 throw new ArgumentOutOfRangeException(nameof(startIndex));
952
953             // startIndex ... 先頭
954             var indices = MyCommon.CountDown(startIndex, 0);
955
956             // 末尾 ... (startIndex + 1)
957             if (startIndex != length - 1)
958                 indices = indices.Concat(MyCommon.CountDown(length - 1, startIndex + 1));
959
960             return indices;
961         }
962
963         /// <summary>
964         /// 2バイト文字も考慮したUrlエンコード
965         /// </summary>
966         /// <param name="stringToEncode">エンコードする文字列</param>
967         /// <returns>エンコード結果文字列</returns>
968         public static string UrlEncode(string stringToEncode)
969         {
970             const string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
971             var sb = new StringBuilder();
972             var bytes = Encoding.UTF8.GetBytes(stringToEncode);
973
974             foreach (var b in bytes)
975             {
976                 if (UnreservedChars.IndexOf((char)b) != -1)
977                     sb.Append((char)b);
978                 else
979                     sb.AppendFormat("%{0:X2}", b);
980             }
981             return sb.ToString();
982         }
983
984         public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
985             => string.IsNullOrEmpty(value);
986
987         public static Task OpenInBrowserAsync(IWin32Window? owner, string urlStr)
988             => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, urlStr);
989
990         public static Task OpenInBrowserAsync(IWin32Window? owner, Uri uri)
991             => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, uri);
992
993         public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, string urlStr)
994         {
995             if (!Uri.TryCreate(urlStr, UriKind.Absolute, out var uri))
996             {
997                 var message = string.Format(Properties.Resources.CannotOpenUriText, urlStr);
998                 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
999             }
1000             await MyCommon.OpenInBrowserAsync(owner, browserPath, uri);
1001         }
1002
1003         public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, Uri uri)
1004         {
1005             if (uri.Scheme != "http" && uri.Scheme != "https")
1006             {
1007                 var message = string.Format(Properties.Resources.CannotOpenUriText, uri.OriginalString);
1008                 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1009             }
1010
1011             try
1012             {
1013                 if (MyCommon.IsNullOrEmpty(browserPath))
1014                 {
1015                     var options = new Windows.System.LauncherOptions
1016                     {
1017                         IgnoreAppUriHandlers = true,
1018                     };
1019                     await Windows.System.Launcher.LaunchUriAsync(uri, options);
1020                 }
1021                 else
1022                 {
1023                     await Task.Run(() =>
1024                     {
1025                         var startInfo = MyCommon.CreateBrowserProcessStartInfo(browserPath, uri.AbsoluteUri);
1026                         Process.Start(startInfo);
1027                     });
1028                 }
1029             }
1030             catch (Win32Exception ex)
1031             {
1032                 var message = string.Format(Properties.Resources.BrowserStartFailed, ex.Message);
1033                 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1034             }
1035             catch (MissingMethodException ex)
1036             {
1037                 // WinRT API で存在しないメソッドを呼び出した(非対応の OS 上で実行した)場合に発生する
1038                 var message = string.Format(Properties.Resources.BrowserStartFailed, ex.Message);
1039                 MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
1040             }
1041         }
1042
1043         public static ProcessStartInfo CreateBrowserProcessStartInfo(string browserPathWithArgs, string url)
1044         {
1045             var quoteEnd = -1;
1046             if (browserPathWithArgs.StartsWith("\"", StringComparison.Ordinal))
1047                 quoteEnd = browserPathWithArgs.IndexOf("\"", 1, StringComparison.Ordinal);
1048
1049             string browserPath, browserArgs;
1050             var isQuoted = quoteEnd != -1;
1051             if (isQuoted)
1052             {
1053                 browserPath = browserPathWithArgs.Substring(1, quoteEnd - 1);
1054                 browserArgs = browserPathWithArgs.Substring(quoteEnd + 1).Trim();
1055             }
1056             else
1057             {
1058                 browserPath = browserPathWithArgs;
1059                 browserArgs = "";
1060             }
1061
1062             var quotedUrl = "\"" + url.Replace("\"", "\\\"") + "\"";
1063             var args = MyCommon.IsNullOrEmpty(browserArgs) ? quotedUrl : browserArgs + " " + quotedUrl;
1064
1065             return new ProcessStartInfo
1066             {
1067                 FileName = browserPath,
1068                 Arguments = args,
1069                 UseShellExecute = false,
1070             };
1071         }
1072
1073         public static IEnumerable<(int Start, int End)> ToRangeChunk(IEnumerable<int> values)
1074         {
1075             var start = -1;
1076             var end = -1;
1077
1078             foreach (var value in values.OrderBy(x => x))
1079             {
1080                 if (start == -1)
1081                 {
1082                     start = value;
1083                     end = value;
1084                 }
1085                 else
1086                 {
1087                     if (value == end + 1)
1088                     {
1089                         end = value;
1090                     }
1091                     else
1092                     {
1093                         yield return (start, end);
1094                         start = value;
1095                         end = value;
1096                     }
1097                 }
1098             }
1099
1100             if (start != -1)
1101                 yield return (start, end);
1102         }
1103     }
1104 }