From fb69f8430c2a01ed7386122b439719b906ded69f Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:17:24 +0200 Subject: [PATCH 01/11] Switch to Integration Framework for handling commands --- Commands/CommandManager.cs | 112 -------------------------- Commands/InteractionServiceHandler.cs | 93 +++++++++++++++++++++ Program.cs | 12 ++- 3 files changed, 103 insertions(+), 114 deletions(-) delete mode 100644 Commands/CommandManager.cs create mode 100644 Commands/InteractionServiceHandler.cs diff --git a/Commands/CommandManager.cs b/Commands/CommandManager.cs deleted file mode 100644 index 8bd2f7e..0000000 --- a/Commands/CommandManager.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace QuickEdit.Commands; -public class CommandManager -{ - private readonly DiscordSocketClient _client = Program.client; - - // Macros for interaction context types - private static readonly InteractionContextType[] interactionContextAll = new InteractionContextType[] { InteractionContextType.PrivateChannel, InteractionContextType.Guild, InteractionContextType.BotDm }; - private static readonly InteractionContextType[] interactionContextUser = new InteractionContextType[] { InteractionContextType.PrivateChannel, InteractionContextType.Guild }; - - #region Command List - List slashCommandBuilders = new List() { - new SlashCommandBuilder() - .WithName("test") - .WithDescription("Test command.") - .WithIntegrationTypes(ApplicationIntegrationType.UserInstall) - .WithContextTypes(interactionContextAll), - - new SlashCommandBuilder() - .WithName("trim") - .WithDescription("Trim a video") - .WithIntegrationTypes(ApplicationIntegrationType.UserInstall) - .WithContextTypes(interactionContextUser) - .AddOption(new SlashCommandOptionBuilder() - .WithName("video") - .WithDescription("The video to trim") - .WithType(ApplicationCommandOptionType.Attachment) - .WithRequired(true)) - .AddOption(new SlashCommandOptionBuilder() - .WithName("start") - .WithDescription("What time should the video start? [XXh XXm XXs XXms]") - .WithType(ApplicationCommandOptionType.String) // TODO: Change to ApplicationCommandOptionType.Time if added one day - .WithAutocomplete(true) - .WithMinLength(2) // The time cannot be expressed with less than 2 characters - .WithRequired(false)) - .AddOption(new SlashCommandOptionBuilder() - .WithName("end") - .WithDescription("What time should the video end? [XXh XXm XXs XXms]") - .WithType(ApplicationCommandOptionType.String) // TODO: Change to ApplicationCommandOptionType.Time if added one day - .WithMinLength(2) // The time cannot be expressed with less than 2 characters - .WithRequired(false)) - .AddOption(new SlashCommandOptionBuilder() - .WithName("message") - .WithDescription("A message to send with the video when it's trimmed") - .WithType(ApplicationCommandOptionType.String) - .WithRequired(false)) - .AddOption(new SlashCommandOptionBuilder() - .WithName("ephemeral") - .WithDescription("If the video should be sent as a temporary message, that's only visible to you") - .WithType(ApplicationCommandOptionType.Boolean) - .WithRequired(false)) - }; - #endregion - - public async Task InitAsync() - { - // Build and register commands - var builtCommands = BulkBuildCommands(slashCommandBuilders); - await RegisterCommandsAsync(builtCommands); - - _client.SlashCommandExecuted += SlashCommandHandlerAsync; - } - - private List BulkBuildCommands(List commandBuilders) { - var builtCommands = new List(); - foreach (var commandBuilder in commandBuilders) { - builtCommands.Add(commandBuilder.Build()); - } - - return builtCommands; - } - - public async Task RegisterCommandsAsync(List slashCommands) { - try { - await _client.BulkOverwriteGlobalApplicationCommandsAsync(slashCommands.ToArray()); - await Program.LogAsync("CommandManager", "Successfully registered slash commands."); - } - catch { - await Program.LogAsync("CommandManager", "Failed to register slash commands.", LogSeverity.Critical); - return; - } - } - - private async Task SlashCommandHandlerAsync(SocketSlashCommand command) - { - switch (command.Data.Name) - { - case "test": - command.RespondAsync("Test command executed!"); - break; - - case "trim": - VideoUtils.TrimVideoAsync(command); - break; - - // In case the command is not recognized by the bot - default: - await command.RespondAsync("An error occurred with the command you tried to execute", ephemeral: true); - await Program.LogAsync("CommandManager", "Failed to execute slash command.", LogSeverity.Error); - break; - } - } -} \ No newline at end of file diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs new file mode 100644 index 0000000..71eb92a --- /dev/null +++ b/Commands/InteractionServiceHandler.cs @@ -0,0 +1,93 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; + +namespace QuickEdit.Commands; +public class InteractionServiceHandler +{ + private static readonly DiscordSocketClient _client = Program.client; + private static InteractionService? _interactionService; + private static readonly InteractionServiceConfig _interactionServiceConfig = new() { UseCompiledLambda = true, DefaultRunMode = RunMode.Async }; + + /// + /// Initialize the InteractionService + /// + /// True if success, false if failure + public static async Task InitAsync() + { + try + { + _interactionService = new InteractionService(_client.Rest, _interactionServiceConfig); + await RegisterModulesAsync(); + } + catch + { + await Program.LogAsync("InteractionServiceHandler", "Error initializing InteractionService", LogSeverity.Critical); + throw; + } + } + + /// + /// Register modules / commands + /// + public static async Task RegisterModulesAsync() + { + // The service might not have been initialized yet + if (_interactionService == null) + { + await Program.LogAsync("InteractionServiceManager.RegisterModulesAsync()", "InteractionService not initialized yet", LogSeverity.Error); + throw new Exception("InteractionService not initialized while trying to register commands"); + } + + try + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + await _interactionService.AddModulesAsync(assembly, null); + } + + await _interactionService.RegisterCommandsGloballyAsync(); + _client.InteractionCreated += OnInteractionCreatedAsync; + await Program.LogAsync("InteractionServiceManager", "Modules registered successfully", LogSeverity.Info); + } + catch (Exception e) + { + await Program.LogAsync("InteractionServiceManager", $"Error registering modules. ({e})", LogSeverity.Critical); + throw; + } + } + + public static async Task OnInteractionCreatedAsync(SocketInteraction interaction) + { + // The service might not have been initialized yet + if (_interactionService == null) + { + await Program.LogAsync("InteractionServiceManager.OnInteractionCreatedAsync()", "InteractionService not initialized yet", LogSeverity.Error); + return; + } + + try + { + var ctx = new SocketInteractionContext(_client, interaction); + var res = await _interactionService.ExecuteCommandAsync(ctx, null); + + if (res.IsSuccess is false) + { + await Program.LogAsync("InteractionServiceManager", $"Error handling interaction: {res}", LogSeverity.Error); + await ctx.Channel.SendMessageAsync(res.ToString()); + } + } + catch (Exception e) + { + await Program.LogAsync("InteractionServiceManager", $"Error handling interaction. {e.Message}", LogSeverity.Error); + + if (interaction.Type is InteractionType.ApplicationCommand) + { + await interaction.GetOriginalResponseAsync().ContinueWith(async msg => await msg.Result.DeleteAsync()); + } + + throw; + } + } +} diff --git a/Program.cs b/Program.cs index beabf5d..521e756 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.WebSocket; using QuickEdit; using QuickEdit.Commands; @@ -36,7 +36,15 @@ public async Task MainAsync() private async Task OnReadyAsync() { - await new CommandManager().InitAsync(); + try + { + await InteractionServiceHandler.InitAsync(); + } + catch + { + await LogAsync("Program", "Exiting", LogSeverity.Info); + Environment.Exit(1); + } } public Task LogAsync(LogMessage message) From 5dc84ed85f428cf51f20457a28d5c54fa2e26b41 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:25:55 +0200 Subject: [PATCH 02/11] Minor Code Cleanup Automatic cleanup using Visual Studio --- ConfigManager.cs | 9 ++++++--- Program.cs | 12 +++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ConfigManager.cs b/ConfigManager.cs index 6a54252..9354f9f 100644 --- a/ConfigManager.cs +++ b/ConfigManager.cs @@ -20,10 +20,13 @@ public class Config return null; } - try { + try + { return JsonConvert.DeserializeObject(File.ReadAllText(path))!; - } catch { - Program.LogAsync("Config" , "Failed to parse config file.", LogSeverity.Critical); + } + catch + { + Program.LogAsync("Config", "Failed to parse config file.", LogSeverity.Critical); return null; } } diff --git a/Program.cs b/Program.cs index 521e756..20b3a32 100644 --- a/Program.cs +++ b/Program.cs @@ -1,12 +1,11 @@ -using Discord; +using Discord; using Discord.WebSocket; -using QuickEdit; using QuickEdit.Commands; namespace QuickEdit; class Program { - public static DiscordSocketClient client; + public static DiscordSocketClient? client; public static Config? config = Config.GetConfig(); public static Task Main(string[] args) => new Program().MainAsync(); @@ -25,9 +24,12 @@ public async Task MainAsync() await client.StartAsync(); // Custom activities use a different method - if (config.statusType == ActivityType.CustomStatus) { + if (config.statusType == ActivityType.CustomStatus) + { await client.SetCustomStatusAsync(config.status); - } else { + } + else + { await client.SetGameAsync(config.status, null, config.statusType); } From 58a6d5f2b9c52c094f03b67aa21270ae4b9c90fa Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:49:22 +0200 Subject: [PATCH 03/11] Fix CS8601 --- Commands/InteractionServiceHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index 71eb92a..dc9afd2 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -5,7 +5,7 @@ namespace QuickEdit.Commands; public class InteractionServiceHandler { - private static readonly DiscordSocketClient _client = Program.client; + private static readonly DiscordSocketClient? _client = Program.client; private static InteractionService? _interactionService; private static readonly InteractionServiceConfig _interactionServiceConfig = new() { UseCompiledLambda = true, DefaultRunMode = RunMode.Async }; @@ -17,7 +17,7 @@ public static async Task InitAsync() { try { - _interactionService = new InteractionService(_client.Rest, _interactionServiceConfig); + _interactionService = new InteractionService(_client!.Rest, _interactionServiceConfig); await RegisterModulesAsync(); } catch @@ -48,7 +48,7 @@ public static async Task RegisterModulesAsync() } await _interactionService.RegisterCommandsGloballyAsync(); - _client.InteractionCreated += OnInteractionCreatedAsync; + _client!.InteractionCreated += OnInteractionCreatedAsync; await Program.LogAsync("InteractionServiceManager", "Modules registered successfully", LogSeverity.Info); } catch (Exception e) From 0ed2b70325ccb346c69b0b61fb5f1c603e534899 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 17 Jun 2024 00:18:13 +0200 Subject: [PATCH 04/11] Port VideoUtils to InteractionFramework --- Commands/Modules/VideoUtils.cs | 243 +++++++++++++++++++++++++++++++++ Commands/VideoUtils.cs | 148 -------------------- 2 files changed, 243 insertions(+), 148 deletions(-) create mode 100644 Commands/Modules/VideoUtils.cs delete mode 100644 Commands/VideoUtils.cs diff --git a/Commands/Modules/VideoUtils.cs b/Commands/Modules/VideoUtils.cs new file mode 100644 index 0000000..ae02517 --- /dev/null +++ b/Commands/Modules/VideoUtils.cs @@ -0,0 +1,243 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using FFMpegCore; +using FFMpegCore.Extend; +using System.Net.Mail; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace QuickEdit.Commands.Modules; +[Group("video", "Video utilities")] +[IntegrationType(ApplicationIntegrationType.UserInstall)] +[CommandContextType(InteractionContextType.Guild, InteractionContextType.PrivateChannel)] +public class VideoUtils : InteractionModuleBase +{ + [SlashCommand("trim", "Trim a video")] + public async Task TrimVideoAsync( + [Summary(description: "The video to trim")] Discord.Attachment video, + [Summary("start", "What time should the video start? [XXh XXm XXs XXms]")] string trimStartString = "", + [Summary("end", "What time should the video end? [XXh XXm XXs XXms]")] string trimEndString = "", + [Summary(description: "A message to send with the video when it's trimmed")] string message = "", + [Summary(description: "If the video should be sent as a temporary message, that's only visible to you")] bool ephemeral = false) + { + string videoInputPath = "./tmp/input.mp4"; // Normally, I would use Path.GetTempFileName(), but FFMpegCore doesn't seem to + string videoOutputPath = "./tmp/output.mp4"; // like the .tmp file extension (or anything other than .mp4) as far as i know + + // Achknowledge the command + await DeferAsync(ephemeral); + + // There is a similar check for the TimeSpan library below, but this it to avoid + if (trimStartString == "" && trimEndString == "") + { + await FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); + return; + } + + // Reject incorrect video formats + if (video.ContentType != "video/mp4") + { + await FollowupAsync("Invalid video format. Please provide an MP4 file.", ephemeral: true); + return; + } + + // Get TimeSpans + TimeSpan trimStart; + TimeSpan trimEnd; + try { + if (trimStartString != "") // Avoid invalid format exceptions + trimStart = TimeSpanFromHMS(trimStartString); + else + trimStart = TimeSpan.Zero; + + if (trimEndString != "") // Avoid invalid format exceptions + trimEnd = TimeSpanFromHMS(trimEndString); + else + trimEnd = TimeSpan.Zero; + } catch (Exception e) { + if (e is ArgumentException) + { + await FollowupAsync("Invalid time format. Please provide a valid time format (XXh XXm XXs XXms).", ephemeral: true); + } else { + throw; + } + return; + } + + // Make sure the times are not negative | https://stackoverflow.com/a/1018659/17003609 (comment) + trimStart = trimStart.Duration(); + trimEnd = trimEnd.Duration(); + + // The video can't be trimmed if both start and end times are 0 + if (trimStart == TimeSpan.Zero && trimEnd == TimeSpan.Zero) + { + await FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); + return; + } + + await DownloadVideoAsync(video.Url, videoInputPath); + + var mediaInfo = await FFProbe.AnalyseAsync(videoInputPath); + CheckTimes(ref trimStart, ref trimEnd, mediaInfo.Duration); + + // Check if the temporary directory, where the video is supposed to be exists + if (!Directory.Exists("./tmp")) + { + Directory.CreateDirectory("./tmp"); + } + + // Process and send video + await FFMpeg.SubVideoAsync(videoInputPath, videoOutputPath, (TimeSpan)trimStart, (TimeSpan)trimEnd); // Need to convert the TimeSpans since the value is nullable + await FollowupWithFileAsync(videoOutputPath, video.Filename, message, ephemeral: ephemeral); + + // Clean up + File.Delete(videoInputPath); + File.Delete(videoOutputPath); + } + + /// + /// Check and set the start and end times to follow restrictions: + /// + /// Set to if smaller or equal to + /// Clamp to the + /// Set to 0 if it's greater or equal to the video's + /// + /// + /// Start of the trim + /// End of the trim + /// Duration of the video + private static void CheckTimes(ref TimeSpan trimStart, ref TimeSpan trimEnd, TimeSpan duration) + { + // Set trimEnd to duration if smaller or equal to trimStart + if (trimEnd <= trimStart) + { + trimEnd = duration; + } + + // Clamp the end time to the video's duration + trimEnd = new[] { duration, trimEnd }.Min(); // https://stackoverflow.com/a/1985326/17003609 + + // Set trimStart to 0 if it's greater or equal to the video's duration + if (trimStart >= duration) + { + trimStart = TimeSpan.Zero; + } + } + + private static async Task DownloadVideoAsync(string uri, string path) + { + using var client = new HttpClient(); + using var s = await client.GetStreamAsync(uri); + using var fs = new FileStream(path, FileMode.OpenOrCreate); + await s.CopyToAsync(fs); + fs.Close(); + } + + /// + /// Parses a string in the format 'XXh XXm XXs XXms' into a TimeSpan object + /// + /// Input string to parse, in format [XXh XXm XXs] + /// The parsed TimeSpan + /// Thrown when the input string is in an invalid format + public static TimeSpan TimeSpanFromHMS(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + throw new ArgumentException("Input string is not in a valid format"); + } + + // Define the regular expression pattern to match hours, minutes, and seconds + string pattern = @"((?\d+)h)?\s*((?\d+)m|min)?\s*((?\d+)s)?\s*((?\d+)ms)?"; + + // Match the input string with the pattern + var match = Regex.Match(input, pattern, RegexOptions.IgnoreCase); + + // Check if at least one component (hours, minutes, or seconds) is present + if (!match.Groups["hours"].Success && !match.Groups["minutes"].Success && !match.Groups["seconds"].Success && !match.Groups["milliseconds"].Success) + { + throw new ArgumentException("Input string is not in a valid format"); + } + + // Extract the matched groups + int hours = 0; + if (match.Groups["hours"].Success) int.TryParse(match.Groups["hours"].Value, out hours); + int minutes = 0; + if (match.Groups["minutes"].Success) int.TryParse(match.Groups["minutes"].Value, out minutes); + int seconds = 0; + if (match.Groups["seconds"].Success) int.TryParse(match.Groups["seconds"].Value, out seconds); + int milliseconds = 0; + if (match.Groups["milliseconds"].Success) int.TryParse(match.Groups["milliseconds"].Value, out milliseconds); + + // Create and return the TimeSpan object + return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); + } + + /* + private static async Task DownloadVideoAsync(string uri, string path) + { + using var client = new HttpClient(); + using var s = await client.GetStreamAsync(uri); + using var fs = new FileStream(path, FileMode.OpenOrCreate); + await s.CopyToAsync(fs); + fs.Close(); + } + + private static async Task GetTrimTimeAsync(string? timeString, SocketSlashCommand command) + { + if (timeString == null) + { + // This will later be replaced with the video's duration + return TimeSpan.Zero; + } + + try + { + return TimeSpanFromHMS(timeString); + } + catch + { + await command.FollowupAsync("Invalid time format. Please provide a valid time format (XXh XXm XXs XXms).", ephemeral: true); + await Program.LogAsync("VideoUtils", $"Invalid time format in TrimVideoAsync (received: {timeString})", LogSeverity.Verbose); + return null; + } + } + + /// + /// Parses a string in the format 'XXh XXm XXs XXms' into a TimeSpan object + /// + /// Input string to parse, in format [XXh XXm XXs] + /// The parsed TimeSpan + /// Thrown when the input string is in an invalid format + public static TimeSpan TimeSpanFromHMS(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + throw new ArgumentException("Input string is not in a valid format"); + } + + // Define the regular expression pattern to match hours, minutes, and seconds + string pattern = @"((?\d+)ms)?\s*((?\d+)h)?\s*((?\d+)m|min)?\s*((?\d+)s)?"; + + // Match the input string with the pattern + var match = Regex.Match(input, pattern, RegexOptions.IgnoreCase); + + // Check if at least one component (hours, minutes, or seconds) is present + if (!match.Groups["hours"].Success && !match.Groups["minutes"].Success && !match.Groups["seconds"].Success && !match.Groups["milliseconds"].Success) + { + throw new ArgumentException("Input string is not in a valid format"); + } + + // Extract the matched groups + int hours = 0; + if (match.Groups["hours"].Success) int.TryParse(match.Groups["hours"].Value, out hours); + int minutes = 0; + if (match.Groups["minutes"].Success) int.TryParse(match.Groups["minutes"].Value, out minutes); + int seconds = 0; + if (match.Groups["seconds"].Success) int.TryParse(match.Groups["seconds"].Value, out seconds); + int milliseconds = 0; + if (match.Groups["milliseconds"].Success) int.TryParse(match.Groups["milliseconds"].Value, out milliseconds); + + // Create and return the TimeSpan object + return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); + }*/ +} \ No newline at end of file diff --git a/Commands/VideoUtils.cs b/Commands/VideoUtils.cs deleted file mode 100644 index 5c88882..0000000 --- a/Commands/VideoUtils.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Discord; -using Discord.WebSocket; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FFMpegCore; -using FFMpegCore.Pipes; -using System.Text.RegularExpressions; -using System.Net.Mail; -using System.Security.Cryptography.X509Certificates; - -namespace QuickEdit.Commands; -public class VideoUtils -{ - public static async Task TrimVideoAsync(SocketSlashCommand command) - { - // Get arguments - string? trimStartString = command.Data.Options.FirstOrDefault(x => x.Name == "start")?.Value as string; - string? trimEndString = command.Data.Options.FirstOrDefault(x => x.Name == "end")?.Value as string; - string message = command.Data.Options.FirstOrDefault(x => x.Name == "message")?.Value as string ?? string.Empty; - bool ephemeral = command.Data.Options.FirstOrDefault(x => x.Name == "ephemeral")?.Value as bool? ?? false; - var attachment = command.Data.Options.FirstOrDefault(x => x.Name == "video")?.Value as Discord.Attachment; - - string videoInputPath = "./tmp/input.mp4"; // Normally, I would use Path.GetTempFileName(), but FFMpegCore doesn't seem to - string videoOutputPath = "./tmp/output.mp4"; // like the .tmp file extension (or anything other than .mp4) as far as i know - - // Achknowledge the command - await command.DeferAsync(ephemeral); - - // The video can't be trimmed if both start and end times are null / 0 - if (trimStartString == null && trimEndString == null) { - await command.FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); - return; - } - - // The attachment should never be null, as it's a required option - if (attachment == null) - { - await command.FollowupAsync("An error occurred while trying to process the video. Please try again.", ephemeral: true); - await Program.LogAsync("VideoUtils", "Attachment was null in TrimVideoAsync", LogSeverity.Error); - return; - } - - // Reject incorrect formats - if (attachment.ContentType != "video/mp4") - { - await command.FollowupAsync("Invalid video format. Please provide an MP4 file.", ephemeral: true); - return; - } - - TimeSpan? trimStart = await GetTrimTimeAsync(trimStartString, command); - TimeSpan? trimEnd = await GetTrimTimeAsync(trimEndString, command); - - // The GetTrimTime method returns null on error - if (trimStart == null || trimEnd == null) return; - - // Check if the directory, where the video is supposed to be exists - if (!Directory.Exists("./tmp")) - { - Directory.CreateDirectory("./tmp"); - } - - await DownloadVideoAsync(attachment.Url, videoInputPath); - - // Replace the end time with the video's duration if it's 0, greater than the video's duration, or smaller than the start time - if (trimEnd <= trimStart) - { - var mediaInfo = await FFProbe.AnalyseAsync(videoInputPath); - trimEnd = mediaInfo.Duration; - } - - // Process and send video - await FFMpeg.SubVideoAsync(videoInputPath, videoOutputPath, (TimeSpan)trimStart, (TimeSpan)trimEnd); // Need to convert the TimeSpans since the value is nullable - await command.FollowupWithFileAsync(videoOutputPath, attachment.Filename, message, ephemeral: ephemeral); - - // Clean up - File.Delete(videoInputPath); - File.Delete(videoOutputPath); - } - - private static async Task DownloadVideoAsync(string uri, string path) { - using var client = new HttpClient(); - using var s = await client.GetStreamAsync(uri); - using var fs = new FileStream(path, FileMode.OpenOrCreate); - await s.CopyToAsync(fs); - fs.Close(); - } - - private static async Task GetTrimTimeAsync(string? timeString, SocketSlashCommand command) { - if (timeString == null) - { - // This will later be replaced with the video's duration - return TimeSpan.Zero; - } - - try - { - return TimeSpanFromHMS(timeString); - } - catch - { - await command.FollowupAsync("Invalid time format. Please provide a valid time format (XXh XXm XXs XXms).", ephemeral: true); - await Program.LogAsync("VideoUtils", $"Invalid time format in TrimVideoAsync (received: {timeString})", LogSeverity.Verbose); - return null; - } - } - - /// - /// Parses a string in the format 'XXh XXm XXs XXms' into a TimeSpan object - /// - /// Input string to parse, in format [XXh XXm XXs] - /// The parsed TimeSpan - /// Thrown when the input string is in an invalid format - public static TimeSpan TimeSpanFromHMS(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentException("Input string is not in a valid format"); - } - - // Define the regular expression pattern to match hours, minutes, and seconds - string pattern = @"((?\d+)ms)?\s*((?\d+)h)?\s*((?\d+)m|min)?\s*((?\d+)s)?"; - - // Match the input string with the pattern - var match = Regex.Match(input, pattern, RegexOptions.IgnoreCase); - - // Check if at least one component (hours, minutes, or seconds) is present - if (!match.Groups["hours"].Success && !match.Groups["minutes"].Success && !match.Groups["seconds"].Success && !match.Groups["milliseconds"].Success) - { - throw new ArgumentException("Input string is not in a valid format"); - } - - // Extract the matched groups - int hours = 0; - if (match.Groups["hours"].Success) int.TryParse(match.Groups["hours"].Value, out hours); - int minutes = 0; - if (match.Groups["minutes"].Success) int.TryParse(match.Groups["minutes"].Value, out minutes); - int seconds = 0; - if (match.Groups["seconds"].Success) int.TryParse(match.Groups["seconds"].Value, out seconds); - int milliseconds = 0; - if (match.Groups["milliseconds"].Success) int.TryParse(match.Groups["milliseconds"].Value, out milliseconds); - - // Create and return the TimeSpan object - return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); - } -} \ No newline at end of file From 40ebe8ac3754819955be52651df12cddc119aa8f Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 17 Jun 2024 00:18:31 +0200 Subject: [PATCH 05/11] Fix default project namespace --- Discord QuickEdit.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Discord QuickEdit.csproj b/Discord QuickEdit.csproj index 37fc5a3..ab87d46 100644 --- a/Discord QuickEdit.csproj +++ b/Discord QuickEdit.csproj @@ -3,7 +3,7 @@ Exe net8.0 - Discord_QuickEdit + QuickEdit enable enable From 1b2a37095b8b701491cf10be5b3aa4322bb92f1f Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 22:40:02 +0000 Subject: [PATCH 06/11] refactor: autofix issues in 1 file Resolved issues in Commands/Modules/VideoUtils.cs with DeepSource Autofix --- Commands/Modules/VideoUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/Modules/VideoUtils.cs b/Commands/Modules/VideoUtils.cs index ae02517..354b110 100644 --- a/Commands/Modules/VideoUtils.cs +++ b/Commands/Modules/VideoUtils.cs @@ -45,12 +45,12 @@ public async Task TrimVideoAsync( TimeSpan trimStart; TimeSpan trimEnd; try { - if (trimStartString != "") // Avoid invalid format exceptions + if (!string.IsNullOrEmpty(trimStartString)) // Avoid invalid format exceptions trimStart = TimeSpanFromHMS(trimStartString); else trimStart = TimeSpan.Zero; - if (trimEndString != "") // Avoid invalid format exceptions + if (!string.IsNullOrEmpty(trimEndString)) // Avoid invalid format exceptions trimEnd = TimeSpanFromHMS(trimEndString); else trimEnd = TimeSpan.Zero; From db3378bb3239a2c77c9faa15fe5ae21732a8add0 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 17 Jun 2024 01:31:30 +0200 Subject: [PATCH 07/11] Remove commented out old code --- Commands/Modules/VideoUtils.cs | 69 ---------------------------------- 1 file changed, 69 deletions(-) diff --git a/Commands/Modules/VideoUtils.cs b/Commands/Modules/VideoUtils.cs index 354b110..b7f8dde 100644 --- a/Commands/Modules/VideoUtils.cs +++ b/Commands/Modules/VideoUtils.cs @@ -171,73 +171,4 @@ public static TimeSpan TimeSpanFromHMS(string input) // Create and return the TimeSpan object return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); } - - /* - private static async Task DownloadVideoAsync(string uri, string path) - { - using var client = new HttpClient(); - using var s = await client.GetStreamAsync(uri); - using var fs = new FileStream(path, FileMode.OpenOrCreate); - await s.CopyToAsync(fs); - fs.Close(); - } - - private static async Task GetTrimTimeAsync(string? timeString, SocketSlashCommand command) - { - if (timeString == null) - { - // This will later be replaced with the video's duration - return TimeSpan.Zero; - } - - try - { - return TimeSpanFromHMS(timeString); - } - catch - { - await command.FollowupAsync("Invalid time format. Please provide a valid time format (XXh XXm XXs XXms).", ephemeral: true); - await Program.LogAsync("VideoUtils", $"Invalid time format in TrimVideoAsync (received: {timeString})", LogSeverity.Verbose); - return null; - } - } - - /// - /// Parses a string in the format 'XXh XXm XXs XXms' into a TimeSpan object - /// - /// Input string to parse, in format [XXh XXm XXs] - /// The parsed TimeSpan - /// Thrown when the input string is in an invalid format - public static TimeSpan TimeSpanFromHMS(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentException("Input string is not in a valid format"); - } - - // Define the regular expression pattern to match hours, minutes, and seconds - string pattern = @"((?\d+)ms)?\s*((?\d+)h)?\s*((?\d+)m|min)?\s*((?\d+)s)?"; - - // Match the input string with the pattern - var match = Regex.Match(input, pattern, RegexOptions.IgnoreCase); - - // Check if at least one component (hours, minutes, or seconds) is present - if (!match.Groups["hours"].Success && !match.Groups["minutes"].Success && !match.Groups["seconds"].Success && !match.Groups["milliseconds"].Success) - { - throw new ArgumentException("Input string is not in a valid format"); - } - - // Extract the matched groups - int hours = 0; - if (match.Groups["hours"].Success) int.TryParse(match.Groups["hours"].Value, out hours); - int minutes = 0; - if (match.Groups["minutes"].Success) int.TryParse(match.Groups["minutes"].Value, out minutes); - int seconds = 0; - if (match.Groups["seconds"].Success) int.TryParse(match.Groups["seconds"].Value, out seconds); - int milliseconds = 0; - if (match.Groups["milliseconds"].Success) int.TryParse(match.Groups["milliseconds"].Value, out milliseconds); - - // Create and return the TimeSpan object - return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); - }*/ } \ No newline at end of file From d522e35dcca3c8d68a047eaccf8231114cafc54c Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 17 Jun 2024 02:09:27 +0200 Subject: [PATCH 08/11] Change assembly name --- Discord QuickEdit.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Discord QuickEdit.csproj b/Discord QuickEdit.csproj index ab87d46..653e44e 100644 --- a/Discord QuickEdit.csproj +++ b/Discord QuickEdit.csproj @@ -6,6 +6,7 @@ QuickEdit enable enable + quickedit From 82205509d24e290f2c32dc475e51ce572c37f3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20D=C4=99bski?= <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 17 Jun 2024 02:30:59 +0200 Subject: [PATCH 09/11] Fix 2 minor "bugs" Change empty string comparison to string.isNullOrEmpty and move video format check before empty time parameter check --- Commands/Modules/VideoUtils.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Commands/Modules/VideoUtils.cs b/Commands/Modules/VideoUtils.cs index b7f8dde..482f2b2 100644 --- a/Commands/Modules/VideoUtils.cs +++ b/Commands/Modules/VideoUtils.cs @@ -27,17 +27,17 @@ public async Task TrimVideoAsync( // Achknowledge the command await DeferAsync(ephemeral); - // There is a similar check for the TimeSpan library below, but this it to avoid - if (trimStartString == "" && trimEndString == "") + // Reject incorrect video formats + if (video.ContentType != "video/mp4") { - await FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); + await FollowupAsync("Invalid video format. Please provide an MP4 file.", ephemeral: true); return; } - // Reject incorrect video formats - if (video.ContentType != "video/mp4") + // There is a similar check for the TimeSpan library below, but this it to avoid + if (string.IsNullOrEmpty(trimStartString) && string.IsNullOrEmpty(trimEndString)) { - await FollowupAsync("Invalid video format. Please provide an MP4 file.", ephemeral: true); + await FollowupAsync("You must provide a start or end time to trim the video.", ephemeral: true); return; } @@ -171,4 +171,4 @@ public static TimeSpan TimeSpanFromHMS(string input) // Create and return the TimeSpan object return new TimeSpan(days: 0, hours, minutes, seconds, milliseconds); } -} \ No newline at end of file +} From 3bbde4bc1583aa8279f548e70b77814d67cb8250 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 17 Jun 2024 02:14:21 +0200 Subject: [PATCH 10/11] Remove unused properties from config --- ConfigManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ConfigManager.cs b/ConfigManager.cs index 9354f9f..3c41807 100644 --- a/ConfigManager.cs +++ b/ConfigManager.cs @@ -5,8 +5,6 @@ namespace QuickEdit; public class Config { public required string token; - public ulong logChannel; - public ulong guildID; public ActivityType statusType; public string status = string.Empty; public bool debug = false; From dd000a3d4d254bd5379b1e7d56fe64efab966957 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Tue, 18 Jun 2024 00:35:50 +0200 Subject: [PATCH 11/11] Fix DeepSource Issues Fixed issues: - Parameters marked as in, out or ref should always be placed after all the required parameters CS-R1138 (VideoUtils.cs) - Abrupt application exit CS-W1005 [ignored issue] (Program.cs) - Variable is uninitialized CS-W1022 (VideoUtils.cs) - if statement can be rewritten using the ternary operator CS-R1105 (VideoUtils.cs) --- Commands/Modules/VideoUtils.cs | 22 ++++++++-------------- Program.cs | 3 ++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Commands/Modules/VideoUtils.cs b/Commands/Modules/VideoUtils.cs index 482f2b2..bf649ce 100644 --- a/Commands/Modules/VideoUtils.cs +++ b/Commands/Modules/VideoUtils.cs @@ -42,18 +42,12 @@ public async Task TrimVideoAsync( } // Get TimeSpans - TimeSpan trimStart; - TimeSpan trimEnd; + TimeSpan trimStart = TimeSpan.Zero; + TimeSpan trimEnd = TimeSpan.Zero; try { - if (!string.IsNullOrEmpty(trimStartString)) // Avoid invalid format exceptions - trimStart = TimeSpanFromHMS(trimStartString); - else - trimStart = TimeSpan.Zero; - - if (!string.IsNullOrEmpty(trimEndString)) // Avoid invalid format exceptions - trimEnd = TimeSpanFromHMS(trimEndString); - else - trimEnd = TimeSpan.Zero; + // Avoid invalid format exceptions + if (!string.IsNullOrEmpty(trimStartString)) trimStart = TimeSpanFromHMS(trimStartString); + if (!string.IsNullOrEmpty(trimEndString)) trimEnd = TimeSpanFromHMS(trimEndString); } catch (Exception e) { if (e is ArgumentException) { @@ -78,7 +72,7 @@ public async Task TrimVideoAsync( await DownloadVideoAsync(video.Url, videoInputPath); var mediaInfo = await FFProbe.AnalyseAsync(videoInputPath); - CheckTimes(ref trimStart, ref trimEnd, mediaInfo.Duration); + CheckTimes(mediaInfo.Duration, ref trimStart, ref trimEnd); // Check if the temporary directory, where the video is supposed to be exists if (!Directory.Exists("./tmp")) @@ -103,10 +97,10 @@ public async Task TrimVideoAsync( /// Set to 0 if it's greater or equal to the video's /// /// + /// Duration of the video /// Start of the trim /// End of the trim - /// Duration of the video - private static void CheckTimes(ref TimeSpan trimStart, ref TimeSpan trimEnd, TimeSpan duration) + private static void CheckTimes(TimeSpan duration, ref TimeSpan trimStart, ref TimeSpan trimEnd) { // Set trimEnd to duration if smaller or equal to trimStart if (trimEnd <= trimStart) diff --git a/Program.cs b/Program.cs index 20b3a32..a62f0bc 100644 --- a/Program.cs +++ b/Program.cs @@ -45,7 +45,8 @@ private async Task OnReadyAsync() catch { await LogAsync("Program", "Exiting", LogSeverity.Info); - Environment.Exit(1); + // The program cannot continue without the InteractionService, so terminate it. Nothing important should be running at this point. + Environment.Exit(1); // skipcq: CS-W1005 } }