OSDN Git Service

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