1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-01 17:34:10 +02:00

Move a bunch of utility classes into TweetLib.Core & refactor

This commit is contained in:
chylex 2019-05-09 16:26:40 +02:00
parent 01df118549
commit dd6e712755
54 changed files with 375 additions and 355 deletions

View File

@ -1,5 +1,5 @@
using System;
using TweetDuck.Data;
using TweetLib.Core.Collections;
namespace TweetDuck.Configuration{
static class Arguments{

View File

@ -2,9 +2,9 @@
using System.Collections.Generic;
using System.Drawing;
using TweetDuck.Configuration.Instance;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Data.Serialization;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
namespace TweetDuck.Configuration{
sealed class ConfigManager{

View File

@ -1,6 +1,6 @@
using System;
using System.IO;
using TweetDuck.Data.Serialization;
using TweetLib.Core.Serialization;
namespace TweetDuck.Configuration.Instance{
sealed class FileConfigInstance<T> : IConfigInstance<T> where T : ConfigManager.BaseConfig{

View File

@ -12,6 +12,7 @@
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Resources;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{

View File

@ -1,6 +1,6 @@
using System;
using CefSharp;
using TweetDuck.Core.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Management{
sealed class ContextInfo{

View File

@ -3,9 +3,9 @@
using System.IO;
using System.Linq;
using TweetDuck.Core.Other;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetLib.Core.Data;
namespace TweetDuck.Core.Management{
sealed class ProfileManager{

View File

@ -6,10 +6,10 @@
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetDuck.Resources;
using TweetLib.Core.Data;
namespace TweetDuck.Core.Notification{
abstract partial class FormNotificationMain : FormNotificationBase{

View File

@ -6,9 +6,9 @@
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetLib.Core.Data;
namespace TweetDuck.Core.Notification.Screenshot{
sealed class FormNotificationScreenshotable : FormNotificationBase{

View File

@ -2,7 +2,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using TweetDuck.Data.Serialization;
using TweetLib.Core.Serialization;
using TweetLib.Core.Serialization.Converters;
namespace TweetDuck.Core.Other.Analytics{
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local")]

View File

@ -9,6 +9,7 @@
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Analytics{
sealed class AnalyticsManager : IDisposable{
@ -20,7 +21,7 @@ sealed class AnalyticsManager : IDisposable{
#else
"https://tweetduck.chylex.com/breadcrumb/report"
#endif
);
);
public AnalyticsFile File { get; }
@ -117,7 +118,7 @@ private void SendReport(){
System.Diagnostics.Debugger.Break();
#endif
BrowserUtils.CreateWebClient().UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
WebUtils.NewClient(BrowserUtils.UserAgentVanilla).UploadValues(CollectionUrl, "POST", report.ToNameValueCollection());
}).ContinueWith(task => browser.InvokeAsyncSafe(() => {
if (task.Status == TaskStatus.RanToCompletion){
SetLastDataCollectionTime(DateTime.Now);

View File

@ -12,6 +12,7 @@
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Analytics{
static class AnalyticsReportGenerator{

View File

@ -2,7 +2,7 @@
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core.Collections;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsCefArgs : Form{

View File

@ -4,8 +4,8 @@
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Management;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsManage : Form{

View File

@ -1,7 +1,7 @@
using System;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Data;
using TweetLib.Core.Collections;
namespace TweetDuck.Core.Other.Settings.Dialogs{
sealed partial class DialogSettingsRestart : Form{

View File

@ -7,6 +7,7 @@
using TweetDuck.Core.Other.Settings.Dialogs;
using TweetDuck.Core.Utils;
using TweetDuck.Updates;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Other.Settings{
sealed partial class TabSettingsGeneral : BaseTabSettings{

View File

@ -3,16 +3,16 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Windows.Forms;
using CefSharp.WinForms;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using TweetLib.Core.Utils;
namespace TweetDuck.Core.Utils{
static class BrowserUtils{
public static string UserAgentVanilla => Program.BrandName+" "+Application.ProductVersion;
public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"+Cef.ChromiumVersion+" Safari/537.36";
public static string UserAgentVanilla => Program.BrandName + " " + Application.ProductVersion;
public static string UserAgentChrome => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + Cef.ChromiumVersion + " Safari/537.36";
public static readonly bool HasDevTools = File.Exists(Path.Combine(Program.ProgramPath, "devtools_resources.pak"));
@ -74,29 +74,11 @@ void UpdateZoomLevel(object sender, EventArgs args){
};
}
private const string TwitterTrackingUrl = "t.co";
public enum UrlCheckResult{
Invalid, Tracking, Fine
}
public static UrlCheckResult CheckUrl(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? UrlCheckResult.Tracking : UrlCheckResult.Fine;
}
}
return UrlCheckResult.Invalid;
}
public static void OpenExternalBrowser(string url){
if (string.IsNullOrWhiteSpace(url))return;
switch(CheckUrl(url)){
case UrlCheckResult.Fine:
switch(UrlUtils.Check(url)){
case UrlUtils.CheckResult.Fine:
if (FormGuide.CheckGuideUrl(url, out string hash)){
FormGuide.Show(hash);
}
@ -117,9 +99,9 @@ public static void OpenExternalBrowser(string url){
break;
case UrlCheckResult.Tracking:
case UrlUtils.CheckResult.Tracking:
if (Config.IgnoreTrackingUrlWarning){
goto case UrlCheckResult.Fine;
goto case UrlUtils.CheckResult.Fine;
}
using(FormMessage form = new FormMessage("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n"+url, MessageBoxIcon.Warning)){
@ -135,13 +117,13 @@ public static void OpenExternalBrowser(string url){
}
if (result == DialogResult.Ignore || result == DialogResult.Yes){
goto case UrlCheckResult.Fine;
goto case UrlUtils.CheckResult.Fine;
}
}
break;
case UrlCheckResult.Invalid:
case UrlUtils.CheckResult.Invalid:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
break;
}
@ -174,50 +156,10 @@ public static void OpenExternalSearch(string query){
}
}
public static string GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
public static string GetErrorName(CefErrorCode code){
return StringUtils.ConvertPascalCaseToScreamingSnakeCase(Enum.GetName(typeof(CefErrorCode), code) ?? string.Empty);
}
public static WebClient CreateWebClient(){
WindowsUtils.EnsureTLS12();
WebClient client = new WebClient{ Proxy = null };
client.Headers[HttpRequestHeader.UserAgent] = UserAgentVanilla;
return client;
}
public static WebClient DownloadFileAsync(string url, string target, string cookie, Action onSuccess, Action<Exception> onFailure){
WebClient client = CreateWebClient();
if (cookie != null){
client.Headers[HttpRequestHeader.Cookie] = cookie;
}
client.DownloadFileCompleted += (sender, args) => {
if (args.Cancelled){
try{
File.Delete(target);
}catch{
// didn't want it deleted anyways
}
}
else if (args.Error != null){
onFailure?.Invoke(args.Error);
}
else{
onSuccess?.Invoke();
}
};
client.DownloadFileAsync(new Uri(url), target);
return client;
}
public static int Scale(int baseValue, double scaleFactor){
return (int)Math.Round(baseValue*scaleFactor);
}

View File

@ -8,7 +8,9 @@
using TweetDuck.Core.Other;
using TweetDuck.Data;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using TweetLib.Core.Utils;
using Cookie = CefSharp.Cookie;
namespace TweetDuck.Core.Utils{
@ -71,7 +73,7 @@ public static string GetMediaLink(string url, ImageQuality quality){
}
public static string GetImageFileName(string url){
return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
return UrlUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
}
public static void ViewImage(string url, ImageQuality quality){
@ -88,7 +90,7 @@ void ViewImageInternal(string path){
string file = Path.Combine(BrowserCache.CacheFolder, GetImageFileName(url) ?? Path.GetRandomFileName());
if (WindowsUtils.FileExistsAndNotEmpty(file)){
if (FileUtils.FileExistsAndNotEmpty(file)){
ViewImageInternal(file);
}
else{
@ -143,7 +145,7 @@ void OnFailure(Exception ex){
}
public static void DownloadVideo(string url, string username){
string filename = BrowserUtils.GetFileNameFromUrl(url);
string filename = UrlUtils.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename);
using(SaveFileDialog dialog = new SaveFileDialog{
@ -178,7 +180,10 @@ private static void DownloadFileAuth(string url, string target, Action onSuccess
}
}
BrowserUtils.DownloadFileAsync(url, target, cookieStr, onSuccess, onFailure);
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentChrome);
client.Headers[HttpRequestHeader.Cookie] = cookieStr;
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(target, onSuccess, onFailure);
client.DownloadFileAsync(new Uri(url), target);
}, scheduler);
}
}

View File

@ -3,7 +3,6 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
@ -16,7 +15,6 @@ static class WindowsUtils{
private static readonly Lazy<Regex> RegexOffsetClipboardHtml = new Lazy<Regex>(() => new Regex(@"(?<=EndHTML:|EndFragment:)(\d+)"), false);
private static readonly bool IsWindows8OrNewer;
private static bool HasMicrosoftBeenBroughtTo2008Yet;
public static int CurrentProcessID { get; }
public static bool ShouldAvoidToolWindow { get; }
@ -32,47 +30,6 @@ static WindowsUtils(){
ShouldAvoidToolWindow = IsWindows8OrNewer;
}
public static void EnsureTLS12(){
if (!HasMicrosoftBeenBroughtTo2008Yet){
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
HasMicrosoftBeenBroughtTo2008Yet = true;
}
}
public static void CreateDirectoryForFile(string file){
string dir = Path.GetDirectoryName(file);
if (dir == null){
throw new ArgumentException("Invalid file path: "+file);
}
else if (dir.Length > 0){
Directory.CreateDirectory(dir);
}
}
public static bool CheckFolderWritePermission(string path){
string testFile = Path.Combine(path, ".test");
try{
Directory.CreateDirectory(path);
using(File.Create(testFile)){}
File.Delete(testFile);
return true;
}catch{
return false;
}
}
public static bool FileExistsAndNotEmpty(string path){
try{
return new FileInfo(path).Length > 0;
}catch{
return false;
}
}
public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
try{

View File

@ -1,8 +0,0 @@
using System;
namespace TweetDuck.Data.Serialization{
interface ITypeConverter{
bool TryWriteType(Type type, object value, out string converted);
bool TryReadType(Type type, string value, out object converted);
}
}

View File

@ -1,8 +1,8 @@
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using TweetDuck.Data.Serialization;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
namespace TweetDuck.Data{
sealed class WindowState{

View File

@ -2,10 +2,11 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetLib.Core.Collections;
using TweetLib.Core.Data;
using TweetLib.Core.Utils;
namespace TweetDuck.Plugins{
sealed class PluginBridge{
@ -80,7 +81,7 @@ private string ReadFileUnsafe(int token, string cacheKey, string fullPath, bool
public void WriteFile(int token, string path, string contents){
string fullPath = GetFullPathOrThrow(token, PluginFolder.Data, path);
WindowsUtils.CreateDirectoryForFile(fullPath);
FileUtils.CreateDirectoryForFile(fullPath);
File.WriteAllText(fullPath, contents, Encoding.UTF8);
fileCache[token, SanitizeCacheKey(path)] = contents;
}

View File

@ -6,10 +6,10 @@
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
using TweetLib.Core.Data;
namespace TweetDuck.Plugins{
sealed class PluginManager{

View File

@ -14,7 +14,8 @@
using TweetDuck.Core.Other;
using TweetDuck.Core.Management;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetLib.Core.Collections;
using TweetLib.Core.Utils;
namespace TweetDuck{
static class Program{
@ -75,7 +76,7 @@ private static void Main(){
WindowRestoreMessage = NativeMethods.RegisterWindowMessage("TweetDuckRestore");
if (!WindowsUtils.CheckFolderWritePermission(StoragePath)){
if (!FileUtils.CheckFolderWritePermission(StoragePath)){
FormMessage.Warning("Permission Error", "TweetDuck does not have write permissions to the storage folder: "+StoragePath, FormMessage.OK);
return;
}
@ -168,7 +169,7 @@ private static void Main(){
// ProgramPath has a trailing backslash
string updaterArgs = "/SP- /SILENT /FORCECLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath);
bool runElevated = !IsPortable || !FileUtils.CheckFolderWritePermission(ProgramPath);
if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
Application.Exit();

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('packages\Microsoft.Net.Compilers.3.0.0\build\Microsoft.Net.Compilers.props')" />
<Import Project="packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.67.0.0\build\CefSharp.WinForms.props')" />
@ -197,10 +197,7 @@
<DependentUpon>TabSettingsFeedback.cs</DependentUpon>
</Compile>
<Compile Include="Core\TweetDeckBrowser.cs" />
<Compile Include="Core\Utils\LocaleUtils.cs" />
<Compile Include="Core\Utils\StringUtils.cs" />
<Compile Include="Core\Utils\TwitterUtils.cs" />
<Compile Include="Data\CombinedFileStream.cs" />
<Compile Include="Core\Management\ProfileManager.cs" />
<Compile Include="Core\Other\Settings\TabSettingsAdvanced.cs">
<SubType>UserControl</SubType>
@ -230,19 +227,11 @@
<DependentUpon>TabSettingsNotifications.cs</DependentUpon>
</Compile>
<Compile Include="Core\Notification\Screenshot\ScreenshotBridge.cs" />
<Compile Include="Data\CommandLineArgs.cs" />
<Compile Include="Core\Notification\Screenshot\FormNotificationScreenshotable.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
<Compile Include="Data\ResourceLink.cs" />
<Compile Include="Data\Result.cs" />
<Compile Include="Data\Serialization\FileSerializer.cs" />
<Compile Include="Data\InjectedHTML.cs" />
<Compile Include="Data\Serialization\ITypeConverter.cs" />
<Compile Include="Data\Serialization\SerializationSoftException.cs" />
<Compile Include="Data\Serialization\SingleTypeConverter.cs" />
<Compile Include="Data\TwoKeyDictionary.cs" />
<Compile Include="Data\WindowState.cs" />
<Compile Include="Core\Utils\WindowsUtils.cs" />
<Compile Include="Core\Bridge\TweetDeckBridge.cs" />

View File

@ -6,6 +6,7 @@
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using TweetDuck.Core.Utils;
using TweetLib.Core.Utils;
using JsonObject = System.Collections.Generic.IDictionary<string, object>;
namespace TweetDuck.Updates{
@ -22,7 +23,7 @@ public UpdateCheckClient(string installerFolder){
public Task<UpdateInfo> Check(){
TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>();
WebClient client = BrowserUtils.CreateWebClient();
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json";
client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {

View File

@ -1,5 +1,5 @@
using System;
using TweetDuck.Data;
using TweetLib.Core.Data;
namespace TweetDuck.Updates{
sealed class UpdateCheckEventArgs : EventArgs{

View File

@ -1,7 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using TweetDuck.Data;
using TweetLib.Core.Data;
using Timer = System.Windows.Forms.Timer;
namespace TweetDuck.Updates{

View File

@ -2,6 +2,7 @@
using System.IO;
using System.Net;
using TweetDuck.Core.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Updates{
sealed class UpdateInfo{
@ -26,7 +27,7 @@ public UpdateInfo(string versionTag, string releaseNotes, string downloadUrl, st
}
public void BeginSilentDownload(){
if (WindowsUtils.FileExistsAndNotEmpty(InstallerPath)){
if (FileUtils.FileExistsAndNotEmpty(InstallerPath)){
DownloadStatus = UpdateDownloadStatus.Done;
return;
}
@ -48,7 +49,9 @@ public void BeginSilentDownload(){
return;
}
currentDownload = BrowserUtils.DownloadFileAsync(downloadUrl, InstallerPath, null, () => {
WebClient client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
client.DownloadFileCompleted += WebUtils.FileDownloadCallback(InstallerPath, () => {
DownloadStatus = UpdateDownloadStatus.Done;
currentDownload = null;
}, e => {
@ -56,6 +59,8 @@ public void BeginSilentDownload(){
DownloadStatus = UpdateDownloadStatus.Failed;
currentDownload = null;
});
client.DownloadFileAsync(new Uri(downloadUrl), InstallerPath);
}
}

View File

@ -2,8 +2,8 @@
using System.Text;
using System.Text.RegularExpressions;
namespace TweetDuck.Data{
sealed class CommandLineArgs{
namespace TweetLib.Core.Collections{
public sealed class CommandLineArgs{
public static CommandLineArgs FromStringArray(char entryChar, string[] array){
CommandLineArgs args = new CommandLineArgs();
ReadStringArray(entryChar, array, args);
@ -15,7 +15,7 @@ public static void ReadStringArray(char entryChar, string[] array, CommandLineAr
string entry = array[index];
if (entry.Length > 0 && entry[0] == entryChar){
if (index < array.Length-1){
if (index < array.Length - 1){
string potentialValue = array[index+1];
if (potentialValue.Length > 0 && potentialValue[0] == entryChar){
@ -52,7 +52,7 @@ public static CommandLineArgs ReadCefArguments(string argumentString){
}
else{
key = matchValue.Substring(0, indexEquals).TrimStart('-');
value = matchValue.Substring(indexEquals+1).Trim('"');
value = matchValue.Substring(indexEquals + 1).Trim('"');
}
if (key.Length != 0){
@ -66,7 +66,7 @@ public static CommandLineArgs ReadCefArguments(string argumentString){
private readonly HashSet<string> flags = new HashSet<string>();
private readonly Dictionary<string, string> values = new Dictionary<string, string>();
public int Count => flags.Count+values.Count;
public int Count => flags.Count + values.Count;
public void AddFlag(string flag){
flags.Add(flag.ToLower());
@ -103,7 +103,7 @@ public CommandLineArgs Clone(){
copy.AddFlag(flag);
}
foreach(KeyValuePair<string, string> kvp in values){
foreach(var kvp in values){
copy.SetValue(kvp.Key, kvp.Value);
}
@ -115,7 +115,7 @@ public void ToDictionary(IDictionary<string, string> target){
target[flag] = "1";
}
foreach(KeyValuePair<string, string> kvp in values){
foreach(var kvp in values){
target[kvp.Key] = kvp.Value;
}
}
@ -127,11 +127,11 @@ public override string ToString(){
build.Append(flag).Append(' ');
}
foreach(KeyValuePair<string, string> kvp in values){
foreach(var kvp in values){
build.Append(kvp.Key).Append(" \"").Append(kvp.Value).Append("\" ");
}
return build.Length == 0 ? string.Empty : build.Remove(build.Length-1, 1).ToString();
return build.Length == 0 ? string.Empty : build.Remove(build.Length - 1, 1).ToString();
}
}
}

View File

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
namespace TweetDuck.Data{
sealed class TwoKeyDictionary<K1, K2, V>{
namespace TweetLib.Core.Collections{
public sealed class TwoKeyDictionary<K1, K2, V>{
private readonly Dictionary<K1, Dictionary<K2, V>> dict;
private readonly int innerCapacity;
@ -85,7 +85,8 @@ public bool Remove(K1 outerKey, K2 innerKey){
return true;
}
else return false;
return false;
}
public bool TryGetValue(K1 outerKey, K2 innerKey, out V value){
@ -93,7 +94,7 @@ public bool TryGetValue(K1 outerKey, K2 innerKey, out V value){
return innerDict.TryGetValue(innerKey, out value);
}
else{
value = default(V);
value = default;
return false;
}
}

View File

@ -1,11 +1,11 @@
using System;
using System.IO;
using System.Text;
using TweetDuck.Core.Utils;
using TweetLib.Core.Utils;
namespace TweetDuck.Data{
sealed class CombinedFileStream : IDisposable{
public const char KeySeparator = '|';
namespace TweetLib.Core.Data{
public sealed class CombinedFileStream : IDisposable{
private const char KeySeparator = '|';
private readonly Stream stream;
@ -45,7 +45,7 @@ public void WriteFile(string identifier, string path){
stream.Write(contents, 0, contents.Length);
}
public Entry ReadFile(){
public Entry? ReadFile(){
int nameLength = stream.ReadByte();
if (nameLength == -1){
@ -64,7 +64,7 @@ public Entry ReadFile(){
return new Entry(Encoding.UTF8.GetString(name), contents);
}
public string SkipFile(){
public string? SkipFile(){
int nameLength = stream.ReadByte();
if (nameLength == -1){
@ -120,7 +120,7 @@ public void WriteToFile(string path){
public void WriteToFile(string path, bool createDirectory){
if (createDirectory){
WindowsUtils.CreateDirectoryForFile(path);
FileUtils.CreateDirectoryForFile(path);
}
File.WriteAllBytes(path, contents);

View File

@ -1,7 +1,7 @@
using System;
namespace TweetDuck.Data{
sealed class InjectedHTML{
namespace TweetLib.Core.Data{
public sealed class InjectedHTML{
public enum Position{
Before, After
}
@ -27,7 +27,7 @@ public string InjectInto(string targetHTML){
switch(position){
case Position.Before: cutIndex = index; break;
case Position.After: cutIndex = index+search.Length; break;
case Position.After: cutIndex = index + search.Length; break;
default: return targetHTML;
}

View File

@ -1,14 +1,14 @@
using System;
namespace TweetDuck.Data{
sealed class Result<T>{
namespace TweetLib.Core.Data{
public sealed class Result<T>{
public bool HasValue => exception == null;
public T Value => HasValue ? value : throw new InvalidOperationException("Requested value from a failed result.");
public Exception Exception => exception ?? throw new InvalidOperationException("Requested exception from a successful result.");
private readonly T value;
private readonly Exception exception;
private readonly Exception? exception;
public Result(T value){
this.value = value;
@ -16,7 +16,7 @@ public Result(T value){
}
public Result(Exception exception){
this.value = default(T);
this.value = default;
this.exception = exception ?? throw new ArgumentNullException(nameof(exception));
}
@ -25,12 +25,12 @@ public void Handle(Action<T> onSuccess, Action<Exception> onException){
onSuccess(value);
}
else{
onException(exception);
onException(exception!!);
}
}
public Result<R> Select<R>(Func<T, R> map){
return HasValue ? new Result<R>(map(value)) : new Result<R>(exception);
return HasValue ? new Result<R>(map(value)) : new Result<R>(exception!!);
}
}
}

View File

@ -0,0 +1,55 @@
using System;
namespace TweetLib.Core.Serialization.Converters{
internal sealed class ClrTypeConverter : ITypeConverter{
public static ITypeConverter Instance { get; } = new ClrTypeConverter();
private ClrTypeConverter(){}
bool ITypeConverter.TryWriteType(Type type, object value, out string? converted){
switch(Type.GetTypeCode(type)){
case TypeCode.Boolean:
converted = value.ToString();
return true;
case TypeCode.Int32:
converted = ((int)value).ToString(); // cast required for enums
return true;
case TypeCode.String:
converted = value?.ToString();
return true;
default:
converted = null;
return false;
}
}
bool ITypeConverter.TryReadType(Type type, string value, out object? converted){
switch(Type.GetTypeCode(type)){
case TypeCode.Boolean:
if (bool.TryParse(value, out bool b)){
converted = b;
return true;
}
else goto default;
case TypeCode.Int32:
if (int.TryParse(value, out int i)){
converted = i;
return true;
}
else goto default;
case TypeCode.String:
converted = value;
return true;
default:
converted = null;
return false;
}
}
}
}

View File

@ -1,11 +1,11 @@
using System;
namespace TweetDuck.Data.Serialization{
sealed class SingleTypeConverter<T> : ITypeConverter{
namespace TweetLib.Core.Serialization.Converters{
public sealed class SingleTypeConverter<T> : ITypeConverter{
public Func<T, string> ConvertToString { get; set; }
public Func<string, T> ConvertToObject { get; set; }
bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
bool ITypeConverter.TryWriteType(Type type, object value, out string? converted){
try{
converted = ConvertToString((T)value);
return true;
@ -15,7 +15,7 @@ bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
}
}
bool ITypeConverter.TryReadType(Type type, string value, out object converted){
bool ITypeConverter.TryReadType(Type type, string value, out object? converted){
try{
converted = ConvertToObject(value);
return true;

View File

@ -4,10 +4,11 @@
using System.Linq;
using System.Reflection;
using System.Text;
using TweetDuck.Core.Utils;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
namespace TweetDuck.Data.Serialization{
sealed class FileSerializer<T>{
namespace TweetLib.Core.Serialization{
public sealed class FileSerializer<T>{
private const string NewLineReal = "\r\n";
private const string NewLineCustom = "\r~\n";
@ -49,8 +50,6 @@ private static string UnescapeStream(StreamReader reader){
return build.Append(data.Substring(index)).ToString();
}
private static readonly ITypeConverter BasicSerializerObj = new BasicTypeConverter();
private readonly Dictionary<string, PropertyInfo> props;
private readonly Dictionary<Type, ITypeConverter> converters;
@ -66,7 +65,7 @@ public void RegisterTypeConverter(Type type, ITypeConverter converter){
public void Write(string file, T obj){
LinkedList<string> errors = new LinkedList<string>();
WindowsUtils.CreateDirectoryForFile(file);
FileUtils.CreateDirectoryForFile(file);
using(StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
foreach(KeyValuePair<string, PropertyInfo> prop in props){
@ -74,10 +73,10 @@ public void Write(string file, T obj){
object value = prop.Value.GetValue(obj);
if (!converters.TryGetValue(type, out ITypeConverter serializer)){
serializer = BasicSerializerObj;
serializer = ClrTypeConverter.Instance;
}
if (serializer.TryWriteType(type, value, out string converted)){
if (serializer.TryWriteType(type, value, out string? converted)){
if (converted != null){
writer.Write(prop.Key);
writer.Write(' ');
@ -142,10 +141,10 @@ public void Read(string file, T obj){
if (props.TryGetValue(property, out PropertyInfo info)){
if (!converters.TryGetValue(info.PropertyType, out ITypeConverter serializer)){
serializer = BasicSerializerObj;
serializer = ClrTypeConverter.Instance;
}
if (serializer.TryReadType(info.PropertyType, value, out object converted)){
if (serializer.TryReadType(info.PropertyType, value, out object? converted)){
info.SetValue(obj, converted);
}
else{
@ -165,53 +164,5 @@ public void ReadIfExists(string file, T obj){
}catch(FileNotFoundException){
}catch(DirectoryNotFoundException){}
}
private sealed class BasicTypeConverter : ITypeConverter{
bool ITypeConverter.TryWriteType(Type type, object value, out string converted){
switch(Type.GetTypeCode(type)){
case TypeCode.Boolean:
converted = value.ToString();
return true;
case TypeCode.Int32:
converted = ((int)value).ToString(); // cast required for enums
return true;
case TypeCode.String:
converted = value?.ToString();
return true;
default:
converted = null;
return false;
}
}
bool ITypeConverter.TryReadType(Type type, string value, out object converted){
switch(Type.GetTypeCode(type)){
case TypeCode.Boolean:
if (bool.TryParse(value, out bool b)){
converted = b;
return true;
}
else goto default;
case TypeCode.Int32:
if (int.TryParse(value, out int i)){
converted = i;
return true;
}
else goto default;
case TypeCode.String:
converted = value;
return true;
default:
converted = null;
return false;
}
}
}
}
}

View File

@ -0,0 +1,8 @@
using System;
namespace TweetLib.Core.Serialization{
public interface ITypeConverter{
bool TryWriteType(Type type, object value, out string? converted);
bool TryReadType(Type type, string value, out object? converted);
}
}

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
namespace TweetDuck.Data.Serialization{
sealed class SerializationSoftException : Exception{
namespace TweetLib.Core.Serialization{
public sealed class SerializationSoftException : Exception{
public IList<string> Errors { get; }
public SerializationSoftException(IList<string> errors) : base(string.Join(Environment.NewLine, errors)){

View File

@ -0,0 +1,39 @@
using System;
using System.IO;
namespace TweetLib.Core.Utils{
public static class FileUtils{
public static void CreateDirectoryForFile(string file){
string dir = Path.GetDirectoryName(file);
if (dir == null){
throw new ArgumentException("Invalid file path: "+file);
}
else if (dir.Length > 0){
Directory.CreateDirectory(dir);
}
}
public static bool CheckFolderWritePermission(string path){
string testFile = Path.Combine(path, ".test");
try{
Directory.CreateDirectory(path);
using(File.Create(testFile)){}
File.Delete(testFile);
return true;
}catch{
return false;
}
}
public static bool FileExistsAndNotEmpty(string path){
try{
return new FileInfo(path).Length > 0;
}catch{
return false;
}
}
}
}

View File

@ -3,8 +3,8 @@
using System.Globalization;
using System.Linq;
namespace TweetDuck.Core.Utils{
static class LocaleUtils{
namespace TweetLib.Core.Utils{
public static class LocaleUtils{
// https://cs.chromium.org/chromium/src/third_party/hunspell_dictionaries/
public static IEnumerable<Item> SpellCheckLanguages { get; } = new List<string>{
"af-ZA", "bg-BG", "ca-ES", "cs-CZ", "da-DK", "de-DE",
@ -33,9 +33,9 @@ public sealed class Item : IComparable<Item>{
private string Name => info?.NativeName ?? Code;
private readonly CultureInfo info;
private readonly CultureInfo? info;
public Item(string code, string alt = null){
public Item(string code, string? alt = null){
this.Code = code;
try{

View File

@ -2,8 +2,8 @@
using System.Linq;
using System.Text.RegularExpressions;
namespace TweetDuck.Core.Utils{
static class StringUtils{
namespace TweetLib.Core.Utils{
public static class StringUtils{
public static readonly string[] EmptyArray = new string[0];
public static string ExtractBefore(string str, char search, int startIndex = 0){
@ -23,7 +23,7 @@ public static string ConvertRot13(string str){
return Regex.Replace(str, @"[a-zA-Z]", match => {
int code = match.Value[0];
int start = code <= 90 ? 65 : 97;
return ((char)(start+(code-start+13)%26)).ToString();
return ((char)(start + (code - start + 13) % 26)).ToString();
});
}

View File

@ -0,0 +1,29 @@
using System;
using System.IO;
namespace TweetLib.Core.Utils{
public static class UrlUtils{
private const string TwitterTrackingUrl = "t.co";
public enum CheckResult{
Invalid, Tracking, Fine
}
public static CheckResult Check(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? CheckResult.Tracking : CheckResult.Fine;
}
}
return CheckResult.Invalid;
}
public static string? GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file;
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
namespace TweetLib.Core.Utils{
public static class WebUtils{
private static bool HasMicrosoftBeenBroughtTo2008Yet;
private static void EnsureTLS12(){
if (!HasMicrosoftBeenBroughtTo2008Yet){
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
ServicePointManager.SecurityProtocol &= ~(SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11);
HasMicrosoftBeenBroughtTo2008Yet = true;
}
}
public static WebClient NewClient(string userAgent){
EnsureTLS12();
WebClient client = new WebClient{ Proxy = null };
client.Headers[HttpRequestHeader.UserAgent] = userAgent;
return client;
}
public static AsyncCompletedEventHandler FileDownloadCallback(string file, Action? onSuccess, Action<Exception>? onFailure){
return (sender, args) => {
if (args.Cancelled){
try{
File.Delete(file);
}catch{
// didn't want it deleted anyways
}
}
else if (args.Error != null){
onFailure?.Invoke(args.Error);
}
else{
onSuccess?.Invoke();
}
};
}
}
}

View File

@ -1,9 +1,4 @@
using System;
using System.Diagnostics.Contracts;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Configuration;
using TweetDuck.Core.Other;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TweetTest.Configuration{
[TestClass]

View File

@ -2,7 +2,7 @@
using System.IO;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Data;
using TweetLib.Core.Data;
namespace TweetTest.Data{
[TestClass]

View File

@ -1,7 +1,7 @@
using System;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Data.Serialization;
using TweetLib.Core.Serialization;
namespace TweetTest.Data{
[TestClass]

View File

@ -1,79 +0,0 @@
namespace TweetTest.Core.BrowserUtils
open Xunit
open TweetDuck.Core.Utils
module CheckUrl =
type Result = BrowserUtils.UrlCheckResult
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, BrowserUtils.CheckUrl("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", BrowserUtils.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", BrowserUtils.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com/path/"))

View File

@ -1,7 +1,7 @@
namespace TweetTest.Core.StringUtils
open Xunit
open TweetDuck.Core.Utils
open TweetLib.Core.Utils
module ExtractBefore =

View File

@ -0,0 +1,79 @@
namespace TweetTest.Core.UrlUtils
open Xunit
open TweetLib.Core.Utils
module Check =
type Result = UrlUtils.CheckResult
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, UrlUtils.Check("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, UrlUtils.Check("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, UrlUtils.Check("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", UrlUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", UrlUtils.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", UrlUtils.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(UrlUtils.GetFileNameFromUrl("http://example.com/path/"))

View File

@ -1,7 +1,7 @@
namespace TweetTest.Data.CommandLineArgs
open Xunit
open TweetDuck.Data
open TweetLib.Core.Collections
type _TestData =

View File

@ -1,7 +1,7 @@
namespace TweetTest.Data.InjectedHTML
open Xunit
open TweetDuck.Data
open TweetLib.Core.Data
module Inject =

View File

@ -1,7 +1,7 @@
namespace TweetTest.Data.Result
open Xunit
open TweetDuck.Data
open TweetLib.Core.Data
open System

View File

@ -1,7 +1,7 @@
namespace TweetTest.Data.TwoKeyDictionary
open Xunit
open TweetDuck.Data
open TweetLib.Core.Collections
open System.Collections.Generic

View File

@ -50,9 +50,9 @@
</Choose>
<Import Project="$(FSharpTargetsPath)" />
<ItemGroup>
<Compile Include="Core\TestBrowserUtils.fs" />
<Compile Include="Core\TestStringUtils.fs" />
<Compile Include="Core\TestTwitterUtils.fs" />
<Compile Include="Core\TestUrlUtils.fs" />
<Compile Include="Data\TestCommandLineArgs.fs" />
<Compile Include="Data\TestInjectedHTML.fs" />
<Compile Include="Data\TestResult.fs" />