diff --git a/DML.AppCore/DML.AppCore.csproj b/DML.AppCore/DML.AppCore.csproj new file mode 100644 index 0000000..6d9fa23 --- /dev/null +++ b/DML.AppCore/DML.AppCore.csproj @@ -0,0 +1,17 @@ + + + + netstandard1.4 + + + + + + + + + + + + + diff --git a/DML.Client/DML.Client.csproj b/DML.Client/DML.Client.csproj new file mode 100644 index 0000000..4b6dd43 --- /dev/null +++ b/DML.Client/DML.Client.csproj @@ -0,0 +1,15 @@ + + + + netstandard1.4 + + + + + + + + + + + diff --git a/DML.Client/DMLClient.cs b/DML.Client/DMLClient.cs new file mode 100644 index 0000000..5f049c0 --- /dev/null +++ b/DML.Client/DMLClient.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; + +namespace DML.Client +{ + public static class DMLClient + { + public static DiscordSocketClient Client { get; set; } = new DiscordSocketClient(new DiscordSocketConfig(){DefaultRetryMode = RetryMode.RetryRatelimit|RetryMode.RetryTimeouts}); + + public static async Task Login(string token) + { + await Client.LoginAsync(TokenType.User, token); + await Client.StartAsync(); + await Task.Delay(1000); + + while (Client.LoginState == LoginState.LoggingIn || Client.ConnectionState == ConnectionState.Connecting) + { + // wait + } + + return Client.LoginState == LoginState.LoggedIn && Client.ConnectionState == ConnectionState.Connected; + } + } +} diff --git a/DML.Core/Classes/Job.cs b/DML.Core/Classes/Job.cs new file mode 100644 index 0000000..2f283e2 --- /dev/null +++ b/DML.Core/Classes/Job.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; + +namespace DML.Core.Classes +{ + public class Job + { + public int Id { get; set; } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public double KnownTimestamp { get; set; } = 0; + private double StopTimestamp { get; set; } = 0; + private bool IsValid { get; set; } = true; + + internal void Store() + { + Debug("Storing job to database..."); + Trace("Getting jobs collection..."); + var jobDb = DML.Core.Core.Database.GetCollection("jobs"); + + Trace("Adding new value..."); + + if (jobDb.Find(x => x.ChannelId == ChannelId && x.GuildId == GuildId).Any()) + { + jobDb.Update(this); + } + else + { + jobDb.Insert(this); + } + } + + internal void Delete() + { + Debug("Deleting job from database..."); + Trace("Getting jobs collection..."); + var jobDb = DML.Core.Core.Database.GetCollection("jobs"); + + Trace("Deleting value..."); + jobDb.Delete(Id); + } + + private SocketGuild FindServerById(ulong id) + { + Trace($"Trying to find server by Id: {id}"); + return (from s in DML.Core.Core.Client.Guilds where s.Id == id select s).FirstOrDefault(); + } + + private SocketTextChannel FindChannelById(SocketGuild server, ulong id) + { + Trace($"Trying to find channel in {server} by id: {id}"); + return (from c in server.TextChannels where c.Id == id select c).FirstOrDefault(); + } + + internal async Task> Scan() + { + Debug($"Starting scan of guild {GuildId} channel {ChannelId}..."); + var result = new List(); + + var limit = 100; + var lastId = ulong.MaxValue; + var isFirst = true; + var finished = false; + + var guild = FindServerById(GuildId); + var channel = FindChannelById(guild, ChannelId); + + if (Math.Abs(StopTimestamp) < 0.4) + StopTimestamp = KnownTimestamp; + Trace("Initialized scanning parameters."); + + while (!finished) + { + Trace("Entering scanning loop..."); + SocketMessage[] messages; + + Trace($"Downloading next {limit} messages..."); + if (isFirst) + { + messages = await channel.GetMessagesAsync(limit).ToArray() as SocketMessage[]; + } + else + { + messages = await channel.GetMessagesAsync(lastId, Direction.Before, limit).ToArray() as SocketMessage[]; + } + Trace($"Downloaded {messages.Length} messages."); + + isFirst = false; + + foreach (var m in messages) + { + if (!IsValid) + return null; + + Debug($"Processing message {m.Id}"); + if (m.Id < lastId) + { + Trace($"Updating lastId ({lastId}) to {m.Id}"); + lastId = m.Id; + } + + if (SweetUtils.DateTimeToUnixTimeStamp(m.CreatedAt.UtcDateTime) <= StopTimestamp) + { + Debug("Found a message with a known timestamp...Stopping scan."); + finished = true; + continue; + } + + Trace($"Message {m.Id} has {m.Attachments.Count} attachments."); + if (m.Attachments.Count > 0) + { + result.Add(m); + DML.Core.Core.Scheduler.TotalAttachments++; + Trace($"Added message {m.Id}"); + } + Debug($"Finished message {m.Id}"); + + DML.Core.Core.Scheduler.MessagesScanned++; + } + + finished = finished || messages.Length < limit; + } + Trace($"Downloaded all messages for guild {GuildId} channel {ChannelId}."); + + Trace("Sorting messages..."); + result.Sort((a, b) => DateTime.Compare(a.CreatedAt.UtcDateTime, b.CreatedAt.UtcDateTime)); + + if (result.Count > 0) + { + Trace("Updating StopTimestamp for next scan..."); + StopTimestamp = SweetUtils.DateTimeToUnixTimeStamp(result[result.Count - 1].CreatedAt.UtcDateTime); + } + + Debug($"Fisnished scan of guild {GuildId} channel {ChannelId}."); + + return result; + } + + internal void Stop() + { + IsValid = false; + } + + internal static IEnumerable RestoreJobs() + { + Debug("Restoring jobs..."); + Trace("Getting jobs collection..."); + var jobDb = DML.Core.Core.Database.GetCollection("jobs"); + + Trace("Creating new empty job list"); + return jobDb.FindAll(); + } + } +} diff --git a/DML.Core/Classes/JobScheduler.cs b/DML.Core/Classes/JobScheduler.cs new file mode 100644 index 0000000..21d3305 --- /dev/null +++ b/DML.Core/Classes/JobScheduler.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Discord.WebSocket; + +namespace DML.Core.Classes +{ + internal class JobScheduler + { + private ulong messageScanned = 0; + private ulong totalAttachments = 0; + private ulong attachmentsDownloaded = 0; + + private bool Run { get; set; } = false; + internal List JobList { get; set; } = new List(); + internal Dictionary> RunningJobs = new Dictionary>(); + internal int RunningThreads { get; set; } = 0; + + internal ulong MessagesScanned + { + get + { + lock (this) + { + return messageScanned; + } + } + set + { + lock (this) + { + messageScanned = value; + } + } + } + + internal ulong TotalAttachments + { + get + { + lock (this) + { + return totalAttachments; + } + } + set + { + lock (this) + { + totalAttachments = value; + } + } + } + + internal ulong AttachmentsDownloaded + { + get + { + lock (this) + { + return attachmentsDownloaded; + } + } + set + { + lock (this) + { + attachmentsDownloaded = value; + } + } + } + + internal ulong AttachmentsToDownload => TotalAttachments - AttachmentsDownloaded; + + internal void Stop() + { + Run = false; + } + + internal void Start() + { + Run = true; + + Task.Run(async () => + { + Info("Started JobScheduler..."); + while (Run) + { + Debug("Entering job list handler loop..."); + //foreach (var job in JobList) + for (var i = JobList.Count - 1; i >= 0; i--) + { + var job = JobList[i]; + Debug($"Checking job {job}"); + var hasJob = false; + + Trace("Locking scheduler..."); + lock (this) + { + Trace("Checking if job is already performed..."); + hasJob = RunningJobs.ContainsKey(job.Id); + } + Trace("Unlocked scheduler."); + + if (!hasJob) + { + Debug("Job is not performed yet...Performing job..."); + var queue = new Queue(); + + Trace("Locking scheduler..."); + lock (this) + { + Trace("Adding job to running jobs."); + RunningJobs.Add(job.Id, queue); + } + Trace("Unlocked scheduler."); + + Trace("Issuing job message scan..."); + var messages = await job.Scan(); + + if (messages == null) + continue; + + Trace($"Adding {messages.Count} messages to queue..."); + foreach (var msg in messages) + { + queue.Enqueue(msg); + } + Trace($"Added {queue.Count} messages to queue."); + + if (messages.Count != queue.Count) + Warn("Not all messages have been added into the queue."); + + var startedDownload = false; + + while (!startedDownload) + { + Debug("Entering loop to check thread availability"); + Trace("Locking scheduler..."); + lock (this) + { + Trace($"Checking thread limit. Running: {RunningThreads}, Max: {DML.Core.Core.Settings.ThreadLimit}"); + if (RunningThreads >= DML.Core.Core.Settings.ThreadLimit) + continue; + + RunningThreads++; + startedDownload = true; + } + Trace("Unlocked scheduler."); + } + + Trace("Start downloading job async."); + Task.Run(() => WorkQueue(job.Id)); // do not await to work parallel + } + } + } + }); + } + + private void WorkQueue(int jobId) + { + try + { + Debug("Beginning job download..."); + Trace("Finding job..."); + var job = (from j in JobList where j.Id == jobId select j).FirstOrDefault(); + + if (job == null) + { + Warn($"Associating job not found! JobId: {jobId}"); + return; + } + Trace("Found job."); + + Queue queue; + Trace("Locking scheduler..."); + lock (this) + { + Trace("Finiding queue..."); + if (!RunningJobs.TryGetValue(jobId, out queue)) + { + Warn($"Queue for job {jobId} not found!"); + return; + } + Trace("Queue found."); + } + Trace("Unlocked scheduler."); + + Debug($"Messages to process for job {jobId}: {queue.Count}"); + while (queue.Count > 0) + { + Trace("Locking scheduler..."); + lock (this) + { + Trace("Checking if still a job..."); + if (!RunningJobs.ContainsKey(jobId)) + { + Warn($"Queue for job {jobId} not found!"); + return; + } + Trace("Continue working..."); + } + Trace("Unlocked scheduler."); + + Trace("Dequeueing message..."); + var message = queue.Dequeue(); + + Debug($"Attachments for message {message.Id}: {message.Attachments.Count}"); + foreach (var a in message.Attachments) + { + try + { + var fileName = Path.Combine(DML.Core.Core.Settings.OperatingFolder, DML.Core.Core.Settings.FileNameScheme); + + Trace("Replacing filename placeholders..."); + + var extensionRequired = !fileName.EndsWith("%name%"); + + var serverName = "unknown"; + + var socketTextChannel = message.Channel as SocketTextChannel; + if (socketTextChannel != null) + { + serverName = socketTextChannel.Guild.Name.Replace(":", "").Replace("/", "") + .Replace("\\", ""); + } + + fileName = + fileName.Replace("%guild%", serverName) + .Replace("%channel%", message.Channel.Name) + .Replace("%timestamp%", SweetUtils.DateTimeToUnixTimeStamp(message.CreatedAt.UtcDateTime).ToString()) + .Replace("%name%", a.Filename) + .Replace("%id%", a.Id.ToString()); + + if (extensionRequired) + fileName += Path.GetExtension(a.Filename); + + Trace($"Detemined file name: {fileName}."); + + + if (File.Exists(fileName) && new FileInfo(fileName).Length == a.Size) + { + Debug($"{fileName} already existing with its estimated size. Skipping..."); + continue; + } + + Trace("Determining directory..."); + var fileDirectory = Path.GetDirectoryName(fileName); + + if (!Directory.Exists(fileDirectory)) + { + Info($"Directory {fileDirectory} does not exist. Creating directory..."); + Directory.CreateDirectory(fileDirectory); + Debug("Created directory."); + } + + var wc = new WebClient(); + Debug($"Starting downloading of attachment {a.Id}..."); + + wc.DownloadFile(new Uri(a.Url), fileName); + Debug($"Downloaded attachment {a.Id}."); + + Trace("Updating known timestamp for job..."); + job.KnownTimestamp = SweetUtils.DateTimeToUnixTimeStamp(message.CreatedAt.UtcDateTime); + job.Store(); + } + finally + { + AttachmentsDownloaded++; + } + } + } + } + finally + { + Trace("Locking scheduler..."); + lock (this) + { + Trace($"Removing {jobId} from running jobs..."); + RunningJobs.Remove(jobId); + Trace("Decreasing thread count..."); + RunningThreads--; + } + Trace("Unlocked scheduler."); + } + } + } +} diff --git a/DML.Core/Classes/Settings.cs b/DML.Core/Classes/Settings.cs new file mode 100644 index 0000000..0466c97 --- /dev/null +++ b/DML.Core/Classes/Settings.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; + +namespace DML.Core.Classes +{ + internal class Settings + { + public int Id { get; } = 1; // using always unique ID + public string Email { get; set; } + public string Password { get; set; } + public string LoginToken { get; set; } + public bool UseUserData { get; set; } = false; + public bool SavePassword { get; set; } = false; + public LogLevel ApplicactionLogLevel { get; set; } = LogLevel.Info | LogLevel.Warn | LogLevel.Error; + public string OperatingFolder { get; set; } + public string FileNameScheme { get; set; } = @"%guild%\%channel%\%id%"; + public bool SkipExistingFiles { get; set; } = true; + public int ThreadLimit { get; set; } = 50; + + internal void Store() + { + Trace("Getting settings collection..."); + var settingsDB = DML.Core.Core.Database.GetCollection("settings"); + + Debug("Storing settings to database..."); + + if (settingsDB.Exists(_setting => _setting.Id == Id)) + { + Trace("Updating existing value..."); + settingsDB.Update(this); + } + else + { + Trace("Adding new value..."); + settingsDB.Insert(this); + } + } + } +} diff --git a/Discord Media Loader.Application/Core.cs b/DML.Core/Core.cs similarity index 83% rename from Discord Media Loader.Application/Core.cs rename to DML.Core/Core.cs index a30ea9d..ffd7e14 100644 --- a/Discord Media Loader.Application/Core.cs +++ b/DML.Core/Core.cs @@ -2,25 +2,17 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Runtime; -using System.Runtime.Remoting.Channels; using System.Threading.Tasks; -using System.Windows.Forms; using Discord; using Discord.Net; -using DML.Application.Classes; -using DML.Application.Dialogs; -using LiteDB; -using SweetLib.Utils; -using SweetLib.Utils.Logger; -using SweetLib.Utils.Logger.Memory; -using static SweetLib.Utils.Logger.Logger; +using Discord.WebSocket; +using DML.Core.Classes; -namespace DML.Application +namespace DML.Core { public static class Core { - internal static DiscordClient Client { get; set; } + internal static DiscordSocketClient Client { get; set; } internal static LiteDatabase Database { get; set; } internal static Settings Settings { get; set; } internal static JobScheduler Scheduler { get; } = new JobScheduler(); @@ -127,11 +119,12 @@ namespace DML.Application } Debug("Creating discord client..."); - Client = new DiscordClient(); - Client.Log.Message += (sender, message) => + + Client = new DiscordSocketClient(); + Client.Log += (arg) => { - var logMessage = $"DiscordClient: {message.Message}"; - switch (message.Severity) + var logMessage = $"DiscordClient: {arg.Message}"; + switch (arg.Severity) { case LogSeverity.Verbose: Trace(logMessage); @@ -149,40 +142,42 @@ namespace DML.Application Error(logMessage); break; } + + return Task.CompletedTask; }; Info("Trying to log into discord..."); var abort = false; - while (Client.State != ConnectionState.Connected && !abort) - { - Trace("Entering login loop."); + Client.Connected += Client_Connected; + while (Client.LoginState != LoginState.LoggedIn && !abort) + { + Debug(Client.ConnectionState.ToString()); + Debug(Client.LoginState.ToString()); + + Trace("Entering login loop."); + try { + if (Client.ConnectionState == ConnectionState.Connecting) + continue; + if (!string.IsNullOrEmpty(Settings.LoginToken)) { Debug("Trying to login with last known token..."); - await Client.Connect(Settings.LoginToken, TokenType.User); + await Client.LoginAsync(TokenType.User, Settings.LoginToken); + await Task.Delay(1000); } - if (Client.State != ConnectionState.Connected && Settings.UseUserData && - !string.IsNullOrEmpty(Settings.Email) && - !string.IsNullOrEmpty(Settings.Password)) - { - Settings.LoginToken = string.Empty; - - Debug("Trying to login with email and password..."); - await Client.Connect(Settings.Email, Settings.Password); - } } - catch (HttpException) + catch (HttpException ex) { - Warn("Login seems to have failed or gone wrong."); + Warn($"Login seems to have failed or gone wrong: {ex.GetType().Name} - {ex.Message}"); } - if (Client.State != ConnectionState.Connected) + if (Client.LoginState == LoginState.LoggedOut) { Settings.Password = string.Empty; Debug("Showing dialog for username and password..."); @@ -193,6 +188,14 @@ namespace DML.Application } Debug("Start checking for invalid jobs..."); + + //Client + + while (Client.Guilds.Count==0) + { + // wait until guilds are loaded + } + for (var i = Scheduler.JobList.Count - 1; i >= 0; i--) { var job = Scheduler.JobList[i]; @@ -235,14 +238,20 @@ namespace DML.Application } } - //TODO: this is thrid time we implement this.....this has to be fixed!!! - private static Server FindServerById(ulong id) + private static Task Client_Connected() { - Trace($"Trying to find server by Id: {id}"); - return (from s in Core.Client.Servers where s.Id == id select s).FirstOrDefault(); + Debug("Connected"); + return Task.CompletedTask; } - private static Channel FindChannelById(Server server, ulong id) + //TODO: this is thrid time we implement this.....this has to be fixed!!! + private static SocketGuild FindServerById(ulong id) + { + Trace($"Trying to find server by Id: {id}"); + return (from s in Core.Client.Guilds where s.Id == id select s).FirstOrDefault(); + } + + private static SocketTextChannel FindChannelById(SocketGuild server, ulong id) { Trace($"Trying to find channel in {server} by id: {id}"); return (from c in server.TextChannels where c.Id == id select c).FirstOrDefault(); diff --git a/DML.Core/DML.Core.Old.csproj b/DML.Core/DML.Core.Old.csproj new file mode 100644 index 0000000..438b9a8 --- /dev/null +++ b/DML.Core/DML.Core.Old.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp1.1 + + + + + + + + + + + diff --git a/Discord Media Loader.Application/Classes/Core.cs b/Discord Media Loader.Application/Classes/Core.cs new file mode 100644 index 0000000..01ad7e7 --- /dev/null +++ b/Discord Media Loader.Application/Classes/Core.cs @@ -0,0 +1,293 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime; +using System.Threading.Tasks; +using System.Windows.Forms; +using Discord; +using Discord.Net; +using Discord.WebSocket; +using DML.AppCore.Classes; +using DML.Application.Dialogs; +using DML.Client; +using LiteDB; +using SweetLib.Utils; +using SweetLib.Utils.Logger; +using SweetLib.Utils.Logger.Memory; +using Logger = SweetLib.Utils.Logger.Logger; + +namespace DML.Application.Classes +{ + public static class Core + { + //internal static DiscordSocketClient Client { get; set; } + internal static LiteDatabase Database { get; set; } + internal static Settings Settings { get; set; } + internal static JobScheduler Scheduler { get; } = new JobScheduler(); + + internal static string DataDirectory + => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Serraniel\Discord Media Loader"); + + public static async Task Run(string[] paramStrings) + { + try + { + var splash = new FrmInternalSplash(); + splash.Show(); + System.Windows.Forms.Application.DoEvents(); + + Logger.Info("Starting up Discord Media Loader application..."); + var useTrace = false; +#if DEBUG + //temporary add debug log level if debugging... + Logger.GlobalLogLevel |= LogLevel.Debug; + Logger.Debug("Running in debug configuartion. Added log level debug."); +#endif + + Logger.Debug($"Parameters: {string.Join(", ", paramStrings)}"); + if (paramStrings.Contains("--trace") || paramStrings.Contains("-t")) + { + useTrace = true; + Logger.GlobalLogLevel |= LogLevel.Trace; + Logger.Trace("Trace parameter found. Added log level trace."); + } + + Logger.Debug($"Application data folder: {DataDirectory}"); + + Logger.Trace("Checking application data folder..."); + if (!Directory.Exists(DataDirectory)) + { + Logger.Debug("Creating application data folder..."); + Directory.CreateDirectory(DataDirectory); + Logger.Trace("Creating application data folder."); + } + + Logger.Trace("Initializing profile optimizations..."); + ProfileOptimization.SetProfileRoot(System.Windows.Forms.Application.UserAppDataPath); + ProfileOptimization.StartProfile("profile.opt"); + Logger.Trace("Finished initializing profile optimizations."); + + Logger.Trace("Trying to identify log memory..."); + var logMemory = Logger.DefaultLogMemory as ArchivableConsoleLogMemory; + if (logMemory != null) + { + var logFolder = Path.Combine(DataDirectory, "logs"); + if (!Directory.Exists(logFolder)) + { + Logger.Debug("Creating log folder..."); + Directory.CreateDirectory(logFolder); + Logger.Trace("Created log folder."); + } + + + var logFile = Path.Combine(logFolder, + SweetUtils.LegalizeFilename($"{DateTime.Now.ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern)}.log.zip")); + + Logger.Trace($"Setting log file: {logFile}"); + logMemory.AutoArchiveOnDispose = true; + logMemory.ArchiveFile = logFile; + } + + Logger.Debug("Loading database..."); + Database = new LiteDatabase(Path.Combine(DataDirectory, "config.db")); + Database.Log.Logging += (message) => Logger.Trace($"LiteDB: {message}"); + + Logger.Debug("Loading settings collection out of database..."); + var settingsDB = Database.GetCollection("settings"); + if (settingsDB.Count() > 1) + { + Logger.Warn("Found more than one setting. Loading first one..."); + } + Settings = settingsDB.FindAll().FirstOrDefault(); + if (Settings == null) + { + Logger.Warn("Settings not found. Creating new one. This is normal on first start up..."); + Settings = new Settings(); + Settings.Store(); + } + + Logger.Debug("Loading jobs collection out of database..."); + Scheduler.JobList = Job.RestoreJobs().ToList(); + + Logger.Info("Loaded settings."); + Logger.Debug( + $"Settings: Email: {Settings.Email}, password: {(string.IsNullOrEmpty(Settings.Password) ? "not set" : "is set")}, use username: {Settings.UseUserData}, loginToken: {Settings.LoginToken}"); + + Logger.Trace("Updating log level..."); + Logger.GlobalLogLevel = Settings.ApplicactionLogLevel; +#if DEBUG + //temporary add debug log level if debugging... + Logger.GlobalLogLevel |= LogLevel.Debug; + Logger.Debug("Running in debug configuartion. Added log level debug."); +#endif + if (useTrace) + { + Logger.GlobalLogLevel |= LogLevel.Trace; + Logger.Trace("Creating application data folder."); + } + + Logger.Debug("Creating discord client..."); + + var config = new DiscordSocketConfig() + { + DefaultRetryMode = RetryMode.AlwaysRetry, + }; + + //Client = new DiscordSocketClient(config); + DMLClient.Client.Log += (arg) => + { + var logMessage = $"DiscordClient: {arg.Message}"; + switch (arg.Severity) + { + case LogSeverity.Verbose: + Logger.Trace(logMessage); + break; + case LogSeverity.Debug: + Logger.Trace(logMessage); + break; + case LogSeverity.Info: + Logger.Info(logMessage); + break; + case LogSeverity.Warning: + Logger.Warn(logMessage); + break; + case LogSeverity.Error: + Logger.Error(logMessage); + break; + } + + return Task.CompletedTask; + }; + + + Logger.Info("Trying to log into discord..."); + var abort = false; + + DMLClient.Client.Connected += Client_Connected; + + var loggedIn = false; + + while (!loggedIn) + { + if (!string.IsNullOrEmpty(Settings.LoginToken)) + { + Logger.Debug("Trying to login with last known token..."); + loggedIn= await DMLClient.Login(Settings.LoginToken); + } + + if (!loggedIn) + { + Logger.Debug("Showing dialog for username and password..."); + var loginDlg = new LoginDialog(); + loginDlg.ShowDialog(); + } + } + + /*while ((Client.LoginState != LoginState.LoggedIn || Client.ConnectionState!=ConnectionState.Connected) && !abort) + { + Logger.Debug(Client.ConnectionState.ToString()); + Logger.Debug(Client.LoginState.ToString()); + + Logger.Trace("Entering login loop."); + + try + { + if (Client.ConnectionState == ConnectionState.Connecting) + continue; + + if (!string.IsNullOrEmpty(Settings.LoginToken)) + { + Logger.Debug("Trying to login with last known token..."); + await Client.LoginAsync(TokenType.User, Settings.LoginToken); + await Client.StartAsync(); + await Task.Delay(1000); + } + + } + catch (HttpException ex) + { + Logger.Warn($"Login seems to have failed or gone wrong: {ex.GetType().Name} - {ex.Message}"); + } + + if (Client.LoginState == LoginState.LoggedOut) + { + Settings.Password = string.Empty; + Logger.Debug("Showing dialog for username and password..."); + var loginDlg = new LoginDialog(); + loginDlg.ShowDialog(); + Logger.Trace("Dialog closed."); + } + }*/ + + Logger.Debug("Start checking for invalid jobs..."); + + //Client + + while (DMLClient.Client.Guilds.Count == 0) + { + // wait until guilds are loaded + } + + for (var i = Scheduler.JobList.Count - 1; i >= 0; i--) + { + var job = Scheduler.JobList[i]; + var isError = false; + var guild = FindServerById(job.GuildId); + if (guild == null) + isError = true; + else + { + var channel = FindChannelById(guild, job.ChannelId); + if (channel == null) + isError = true; + } + + if (isError) + { + MessageBox.Show($"Invalid job for guild {job.GuildId}, channel {job.ChannelId} found. Guild or channel may not exist any more. This job will be deleted...", "Invalid job", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + + Scheduler.JobList.Remove(job); + Scheduler.RunningJobs.Remove(job.Id); + job.Stop(); + job.Delete(); + } + } + + splash.Close(); + + Logger.Info("Starting scheduler..."); + Scheduler.Start(); + + System.Windows.Forms.Application.Run(new MainForm()); + + Logger.Info("Stopping scheduler..."); + Scheduler.Stop(); + } + catch (Exception ex) + { + Logger.Error($"{ex.Message} occured at: {ex.StackTrace}"); + } + } + + private static Task Client_Connected() + { + Logger.Debug("Connected"); + return Task.CompletedTask; + } + + //TODO: this is thrid time we implement this.....this has to be fixed!!! + private static SocketGuild FindServerById(ulong id) + { + Logger.Trace($"Trying to find server by Id: {id}"); + return (from s in DMLClient.Client.Guilds where s.Id == id select s).FirstOrDefault(); + } + + private static SocketTextChannel FindChannelById(SocketGuild server, ulong id) + { + Logger.Trace($"Trying to find channel in {server} by id: {id}"); + return (from c in server.TextChannels where c.Id == id select c).FirstOrDefault(); + } + } +} diff --git a/Discord Media Loader.Application/Classes/Job.cs b/Discord Media Loader.Application/Classes/Job.cs index 78ed1c8..0a6d57c 100644 --- a/Discord Media Loader.Application/Classes/Job.cs +++ b/Discord Media Loader.Application/Classes/Job.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Eventing.Reader; using System.Linq; -using System.Text; using System.Threading.Tasks; using Discord; +using Discord.WebSocket; +using DML.Application.Classes; +using DML.Client; using SweetLib.Utils; using static SweetLib.Utils.Logger.Logger; -namespace DML.Application.Classes +namespace DML.AppCore.Classes { public class Job { @@ -37,7 +38,7 @@ namespace DML.Application.Classes } } - internal void Delete() + public void Delete() { Debug("Deleting job from database..."); Trace("Getting jobs collection..."); @@ -47,22 +48,22 @@ namespace DML.Application.Classes jobDb.Delete(Id); } - private Server FindServerById(ulong id) + private SocketGuild FindServerById(ulong id) { Trace($"Trying to find server by Id: {id}"); - return (from s in Core.Client.Servers where s.Id == id select s).FirstOrDefault(); + return (from s in DMLClient.Client.Guilds where s.Id == id select s).FirstOrDefault(); } - private Channel FindChannelById(Server server, ulong id) + private SocketTextChannel FindChannelById(SocketGuild server, ulong id) { Trace($"Trying to find channel in {server} by id: {id}"); return (from c in server.TextChannels where c.Id == id select c).FirstOrDefault(); } - internal async Task> Scan() + internal async Task> Scan() { Debug($"Starting scan of guild {GuildId} channel {ChannelId}..."); - var result = new List(); + var result = new List(); var limit = 100; var lastId = ulong.MaxValue; @@ -72,6 +73,13 @@ namespace DML.Application.Classes var guild = FindServerById(GuildId); var channel = FindChannelById(guild, ChannelId); + Debug("Checking channel access"); + if (!channel.Users.Contains(channel.Guild.CurrentUser)) + { + Info("Skipping channel without access"); + return result; + } + if (Math.Abs(StopTimestamp) < 0.4) StopTimestamp = KnownTimestamp; Trace("Initialized scanning parameters."); @@ -79,18 +87,37 @@ namespace DML.Application.Classes while (!finished) { Trace("Entering scanning loop..."); - Message[] messages; + var messages = new List(); Trace($"Downloading next {limit} messages..."); if (isFirst) { - messages = await channel.DownloadMessages(limit, null); + //messages = await channel.GetMessagesAsync(limit).ToArray() as SocketMessage[]; + var realMessages = await channel.GetMessagesAsync(limit).ToArray(); + + foreach (var realMessageArray in realMessages) + { + foreach (var realMessage in realMessageArray) + { + messages.Add(realMessage); + } + } } else { - messages = await channel.DownloadMessages(limit, lastId); + var realMessages = await channel.GetMessagesAsync(lastId, Direction.Before, limit).ToArray(); + + foreach (var realMessageArray in realMessages) + { + foreach (var realMessage in realMessageArray) + { + messages.Add(realMessage); + } + } + + //messages = await channel.GetMessagesAsync(lastId, Direction.Before, limit).ToArray() as SocketMessage[]; } - Trace($"Downloaded {messages.Length} messages."); + Trace($"Downloaded {messages.Count} messages."); isFirst = false; @@ -106,18 +133,18 @@ namespace DML.Application.Classes lastId = m.Id; } - if (SweetUtils.DateTimeToUnixTimeStamp(m.Timestamp) <= StopTimestamp) + if (SweetUtils.DateTimeToUnixTimeStamp(m.CreatedAt.UtcDateTime) <= StopTimestamp) { Debug("Found a message with a known timestamp...Stopping scan."); finished = true; continue; } - Trace($"Message {m.Id} has {m.Attachments.Length} attachments."); - if (m.Attachments.Length > 0) + Trace($"Message {m.Id} has {m.Attachments.Count} attachments."); + if (m.Attachments.Count > 0) { result.Add(m); - Core.Scheduler.TotalAttachments++; + Core.Scheduler.TotalAttachments += (ulong)m.Attachments.Count; Trace($"Added message {m.Id}"); } Debug($"Finished message {m.Id}"); @@ -125,17 +152,17 @@ namespace DML.Application.Classes Core.Scheduler.MessagesScanned++; } - finished = finished || messages.Length < limit; + finished = finished || messages.Count < limit; } Trace($"Downloaded all messages for guild {GuildId} channel {ChannelId}."); Trace("Sorting messages..."); - result.Sort((a, b) => DateTime.Compare(a.Timestamp, b.Timestamp)); + result.Sort((a, b) => DateTime.Compare(a.CreatedAt.UtcDateTime, b.CreatedAt.UtcDateTime)); if (result.Count > 0) { Trace("Updating StopTimestamp for next scan..."); - StopTimestamp = SweetUtils.DateTimeToUnixTimeStamp(result[result.Count - 1].Timestamp); + StopTimestamp = SweetUtils.DateTimeToUnixTimeStamp(result[result.Count - 1].CreatedAt.UtcDateTime); } Debug($"Fisnished scan of guild {GuildId} channel {ChannelId}."); @@ -143,12 +170,12 @@ namespace DML.Application.Classes return result; } - internal void Stop() + public void Stop() { IsValid = false; } - internal static IEnumerable RestoreJobs() + public static IEnumerable RestoreJobs() { Debug("Restoring jobs..."); Trace("Getting jobs collection..."); diff --git a/Discord Media Loader.Application/Classes/JobScheduler.cs b/Discord Media Loader.Application/Classes/JobScheduler.cs index f5eefcf..8d4f504 100644 --- a/Discord Media Loader.Application/Classes/JobScheduler.cs +++ b/Discord Media Loader.Application/Classes/JobScheduler.cs @@ -1,27 +1,27 @@ using System; -using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; -using System.Text; using System.Threading.Tasks; using Discord; +using Discord.WebSocket; +using DML.Application.Classes; using SweetLib.Utils; -using static SweetLib.Utils.Logger.Logger; +using SweetLib.Utils.Logger; -namespace DML.Application.Classes +namespace DML.AppCore.Classes { - internal class JobScheduler + public class JobScheduler { private ulong messageScanned = 0; private ulong totalAttachments = 0; private ulong attachmentsDownloaded = 0; private bool Run { get; set; } = false; - internal List JobList { get; set; } = new List(); - internal Dictionary> RunningJobs = new Dictionary>(); + public List JobList { get; set; } = new List(); + public Dictionary> RunningJobs = new Dictionary>(); internal int RunningThreads { get; set; } = 0; internal ulong MessagesScanned @@ -78,86 +78,84 @@ namespace DML.Application.Classes } } - internal ulong AttachmentsToDownload => TotalAttachments - AttachmentsDownloaded; - - internal void Stop() + public void Stop() { Run = false; } - internal void Start() + public void Start() { Run = true; Task.Run(async () => { - Info("Started JobScheduler..."); + Logger.Info("Started JobScheduler..."); while (Run) { - Debug("Entering job list handler loop..."); + Logger.Debug("Entering job list handler loop..."); //foreach (var job in JobList) - for(var i = JobList.Count-1;i>=0;i--) + for (var i = JobList.Count - 1; i >= 0; i--) { var job = JobList[i]; - Debug($"Checking job {job}"); + Logger.Debug($"Checking job {job}"); var hasJob = false; - Trace("Locking scheduler..."); + Logger.Trace("Locking scheduler..."); lock (this) { - Trace("Checking if job is already performed..."); + Logger.Trace("Checking if job is already performed..."); hasJob = RunningJobs.ContainsKey(job.Id); } - Trace("Unlocked scheduler."); + Logger.Trace("Unlocked scheduler."); if (!hasJob) { - Debug("Job is not performed yet...Performing job..."); - var queue = new Queue(); + Logger.Debug("Job is not performed yet...Performing job..."); + var queue = new Queue(); - Trace("Locking scheduler..."); + Logger.Trace("Locking scheduler..."); lock (this) { - Trace("Adding job to running jobs."); + Logger.Trace("Adding job to running jobs."); RunningJobs.Add(job.Id, queue); } - Trace("Unlocked scheduler."); + Logger.Trace("Unlocked scheduler."); - Trace("Issuing job message scan..."); + Logger.Trace("Issuing job message scan..."); var messages = await job.Scan(); - if(messages==null) + if (messages == null) continue; - Trace($"Adding {messages.Count} messages to queue..."); + Logger.Trace($"Adding {messages.Count} messages to queue..."); foreach (var msg in messages) { queue.Enqueue(msg); } - Trace($"Added {queue.Count} messages to queue."); + Logger.Trace($"Added {queue.Count} messages to queue."); if (messages.Count != queue.Count) - Warn("Not all messages have been added into the queue."); + Logger.Warn("Not all messages have been added into the queue."); var startedDownload = false; while (!startedDownload) { - Debug("Entering loop to check thread availability"); - Trace("Locking scheduler..."); + Logger.Debug("Entering loop to check thread availability"); + Logger.Trace("Locking scheduler..."); lock (this) { - Trace($"Checking thread limit. Running: {RunningThreads}, Max: {Core.Settings.ThreadLimit}"); + Logger.Trace($"Checking thread limit. Running: {RunningThreads}, Max: {Core.Settings.ThreadLimit}"); if (RunningThreads >= Core.Settings.ThreadLimit) continue; RunningThreads++; startedDownload = true; } - Trace("Unlocked scheduler."); + Logger.Trace("Unlocked scheduler."); } - Trace("Start downloading job async."); + Logger.Trace("Start downloading job async."); Task.Run(() => WorkQueue(job.Id)); // do not await to work parallel } } @@ -169,98 +167,110 @@ namespace DML.Application.Classes { try { - Debug("Beginning job download..."); - Trace("Finding job..."); + Logger.Debug("Beginning job download..."); + Logger.Trace("Finding job..."); var job = (from j in JobList where j.Id == jobId select j).FirstOrDefault(); if (job == null) { - Warn($"Associating job not found! JobId: {jobId}"); + Logger.Warn($"Associating job not found! JobId: {jobId}"); return; } - Trace("Found job."); + Logger.Trace("Found job."); - Queue queue; - Trace("Locking scheduler..."); + Queue queue; + Logger.Trace("Locking scheduler..."); lock (this) { - Trace("Finiding queue..."); + Logger.Trace("Finiding queue..."); if (!RunningJobs.TryGetValue(jobId, out queue)) { - Warn($"Queue for job {jobId} not found!"); + Logger.Warn($"Queue for job {jobId} not found!"); return; } - Trace("Queue found."); + Logger.Trace("Queue found."); } - Trace("Unlocked scheduler."); + Logger.Trace("Unlocked scheduler."); - Debug($"Messages to process for job {jobId}: {queue.Count}"); + Logger.Debug($"Messages to process for job {jobId}: {queue.Count}"); while (queue.Count > 0) { - Trace("Locking scheduler..."); + Logger.Trace("Locking scheduler..."); lock (this) { - Trace("Checking if still a job..."); + Logger.Trace("Checking if still a job..."); if (!RunningJobs.ContainsKey(jobId)) { - Warn($"Queue for job {jobId} not found!"); + Logger.Warn($"Queue for job {jobId} not found!"); return; } - Trace("Continue working..."); + Logger.Trace("Continue working..."); } - Trace("Unlocked scheduler."); + Logger.Trace("Unlocked scheduler."); - Trace("Dequeueing message..."); + Logger.Trace("Dequeueing message..."); var message = queue.Dequeue(); - Debug($"Attachments for message {message.Id}: {message.Attachments.Length}"); + Logger.Debug($"Attachments for message {message.Id}: {message.Attachments.Count}"); foreach (var a in message.Attachments) { try { var fileName = Path.Combine(Core.Settings.OperatingFolder, Core.Settings.FileNameScheme); - Trace("Replacing filename placeholders..."); + Logger.Trace("Replacing filename placeholders..."); var extensionRequired = !fileName.EndsWith("%name%"); - fileName = - fileName.Replace("%guild%", message.Server.Name.Replace(":","").Replace("/","").Replace("\\","")) - .Replace("%channel%", message.Channel.Name) - .Replace("%timestamp%", SweetUtils.DateTimeToUnixTimeStamp(message.Timestamp).ToString()) - .Replace("%name%", a.Filename) - .Replace("%id%", a.Id); + var serverName = "unknown"; + var socketTextChannel = message.Channel as SocketTextChannel; + if (socketTextChannel != null) + { + serverName = socketTextChannel.Guild.Name; + serverName = Path.GetInvalidFileNameChars().Aggregate(serverName, (current, c) => current.Replace(c, ' ')); + } + + var channelName = message.Channel.Name; + channelName = Path.GetInvalidFileNameChars().Aggregate(channelName, (current, c) => current.Replace(c, ' ')); + + fileName = + fileName.Replace("%guild%", serverName) + .Replace("%channel%", channelName) + .Replace("%timestamp%", SweetUtils.DateTimeToUnixTimeStamp(message.CreatedAt.UtcDateTime).ToString()) + .Replace("%name%", a.Filename) + .Replace("%id%", a.Id.ToString()); + if (extensionRequired) fileName += Path.GetExtension(a.Filename); - Trace($"Detemined file name: {fileName}."); - + Logger.Trace($"Detemined file name: {fileName}."); + if (File.Exists(fileName) && new FileInfo(fileName).Length == a.Size) { - Debug($"{fileName} already existing with its estimated size. Skipping..."); + Logger.Debug($"{fileName} already existing with its estimated size. Skipping..."); continue; } - Trace("Determining directory..."); + Logger.Trace("Determining directory..."); var fileDirectory = Path.GetDirectoryName(fileName); if (!Directory.Exists(fileDirectory)) { - Info($"Directory {fileDirectory} does not exist. Creating directory..."); + Logger.Info($"Directory {fileDirectory} does not exist. Creating directory..."); Directory.CreateDirectory(fileDirectory); - Debug("Created directory."); + Logger.Debug("Created directory."); } var wc = new WebClient(); - Debug($"Starting downloading of attachment {a.Id}..."); + Logger.Debug($"Starting downloading of attachment {a.Id}..."); wc.DownloadFile(new Uri(a.Url), fileName); - Debug($"Downloaded attachment {a.Id}."); + Logger.Debug($"Downloaded attachment {a.Id}."); - Trace("Updating known timestamp for job..."); - job.KnownTimestamp = SweetUtils.DateTimeToUnixTimeStamp(message.Timestamp); + Logger.Trace("Updating known timestamp for job..."); + job.KnownTimestamp = SweetUtils.DateTimeToUnixTimeStamp(message.CreatedAt.UtcDateTime); job.Store(); } finally @@ -272,15 +282,15 @@ namespace DML.Application.Classes } finally { - Trace("Locking scheduler..."); + Logger.Trace("Locking scheduler..."); lock (this) { - Trace($"Removing {jobId} from running jobs..."); + Logger.Trace($"Removing {jobId} from running jobs..."); RunningJobs.Remove(jobId); - Trace("Decreasing thread count..."); + Logger.Trace("Decreasing thread count..."); RunningThreads--; } - Trace("Unlocked scheduler."); + Logger.Trace("Unlocked scheduler."); } } } diff --git a/Discord Media Loader.Application/Classes/Settings.cs b/Discord Media Loader.Application/Classes/Settings.cs index 9984ca8..4cc9067 100644 --- a/Discord Media Loader.Application/Classes/Settings.cs +++ b/Discord Media Loader.Application/Classes/Settings.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics; using SweetLib.Utils.Logger; -using static SweetLib.Utils.Logger.Logger; namespace DML.Application.Classes { - internal class Settings + public class Settings { public int Id { get; } = 1; // using always unique ID public string Email { get; set; } @@ -23,21 +17,21 @@ namespace DML.Application.Classes public bool SkipExistingFiles { get; set; } = true; public int ThreadLimit { get; set; } = 50; - internal void Store() + public void Store() { - Trace("Getting settings collection..."); + Logger.Trace("Getting settings collection..."); var settingsDB = Core.Database.GetCollection("settings"); - Debug("Storing settings to database..."); + Logger.Debug("Storing settings to database..."); if (settingsDB.Exists(_setting => _setting.Id == Id)) { - Trace("Updating existing value..."); + Logger.Trace("Updating existing value..."); settingsDB.Update(this); } else { - Trace("Adding new value..."); + Logger.Trace("Adding new value..."); settingsDB.Insert(this); } } diff --git a/Discord Media Loader.Application/DML.Application.csproj b/Discord Media Loader.Application/DML.Application.csproj index 84faa2b..32a438f 100644 --- a/Discord Media Loader.Application/DML.Application.csproj +++ b/Discord Media Loader.Application/DML.Application.csproj @@ -30,17 +30,39 @@ 4 - - ..\packages\Discord.Net.0.9.6\lib\net45\Discord.Net.dll - True + + ..\packages\Discord.Net.Commands.1.0.2\lib\netstandard1.1\Discord.Net.Commands.dll + + + ..\packages\Discord.Net.Core.1.0.2\lib\net45\Discord.Net.Core.dll + + + ..\packages\Discord.Net.Rest.1.0.2\lib\net45\Discord.Net.Rest.dll + + + ..\packages\Discord.Net.Rpc.1.0.2\lib\net45\Discord.Net.Rpc.dll + + + ..\packages\Discord.Net.Webhook.1.0.2\lib\netstandard1.1\Discord.Net.Webhook.dll + + + ..\packages\Discord.Net.WebSocket.1.0.2\lib\net45\Discord.Net.WebSocket.dll ..\packages\LiteDB.3.1.0\lib\net35\LiteDB.dll True - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Microsoft.Extensions.DependencyInjection.1.1.1\lib\netstandard1.1\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.1.1.1\lib\netstandard1.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + + + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll ..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll @@ -59,25 +81,83 @@ True + + ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll + + + ..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + + + ..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll + + + ..\packages\System.Interactive.Async.3.1.1\lib\net46\System.Interactive.Async.dll + + + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + True + + + + ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + + + ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + + + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + + + ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll + + + ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + + + ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + - + + ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + ..\packages\WebSocket4Net.0.14.1\lib\net45\WebSocket4Net.dll True + - - Form @@ -102,6 +182,7 @@ True Resources.resx + @@ -119,6 +200,7 @@ + @@ -126,6 +208,10 @@ {02c1f8ef-32f2-4e77-a36d-79129402af37} SweetLib + + {045eb4a1-34e7-47e0-867e-e10c40505095} + DML.Client + diff --git a/Discord Media Loader.Application/Dialogs/LoginDialog.Designer.cs b/Discord Media Loader.Application/Dialogs/LoginDialog.Designer.cs index fd09c4b..0a48921 100644 --- a/Discord Media Loader.Application/Dialogs/LoginDialog.Designer.cs +++ b/Discord Media Loader.Application/Dialogs/LoginDialog.Designer.cs @@ -30,25 +30,12 @@ { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LoginDialog)); this.pnlButtons = new System.Windows.Forms.Panel(); - this.tbcLoginMethods = new System.Windows.Forms.TabControl(); - this.tpgToken = new System.Windows.Forms.TabPage(); - this.tpgUserdata = new System.Windows.Forms.TabPage(); - this.lbToken = new System.Windows.Forms.Label(); - this.edToken = new System.Windows.Forms.TextBox(); - this.lbHowToToken = new System.Windows.Forms.Label(); - this.edEmail = new System.Windows.Forms.TextBox(); - this.edPassword = new System.Windows.Forms.TextBox(); - this.lbEmail = new System.Windows.Forms.Label(); - this.lbPassword = new System.Windows.Forms.Label(); - this.cbUseUserdata = new System.Windows.Forms.CheckBox(); - this.cbSavePassword = new System.Windows.Forms.CheckBox(); - this.lbUserdataHints = new System.Windows.Forms.Label(); - this.btnOk = new System.Windows.Forms.Button(); this.btnAbort = new System.Windows.Forms.Button(); + this.btnOk = new System.Windows.Forms.Button(); + this.lbHowToToken = new System.Windows.Forms.Label(); + this.edToken = new System.Windows.Forms.TextBox(); + this.lbToken = new System.Windows.Forms.Label(); this.pnlButtons.SuspendLayout(); - this.tbcLoginMethods.SuspendLayout(); - this.tpgToken.SuspendLayout(); - this.tpgUserdata.SuspendLayout(); this.SuspendLayout(); // // pnlButtons @@ -62,133 +49,15 @@ this.pnlButtons.Size = new System.Drawing.Size(426, 51); this.pnlButtons.TabIndex = 0; // - // tbcLoginMethods + // btnAbort // - this.tbcLoginMethods.Controls.Add(this.tpgToken); - this.tbcLoginMethods.Controls.Add(this.tpgUserdata); - this.tbcLoginMethods.Dock = System.Windows.Forms.DockStyle.Fill; - this.tbcLoginMethods.Location = new System.Drawing.Point(0, 0); - this.tbcLoginMethods.Name = "tbcLoginMethods"; - this.tbcLoginMethods.SelectedIndex = 0; - this.tbcLoginMethods.Size = new System.Drawing.Size(426, 168); - this.tbcLoginMethods.TabIndex = 1; - // - // tpgToken - // - this.tpgToken.Controls.Add(this.lbHowToToken); - this.tpgToken.Controls.Add(this.edToken); - this.tpgToken.Controls.Add(this.lbToken); - this.tpgToken.Location = new System.Drawing.Point(4, 22); - this.tpgToken.Name = "tpgToken"; - this.tpgToken.Padding = new System.Windows.Forms.Padding(3); - this.tpgToken.Size = new System.Drawing.Size(418, 142); - this.tpgToken.TabIndex = 0; - this.tpgToken.Text = "Token"; - this.tpgToken.UseVisualStyleBackColor = true; - // - // tpgUserdata - // - this.tpgUserdata.Controls.Add(this.lbUserdataHints); - this.tpgUserdata.Controls.Add(this.cbSavePassword); - this.tpgUserdata.Controls.Add(this.cbUseUserdata); - this.tpgUserdata.Controls.Add(this.lbPassword); - this.tpgUserdata.Controls.Add(this.lbEmail); - this.tpgUserdata.Controls.Add(this.edPassword); - this.tpgUserdata.Controls.Add(this.edEmail); - this.tpgUserdata.Location = new System.Drawing.Point(4, 22); - this.tpgUserdata.Name = "tpgUserdata"; - this.tpgUserdata.Padding = new System.Windows.Forms.Padding(3); - this.tpgUserdata.Size = new System.Drawing.Size(418, 142); - this.tpgUserdata.TabIndex = 1; - this.tpgUserdata.Text = "Userdata"; - this.tpgUserdata.UseVisualStyleBackColor = true; - // - // lbToken - // - this.lbToken.AutoSize = true; - this.lbToken.Location = new System.Drawing.Point(3, 9); - this.lbToken.Name = "lbToken"; - this.lbToken.Size = new System.Drawing.Size(66, 13); - this.lbToken.TabIndex = 0; - this.lbToken.Text = "Login token:"; - // - // edToken - // - this.edToken.Location = new System.Drawing.Point(75, 6); - this.edToken.Name = "edToken"; - this.edToken.Size = new System.Drawing.Size(335, 20); - this.edToken.TabIndex = 1; - // - // lbHowToToken - // - this.lbHowToToken.Location = new System.Drawing.Point(3, 52); - this.lbHowToToken.Name = "lbHowToToken"; - this.lbHowToToken.Size = new System.Drawing.Size(412, 87); - this.lbHowToToken.TabIndex = 2; - this.lbHowToToken.Text = resources.GetString("lbHowToToken.Text"); - // - // edEmail - // - this.edEmail.Location = new System.Drawing.Point(47, 6); - this.edEmail.Name = "edEmail"; - this.edEmail.Size = new System.Drawing.Size(133, 20); - this.edEmail.TabIndex = 0; - // - // edPassword - // - this.edPassword.Location = new System.Drawing.Point(279, 6); - this.edPassword.Name = "edPassword"; - this.edPassword.PasswordChar = '•'; - this.edPassword.Size = new System.Drawing.Size(133, 20); - this.edPassword.TabIndex = 1; - // - // lbEmail - // - this.lbEmail.AutoSize = true; - this.lbEmail.Location = new System.Drawing.Point(6, 9); - this.lbEmail.Name = "lbEmail"; - this.lbEmail.Size = new System.Drawing.Size(35, 13); - this.lbEmail.TabIndex = 2; - this.lbEmail.Text = "Email:"; - // - // lbPassword - // - this.lbPassword.AutoSize = true; - this.lbPassword.Location = new System.Drawing.Point(217, 9); - this.lbPassword.Name = "lbPassword"; - this.lbPassword.Size = new System.Drawing.Size(56, 13); - this.lbPassword.TabIndex = 3; - this.lbPassword.Text = "Password:"; - // - // cbUseUserdata - // - this.cbUseUserdata.AutoSize = true; - this.cbUseUserdata.Location = new System.Drawing.Point(6, 32); - this.cbUseUserdata.Name = "cbUseUserdata"; - this.cbUseUserdata.Size = new System.Drawing.Size(139, 17); - this.cbUseUserdata.TabIndex = 4; - this.cbUseUserdata.Text = "Use login with user data"; - this.cbUseUserdata.UseVisualStyleBackColor = true; - // - // cbSavePassword - // - this.cbSavePassword.AutoSize = true; - this.cbSavePassword.Location = new System.Drawing.Point(313, 32); - this.cbSavePassword.Name = "cbSavePassword"; - this.cbSavePassword.Size = new System.Drawing.Size(99, 17); - this.cbSavePassword.TabIndex = 5; - this.cbSavePassword.Text = "Save password"; - this.cbSavePassword.UseVisualStyleBackColor = true; - // - // lbUserdataHints - // - this.lbUserdataHints.Location = new System.Drawing.Point(3, 52); - this.lbUserdataHints.Name = "lbUserdataHints"; - this.lbUserdataHints.Size = new System.Drawing.Size(412, 87); - this.lbUserdataHints.TabIndex = 6; - this.lbUserdataHints.Text = "Login with email and password is not recommended. If you use two factor authentic" + - "ation this can cause a ban of your discord account.\r\n\r\nFor safety reasons we rec" + - "ommend to login with login token."; + this.btnAbort.Location = new System.Drawing.Point(348, 16); + this.btnAbort.Name = "btnAbort"; + this.btnAbort.Size = new System.Drawing.Size(75, 23); + this.btnAbort.TabIndex = 1; + this.btnAbort.Text = "&Abort"; + this.btnAbort.UseVisualStyleBackColor = true; + this.btnAbort.Click += new System.EventHandler(this.btnAbort_Click); // // btnOk // @@ -200,22 +69,38 @@ this.btnOk.UseVisualStyleBackColor = true; this.btnOk.Click += new System.EventHandler(this.btnOk_Click); // - // btnAbort + // lbHowToToken // - this.btnAbort.Location = new System.Drawing.Point(348, 16); - this.btnAbort.Name = "btnAbort"; - this.btnAbort.Size = new System.Drawing.Size(75, 23); - this.btnAbort.TabIndex = 1; - this.btnAbort.Text = "&Abort"; - this.btnAbort.UseVisualStyleBackColor = true; - this.btnAbort.Click += new System.EventHandler(this.btnAbort_Click); + this.lbHowToToken.Location = new System.Drawing.Point(7, 58); + this.lbHowToToken.Name = "lbHowToToken"; + this.lbHowToToken.Size = new System.Drawing.Size(412, 87); + this.lbHowToToken.TabIndex = 5; + this.lbHowToToken.Text = resources.GetString("lbHowToToken.Text"); + // + // edToken + // + this.edToken.Location = new System.Drawing.Point(79, 12); + this.edToken.Name = "edToken"; + this.edToken.Size = new System.Drawing.Size(335, 20); + this.edToken.TabIndex = 4; + // + // lbToken + // + this.lbToken.AutoSize = true; + this.lbToken.Location = new System.Drawing.Point(7, 15); + this.lbToken.Name = "lbToken"; + this.lbToken.Size = new System.Drawing.Size(66, 13); + this.lbToken.TabIndex = 3; + this.lbToken.Text = "Login token:"; // // LoginDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(426, 219); - this.Controls.Add(this.tbcLoginMethods); + this.Controls.Add(this.lbHowToToken); + this.Controls.Add(this.edToken); + this.Controls.Add(this.lbToken); this.Controls.Add(this.pnlButtons); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); @@ -227,32 +112,18 @@ this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.LoginDialog_FormClosing); this.Shown += new System.EventHandler(this.LoginDialog_Shown); this.pnlButtons.ResumeLayout(false); - this.tbcLoginMethods.ResumeLayout(false); - this.tpgToken.ResumeLayout(false); - this.tpgToken.PerformLayout(); - this.tpgUserdata.ResumeLayout(false); - this.tpgUserdata.PerformLayout(); this.ResumeLayout(false); + this.PerformLayout(); } #endregion private System.Windows.Forms.Panel pnlButtons; - private System.Windows.Forms.TabControl tbcLoginMethods; - private System.Windows.Forms.TabPage tpgToken; + private System.Windows.Forms.Button btnAbort; + private System.Windows.Forms.Button btnOk; private System.Windows.Forms.Label lbHowToToken; private System.Windows.Forms.TextBox edToken; private System.Windows.Forms.Label lbToken; - private System.Windows.Forms.TabPage tpgUserdata; - private System.Windows.Forms.Button btnAbort; - private System.Windows.Forms.Button btnOk; - private System.Windows.Forms.Label lbUserdataHints; - private System.Windows.Forms.CheckBox cbSavePassword; - private System.Windows.Forms.CheckBox cbUseUserdata; - private System.Windows.Forms.Label lbPassword; - private System.Windows.Forms.Label lbEmail; - private System.Windows.Forms.TextBox edPassword; - private System.Windows.Forms.TextBox edEmail; } } \ No newline at end of file diff --git a/Discord Media Loader.Application/Dialogs/LoginDialog.cs b/Discord Media Loader.Application/Dialogs/LoginDialog.cs index 6163db7..16ca42f 100644 --- a/Discord Media Loader.Application/Dialogs/LoginDialog.cs +++ b/Discord Media Loader.Application/Dialogs/LoginDialog.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; +using DML.Application.Classes; using static SweetLib.Utils.Logger.Logger; namespace DML.Application.Dialogs @@ -22,12 +23,6 @@ namespace DML.Application.Dialogs { Trace("Login dialog shown."); edToken.Text = Core.Settings.LoginToken; - edEmail.Text = Core.Settings.Email; - edPassword.Text = Core.Settings.Password; - cbUseUserdata.Checked = Core.Settings.UseUserData; - cbSavePassword.Checked = Core.Settings.SavePassword; - - tbcLoginMethods.SelectedTab = Core.Settings.UseUserData ? tpgUserdata : tpgToken; } private void LoginDialog_FormClosing(object sender, FormClosingEventArgs e) @@ -38,10 +33,6 @@ namespace DML.Application.Dialogs Debug("Adjusting login settings..."); Core.Settings.LoginToken = edToken.Text; - Core.Settings.Email = edEmail.Text; - Core.Settings.Password = edPassword.Text; - Core.Settings.UseUserData = cbUseUserdata.Checked; - Core.Settings.SavePassword = cbSavePassword.Checked; Core.Settings.Store(); } diff --git a/Discord Media Loader.Application/MainForm.cs b/Discord Media Loader.Application/MainForm.cs index d630aef..7d629b9 100644 --- a/Discord Media Loader.Application/MainForm.cs +++ b/Discord Media Loader.Application/MainForm.cs @@ -4,7 +4,11 @@ using System.Linq; using System.Reflection; using System.Windows.Forms; using Discord; +using Discord.WebSocket; +using DML.AppCore; +using DML.AppCore.Classes; using DML.Application.Classes; +using DML.Client; using static SweetLib.Utils.Logger.Logger; namespace DML.Application @@ -46,7 +50,7 @@ namespace DML.Application if (cbGuild.Items.Count == 0) { Trace("Adding guilds to component..."); - cbGuild.Items.AddRange(Core.Client.Servers.OrderBy(g => g.Name).Select(g => g.Name).ToArray()); + cbGuild.Items.AddRange(DMLClient.Client.Guilds.OrderBy(g => g.Name).Select(g => g.Name).ToArray()); cbGuild.SelectedIndex = 0; Trace("Guild component initialized."); } @@ -108,25 +112,25 @@ namespace DML.Application } } - private Server FindServerByName(string name) + private SocketGuild FindServerByName(string name) { Trace($"Trying to find server by name: {name}"); - return (from s in Core.Client.Servers where s.Name == name select s).FirstOrDefault(); + return (from s in DMLClient.Client.Guilds where s.Name == name select s).FirstOrDefault(); } - private Channel FindChannelByName(Server server, string name) + private SocketTextChannel FindChannelByName(SocketGuild server, string name) { Trace($"Trying to find channel in {server} by name: {name}"); return (from c in server.TextChannels where c.Name == name select c).FirstOrDefault(); } - private Server FindServerById(ulong id) + private SocketGuild FindServerById(ulong id) { Trace($"Trying to find server by Id: {id}"); - return (from s in Core.Client.Servers where s.Id == id select s).FirstOrDefault(); + return (from s in DMLClient.Client.Guilds where s.Id == id select s).FirstOrDefault(); } - private Channel FindChannelById(Server server, ulong id) + private SocketTextChannel FindChannelById(SocketGuild server, ulong id) { Trace($"Trying to find channel in {server} by id: {id}"); return (from c in server.TextChannels where c.Id == id select c).FirstOrDefault(); diff --git a/Discord Media Loader.Application/app.config b/Discord Media Loader.Application/app.config new file mode 100644 index 0000000..5b184e5 --- /dev/null +++ b/Discord Media Loader.Application/app.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Discord Media Loader.Application/packages.config b/Discord Media Loader.Application/packages.config index f45fb55..d3b12fc 100644 --- a/Discord Media Loader.Application/packages.config +++ b/Discord Media Loader.Application/packages.config @@ -1,9 +1,65 @@  - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Discord Media Loader.sln b/Discord Media Loader.sln index c56a43c..8124baf 100644 --- a/Discord Media Loader.sln +++ b/Discord Media Loader.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26403.7 +VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord Media Loader", "Discord Media Loader\Discord Media Loader.csproj", "{EDC92554-DBC1-4F9C-9317-379A8BF441E8}" EndProject @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DML.Application", "Discord EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SweetLib", "..\SweetLib\SweetLib\SweetLib.csproj", "{02C1F8EF-32F2-4E77-A36D-79129402AF37}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DML.Client", "DML.Client\DML.Client.csproj", "{045EB4A1-34E7-47E0-867E-E10C40505095}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,8 +29,15 @@ Global {02C1F8EF-32F2-4E77-A36D-79129402AF37}.Debug|Any CPU.Build.0 = Debug|Any CPU {02C1F8EF-32F2-4E77-A36D-79129402AF37}.Release|Any CPU.ActiveCfg = Release|Any CPU {02C1F8EF-32F2-4E77-A36D-79129402AF37}.Release|Any CPU.Build.0 = Release|Any CPU + {045EB4A1-34E7-47E0-867E-E10C40505095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {045EB4A1-34E7-47E0-867E-E10C40505095}.Debug|Any CPU.Build.0 = Debug|Any CPU + {045EB4A1-34E7-47E0-867E-E10C40505095}.Release|Any CPU.ActiveCfg = Release|Any CPU + {045EB4A1-34E7-47E0-867E-E10C40505095}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0B742DE0-D6AF-4033-9605-863C32A7FFD8} + EndGlobalSection EndGlobal diff --git a/Discord Media Loader/App.config b/Discord Media Loader/App.config index bae5d6d..3bddcc4 100644 --- a/Discord Media Loader/App.config +++ b/Discord Media Loader/App.config @@ -1,6 +1,22 @@ - + - + + + + + + + + + + + + + + + + + diff --git a/Discord Media Loader/Discord Media Loader.csproj b/Discord Media Loader/Discord Media Loader.csproj index 79fc96c..0977fa7 100644 --- a/Discord Media Loader/Discord Media Loader.csproj +++ b/Discord Media Loader/Discord Media Loader.csproj @@ -40,6 +40,36 @@ + + ..\packages\Discord.Net.Commands.1.0.2\lib\netstandard1.1\Discord.Net.Commands.dll + + + ..\packages\Discord.Net.Core.1.0.2\lib\net45\Discord.Net.Core.dll + + + ..\packages\Discord.Net.Rest.1.0.2\lib\net45\Discord.Net.Rest.dll + + + ..\packages\Discord.Net.Rpc.1.0.2\lib\net45\Discord.Net.Rpc.dll + + + ..\packages\Discord.Net.Webhook.1.0.2\lib\netstandard1.1\Discord.Net.Webhook.dll + + + ..\packages\Discord.Net.WebSocket.1.0.2\lib\net45\Discord.Net.WebSocket.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.1.1.1\lib\netstandard1.1\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.1.1.1\lib\netstandard1.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + + + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + ..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll True @@ -57,17 +87,75 @@ True + + ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll + + + ..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + + + ..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll + + + ..\packages\System.Interactive.Async.3.1.1\lib\net46\System.Interactive.Async.dll + + + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + True + + + ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + + + ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + + + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + + + ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll + + + ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + + + ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + - + + ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + diff --git a/Discord Media Loader/FrmSplash.cs b/Discord Media Loader/FrmSplash.cs index 8d0957b..33558d1 100644 --- a/Discord Media Loader/FrmSplash.cs +++ b/Discord Media Loader/FrmSplash.cs @@ -25,7 +25,7 @@ namespace Discord_Media_Loader UseWaitCursor = true; try { - var releaseVersion = await VersionHelper.GetReleaseVersion(); + /*var releaseVersion = await VersionHelper.GetReleaseVersion(); if (releaseVersion > VersionHelper.CurrentVersion) { var tmpFile = Path.GetTempFileName(); @@ -52,7 +52,7 @@ namespace Discord_Media_Loader } File.Delete(tmpFile); - } + }*/ } finally { diff --git a/Discord Media Loader/Program.cs b/Discord Media Loader/Program.cs index 613769a..cd6f3b8 100644 --- a/Discord Media Loader/Program.cs +++ b/Discord Media Loader/Program.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Windows.Forms; using Discord_Media_Loader.Helper; using DML.Application; +using DML.Application.Classes; using Nito.AsyncEx; namespace Discord_Media_Loader diff --git a/Discord Media Loader/packages.config b/Discord Media Loader/packages.config index 4832885..eff2573 100644 --- a/Discord Media Loader/packages.config +++ b/Discord Media Loader/packages.config @@ -1,5 +1,64 @@  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file