From e375d7f0bdcee7554b6bc834dd6ce5a12a995b38 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Tue, 24 Oct 2023 13:29:53 +0100 Subject: [PATCH 1/9] Migrate from System.CommandLine to Spectre.Console. --- Solutions/Stacker.Cli/BufferClient.cs | 16 +- Solutions/Stacker.Cli/BufferError.cs | 1 + .../ContentItemAttachmentPathCleaner.cs | 2 + .../Cleaners/ContentItemCleaner.cs | 2 + .../Cleaners/EnsureEndjinHttpsInBody.cs | 1 + .../Cleaners/RemoveHostNamesFromBody.cs | 1 + .../Cleaners/WordPressImageResizerCleaner.cs | 1 + .../Commands/EnvironmentCommandFactory.cs | 27 --- .../Commands/EnvironmentInitCommand.cs | 56 +++++ .../Commands/EnvironmentInitCommandFactory.cs | 58 ----- .../Commands/FacebookBufferCommand.cs | 76 +++++++ .../Commands/FacebookBufferCommandFactory.cs | 42 ---- .../Commands/FacebookCommandFactory.cs | 27 --- .../Commands/LinkedInBufferCommand.cs | 77 +++++++ .../Commands/LinkedInBufferCommandFactory.cs | 44 ---- .../Commands/LinkedInCommandFactory.cs | 27 --- .../Commands/TwitterBufferCommand.cs | 77 +++++++ .../Commands/TwitterBufferCommandFactory.cs | 44 ---- .../Commands/TwitterCommandFactory.cs | 27 --- .../Commands/WordPressCommandFactory.cs | 27 --- .../Commands/WordPressExportCommandFactory.cs | 31 --- ...y.cs => WordPressExportMarkdownCommand.cs} | 207 ++++++++++-------- .../WordPressExportUniversalCommand.cs | 122 +++++++++++ .../WordPressExportUniversalCommandFactory.cs | 110 ---------- .../FileSystemLocalProfileAppEnvironment.cs | 1 + .../Configuration/SettingsManager{T}.cs | 2 + .../Contracts/Commands/ICommandFactory.cs | 12 - .../Contracts/Formatters/IContentFormatter.cs | 1 + .../Contracts/Tasks/IContentTasks.cs | 14 +- .../Converters/PublicationPeriodConverter.cs | 24 +- .../Stacker.Cli/Domain/WordPress/BlogSite.cs | 1 + .../Domain/WordPress/PostExtensions.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 20 +- .../Formatters/LongFormContentFormatter.cs | 1 + .../Stacker.Cli/Formatters/TweetFormatter.cs | 1 + .../Infrastructure/Injection/TypeRegistrar.cs | 73 ++++++ .../Infrastructure/Injection/TypeResolver.cs | 50 +++++ .../Properties/launchSettings.json | 2 +- .../ForceQuotedStringValuesEventEmitter.cs | 1 + Solutions/Stacker.Cli/Stacker.Cli.csproj | 7 +- Solutions/Stacker.Cli/StackerCli.cs | 80 +++++-- Solutions/Stacker.Cli/Tasks/ContentTasks.cs | 22 +- Solutions/Stacker.Cli/Tasks/DownloadTasks.cs | 14 +- Solutions/Stacker.Cli/Tasks/IDownloadTasks.cs | 1 + Solutions/Stacker.Cli/packages.lock.json | 42 ++-- 45 files changed, 811 insertions(+), 662 deletions(-) delete mode 100644 Solutions/Stacker.Cli/Commands/EnvironmentCommandFactory.cs create mode 100644 Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs delete mode 100644 Solutions/Stacker.Cli/Commands/EnvironmentInitCommandFactory.cs create mode 100644 Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs delete mode 100644 Solutions/Stacker.Cli/Commands/FacebookBufferCommandFactory.cs delete mode 100644 Solutions/Stacker.Cli/Commands/FacebookCommandFactory.cs create mode 100644 Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs delete mode 100644 Solutions/Stacker.Cli/Commands/LinkedInBufferCommandFactory.cs delete mode 100644 Solutions/Stacker.Cli/Commands/LinkedInCommandFactory.cs create mode 100644 Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs delete mode 100644 Solutions/Stacker.Cli/Commands/TwitterBufferCommandFactory.cs delete mode 100644 Solutions/Stacker.Cli/Commands/TwitterCommandFactory.cs delete mode 100644 Solutions/Stacker.Cli/Commands/WordPressCommandFactory.cs delete mode 100644 Solutions/Stacker.Cli/Commands/WordPressExportCommandFactory.cs rename Solutions/Stacker.Cli/Commands/{WordPressExportMarkDownCommandFactory.cs => WordPressExportMarkdownCommand.cs} (57%) create mode 100644 Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs delete mode 100644 Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommandFactory.cs delete mode 100644 Solutions/Stacker.Cli/Contracts/Commands/ICommandFactory.cs create mode 100644 Solutions/Stacker.Cli/Infrastructure/Injection/TypeRegistrar.cs create mode 100644 Solutions/Stacker.Cli/Infrastructure/Injection/TypeResolver.cs diff --git a/Solutions/Stacker.Cli/BufferClient.cs b/Solutions/Stacker.Cli/BufferClient.cs index 4f10cab..616ba46 100644 --- a/Solutions/Stacker.Cli/BufferClient.cs +++ b/Solutions/Stacker.Cli/BufferClient.cs @@ -11,6 +11,8 @@ using Newtonsoft.Json; +using Spectre.Console; + using Stacker.Cli.Configuration; using Stacker.Cli.Contracts.Buffer; using Stacker.Cli.Contracts.Configuration; @@ -48,9 +50,9 @@ public async Task UploadAsync(IEnumerable content, string profileId) { using (HttpClient client = this.httpClientFactory.CreateClient()) { - client.BaseAddress = new(BaseUri); + client.BaseAddress = new Uri(BaseUri); client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add(new("application/x-www-form-urlencoded")); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded")); StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); string updateOperationUrl = $"{UpdateOperation}?access_token={settings.BufferAccessToken}"; @@ -59,7 +61,7 @@ public async Task UploadAsync(IEnumerable content, string profileId) { HttpContent payload = new FormUrlEncodedContent(this.ConvertToPayload(item, new string[] { profileId })); - Console.WriteLine($"Buffering: {item}"); + AnsiConsole.WriteLine($"Buffering: {item}"); HttpResponseMessage response = await client.PostAsync(updateOperationUrl, payload).ConfigureAwait(false); @@ -68,10 +70,10 @@ public async Task UploadAsync(IEnumerable content, string profileId) string errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); BufferError error = JsonConvert.DeserializeObject(errorContent); - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Buffering Failed: {error.Message}"); - Console.WriteLine(); - Console.ResetColor(); + AnsiConsole.Foreground = ConsoleColor.Red; + AnsiConsole.WriteLine($"Buffering Failed: {error.Message}"); + AnsiConsole.WriteLine(); + AnsiConsole.ResetColors(); } } } diff --git a/Solutions/Stacker.Cli/BufferError.cs b/Solutions/Stacker.Cli/BufferError.cs index da08e4e..8f13dcd 100644 --- a/Solutions/Stacker.Cli/BufferError.cs +++ b/Solutions/Stacker.Cli/BufferError.cs @@ -3,6 +3,7 @@ // using System.Collections.Generic; + using Newtonsoft.Json; namespace Stacker.Cli; diff --git a/Solutions/Stacker.Cli/Cleaners/ContentItemAttachmentPathCleaner.cs b/Solutions/Stacker.Cli/Cleaners/ContentItemAttachmentPathCleaner.cs index 9feccab..3e3b88a 100644 --- a/Solutions/Stacker.Cli/Cleaners/ContentItemAttachmentPathCleaner.cs +++ b/Solutions/Stacker.Cli/Cleaners/ContentItemAttachmentPathCleaner.cs @@ -4,7 +4,9 @@ using System; using System.Text.RegularExpressions; + using Flurl; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Cleaners; diff --git a/Solutions/Stacker.Cli/Cleaners/ContentItemCleaner.cs b/Solutions/Stacker.Cli/Cleaners/ContentItemCleaner.cs index 90ecd73..be02f62 100644 --- a/Solutions/Stacker.Cli/Cleaners/ContentItemCleaner.cs +++ b/Solutions/Stacker.Cli/Cleaners/ContentItemCleaner.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; + using Microsoft.Extensions.DependencyInjection; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Cleaners; diff --git a/Solutions/Stacker.Cli/Cleaners/EnsureEndjinHttpsInBody.cs b/Solutions/Stacker.Cli/Cleaners/EnsureEndjinHttpsInBody.cs index 262effa..cef8f60 100644 --- a/Solutions/Stacker.Cli/Cleaners/EnsureEndjinHttpsInBody.cs +++ b/Solutions/Stacker.Cli/Cleaners/EnsureEndjinHttpsInBody.cs @@ -4,6 +4,7 @@ using System; using System.Text.RegularExpressions; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Cleaners; diff --git a/Solutions/Stacker.Cli/Cleaners/RemoveHostNamesFromBody.cs b/Solutions/Stacker.Cli/Cleaners/RemoveHostNamesFromBody.cs index 2fb09fa..8417770 100644 --- a/Solutions/Stacker.Cli/Cleaners/RemoveHostNamesFromBody.cs +++ b/Solutions/Stacker.Cli/Cleaners/RemoveHostNamesFromBody.cs @@ -4,6 +4,7 @@ using System; using System.Text.RegularExpressions; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Cleaners; diff --git a/Solutions/Stacker.Cli/Cleaners/WordPressImageResizerCleaner.cs b/Solutions/Stacker.Cli/Cleaners/WordPressImageResizerCleaner.cs index 5220e11..42a3e3b 100644 --- a/Solutions/Stacker.Cli/Cleaners/WordPressImageResizerCleaner.cs +++ b/Solutions/Stacker.Cli/Cleaners/WordPressImageResizerCleaner.cs @@ -3,6 +3,7 @@ // using System.Text.RegularExpressions; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Cleaners; diff --git a/Solutions/Stacker.Cli/Commands/EnvironmentCommandFactory.cs b/Solutions/Stacker.Cli/Commands/EnvironmentCommandFactory.cs deleted file mode 100644 index 85bdeef..0000000 --- a/Solutions/Stacker.Cli/Commands/EnvironmentCommandFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; -using Stacker.Cli.Contracts.Commands; - -namespace Stacker.Cli.Commands; - -public class EnvironmentCommandFactory : ICommandFactory -{ - private readonly ICommandFactory environmentResetCommandFactory; - - public EnvironmentCommandFactory(ICommandFactory environmentResetCommandFactory) - { - this.environmentResetCommandFactory = environmentResetCommandFactory; - } - - public Command Create() - { - var cmd = new Command("environment", "Manipulate the stacker environment."); - - cmd.AddCommand(this.environmentResetCommandFactory.Create()); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs b/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs new file mode 100644 index 0000000..9c35d18 --- /dev/null +++ b/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +#nullable enable annotations + +using System.Collections.Generic; + +using Spectre.Console; +using Spectre.Console.Cli; + +using Stacker.Cli.Configuration; +using Stacker.Cli.Contracts.Configuration; + +namespace Stacker.Cli.Commands; + +public class EnvironmentInitCommand : Command +{ + private readonly IAppEnvironment appEnvironment; + private readonly IStackerSettingsManager settingsManager; + + public EnvironmentInitCommand(IAppEnvironment appEnvironment, IStackerSettingsManager settingsManager) + { + this.appEnvironment = appEnvironment; + this.settingsManager = settingsManager; + } + + public override int Execute(CommandContext context) + { + this.appEnvironment.Initialize(); + this.settingsManager.SaveSettings( + new StackerSettings + { + BufferAccessToken = "", + BufferProfiles = new() + { + { "facebook|", "" }, + { "linkedin|", "" }, + { "twitter|", "" }, + }, + Users = new List + { + new() + { + Email = string.Empty, + IsActive = true, + }, + }, + }, + nameof(StackerSettings)); + + AnsiConsole.WriteLine($"Environment Initialized {this.appEnvironment.AppPath}"); + + return 0; + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/EnvironmentInitCommandFactory.cs b/Solutions/Stacker.Cli/Commands/EnvironmentInitCommandFactory.cs deleted file mode 100644 index c333cdc..0000000 --- a/Solutions/Stacker.Cli/Commands/EnvironmentInitCommandFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Invocation; -using Stacker.Cli.Configuration; -using Stacker.Cli.Contracts.Commands; -using Stacker.Cli.Contracts.Configuration; - -namespace Stacker.Cli.Commands; - -public class EnvironmentInitCommandFactory : ICommandFactory -{ - private readonly IAppEnvironment appEnvironment; - private readonly IStackerSettingsManager settingsManager; - - public EnvironmentInitCommandFactory(IAppEnvironment appEnvironment, IStackerSettingsManager settingsManager) - { - this.appEnvironment = appEnvironment; - this.settingsManager = settingsManager; - } - - public Command Create() - { - return new("init", "Initializes the stacker environment.") - { - Handler = CommandHandler.Create(() => - { - this.appEnvironment.Initialize(); - this.settingsManager.SaveSettings( - new() - { - BufferAccessToken = "", - BufferProfiles = new() - { - { "facebook|", "" }, - { "linkedin|", "" }, - { "twitter|", "" }, - }, - Users = new() - { - new() - { - Email = string.Empty, - IsActive = true, - }, - }, - }, - nameof(StackerSettings)); - - Console.WriteLine($"Environment Initialized {this.appEnvironment.AppPath}"); - }), - }; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs new file mode 100644 index 0000000..b02bf4f --- /dev/null +++ b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +using Spectre.Console.Cli; +using Spectre.IO; + +using Stacker.Cli.Contracts.Tasks; +using Stacker.Cli.Domain.Publication; +using Stacker.Cli.Formatters; + +namespace Stacker.Cli.Commands; + +public class FacebookBufferCommand : AsyncCommand +{ + private readonly IContentTasks contentTasks; + + public FacebookBufferCommand(IContentTasks contentTasks) + { + this.contentTasks = contentTasks; + } + + /// + public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] Settings settings) + { + await this.contentTasks.BufferContentItemsAsync( + settings.ContentFilePath, + $"facebook|", + settings.ProfileName, + settings.PublicationPeriod, + settings.FromDate, + settings.ToDate, + settings.ItemCount).ConfigureAwait(false); + + return 0; + } + + /// + /// The settings for the command. + /// + public class Settings : CommandSettings + { +#nullable disable annotations + + [CommandOption("-c|--content-file-path ")] + [Description("Content file path.")] + public FilePath ContentFilePath { get; init; } + + [CommandOption("-n|--profile-name ")] + [Description("Facebook profile to Buffer.")] + public string ProfileName { get; init; } + + [CommandOption("-i|--item-count ")] + [Description("Number of content items to buffer. If omitted all content is buffered.")] + public int ItemCount { get; init; } + + [CommandOption("-p|--publication-period ")] + [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] + public PublicationPeriod PublicationPeriod { get; init; } + + [CommandOption("-f|--from-date ")] + [Description("Include content items published on, or after this date. Use YYYY/MM/DD Format. If omitted DateTime.MinValue is used.")] + public DateTime FromDate { get; init; } + + [CommandOption("-t|--to-date ")] + [Description("Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.")] + public DateTime ToDate { get; init; } + +#nullable enable annotations + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/FacebookBufferCommandFactory.cs b/Solutions/Stacker.Cli/Commands/FacebookBufferCommandFactory.cs deleted file mode 100644 index 25d9833..0000000 --- a/Solutions/Stacker.Cli/Commands/FacebookBufferCommandFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using Stacker.Cli.Contracts.Commands; -using Stacker.Cli.Contracts.Tasks; -using Stacker.Cli.Domain.Publication; -using Stacker.Cli.Formatters; - -namespace Stacker.Cli.Commands; - -public class FacebookBufferCommandFactory : ICommandFactory -{ - private readonly IContentTasks contentTasks; - - public FacebookBufferCommandFactory(IContentTasks contentTasks) - { - this.contentTasks = contentTasks; - } - - public Command Create() - { - var cmd = new Command("buffer", "Uploads content to Buffer to be published via Facebook") - { - Handler = CommandHandler.Create(async (string contentFilePath, string profileName, int itemCount, DateTime fromDate, DateTime toDate, PublicationPeriod publicationPeriod) => - await this.contentTasks.BufferContentItemsAsync(contentFilePath, $"facebook|", profileName, publicationPeriod, fromDate, toDate, itemCount).ConfigureAwait(false)), - }; - - cmd.Add(new Argument("content-file-path") { Description = "Content file path." }); - cmd.Add(new Argument("profile-name") { Description = "Facebook profile to Buffer." }); - - cmd.AddOption(new("--item-count", "Number of content items to buffer. If omitted all content is buffered.") { Argument = new Argument() }); - cmd.AddOption(new("--publication-period", "Publication period to filter content items by. If specified --from-date and --to-date are ignored.") { Argument = new Argument() }); - cmd.AddOption(new("--from-date", "Include content items published on, or after this date. Use YYYY/MM/DD Format. If omitted DateTime.MinValue is used.") { Argument = new Argument() }); - cmd.AddOption(new("--to-date", "Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.") { Argument = new Argument() }); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/FacebookCommandFactory.cs b/Solutions/Stacker.Cli/Commands/FacebookCommandFactory.cs deleted file mode 100644 index 3742c48..0000000 --- a/Solutions/Stacker.Cli/Commands/FacebookCommandFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; -using Stacker.Cli.Contracts.Commands; - -namespace Stacker.Cli.Commands; - -public class FacebookCommandFactory : ICommandFactory -{ - private readonly ICommandFactory facebookBufferCommandFactory; - - public FacebookCommandFactory(ICommandFactory facebookBufferCommandFactory) - { - this.facebookBufferCommandFactory = facebookBufferCommandFactory; - } - - public Command Create() - { - var cmd = new Command("facebook", "Facebook functionality."); - - cmd.AddCommand(this.facebookBufferCommandFactory.Create()); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs new file mode 100644 index 0000000..98967f4 --- /dev/null +++ b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +#nullable enable annotations +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +using Spectre.Console.Cli; +using Spectre.IO; + +using Stacker.Cli.Contracts.Tasks; +using Stacker.Cli.Domain.Publication; +using Stacker.Cli.Formatters; + +namespace Stacker.Cli.Commands; + +public class LinkedInBufferCommand : AsyncCommand +{ + private readonly IContentTasks contentTasks; + + public LinkedInBufferCommand(IContentTasks contentTasks) + { + this.contentTasks = contentTasks; + } + + /// + public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] Settings settings) + { + await this.contentTasks.BufferContentItemsAsync( + settings.ContentFilePath, + $"linkedin|", + settings.ProfileName, + settings.PublicationPeriod, + settings.FromDate, + settings.ToDate, + settings.ItemCount).ConfigureAwait(false); + + return 0; + } + + /// + /// The settings for the command. + /// + public class Settings : CommandSettings + { +#nullable disable annotations + + [CommandOption("-c|--content-file-path ")] + [Description("Content file path.")] + public FilePath ContentFilePath { get; init; } + + [CommandOption("-n|--profile-name ")] + [Description("LinkedIn profile to Buffer.")] + public string ProfileName { get; init; } + + [CommandOption("-i|--item-count ")] + [Description("Number of content items to buffer. If omitted all content is buffered.")] + public int ItemCount { get; init; } + + [CommandOption("-p|--publication-period ")] + [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] + public PublicationPeriod PublicationPeriod { get; init; } + + [CommandOption("-f|--from-date ")] + [Description("Include content items published on, or after this date. Use YYYY/MM/DD Format. If omitted DateTime.MinValue is used.")] + public DateTime FromDate { get; init; } + + [CommandOption("-t|--to-date ")] + [Description("Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.")] + public DateTime ToDate { get; init; } + +#nullable enable annotations + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommandFactory.cs b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommandFactory.cs deleted file mode 100644 index 35bc29c..0000000 --- a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommandFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using Stacker.Cli.Contracts.Commands; -using Stacker.Cli.Contracts.Tasks; -using Stacker.Cli.Domain.Publication; -using Stacker.Cli.Formatters; - -namespace Stacker.Cli.Commands; - -public class LinkedInBufferCommandFactory : ICommandFactory -{ - private readonly IContentTasks contentTasks; - - public LinkedInBufferCommandFactory(IContentTasks contentTasks) - { - this.contentTasks = contentTasks; - } - - public Command Create() - { - var cmd = new Command("buffer", "Uploads content to Buffer to be published via Twitter") - { - Handler = CommandHandler.Create(async (string contentFilePath, string profileName, int itemCount, DateTime fromDate, DateTime toDate, PublicationPeriod publicationPeriod) => - { - await this.contentTasks.BufferContentItemsAsync(contentFilePath, $"linkedin|", profileName, publicationPeriod, fromDate, toDate, itemCount).ConfigureAwait(false); - }), - }; - - cmd.Add(new Argument("content-file-path") { Description = "Content file path." }); - cmd.Add(new Argument("profile-name") { Description = "LinkedIn profile to Buffer." }); - - cmd.AddOption(new("--item-count", "Number of content items to buffer. If omitted all content is buffered.") { Argument = new Argument() }); - cmd.AddOption(new("--publication-period", "Publication period to filter content items by. If specified --from-date and --to-date are ignored.") { Argument = new Argument() }); - cmd.AddOption(new("--from-date", "Include content items published on, or after this date. Use YYYY/MM/DD Format. If omitted DateTime.MinValue is used.") { Argument = new Argument() }); - cmd.AddOption(new("--to-date", "Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.") { Argument = new Argument() }); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/LinkedInCommandFactory.cs b/Solutions/Stacker.Cli/Commands/LinkedInCommandFactory.cs deleted file mode 100644 index 20165e0..0000000 --- a/Solutions/Stacker.Cli/Commands/LinkedInCommandFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; -using Stacker.Cli.Contracts.Commands; - -namespace Stacker.Cli.Commands; - -public class LinkedInCommandFactory : ICommandFactory -{ - private readonly ICommandFactory twitterBufferCommandFactory; - - public LinkedInCommandFactory(ICommandFactory twitterBufferCommandFactory) - { - this.twitterBufferCommandFactory = twitterBufferCommandFactory; - } - - public Command Create() - { - var cmd = new Command("linkedin", "LinkedIn functionality."); - - cmd.AddCommand(this.twitterBufferCommandFactory.Create()); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs new file mode 100644 index 0000000..09163f0 --- /dev/null +++ b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +#nullable enable annotations +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +using Spectre.Console.Cli; +using Spectre.IO; + +using Stacker.Cli.Contracts.Tasks; +using Stacker.Cli.Domain.Publication; +using Stacker.Cli.Formatters; + +namespace Stacker.Cli.Commands; + +public class TwitterBufferCommand : AsyncCommand +{ + private readonly IContentTasks contentTasks; + + public TwitterBufferCommand(IContentTasks contentTasks) + { + this.contentTasks = contentTasks; + } + + /// + public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] Settings settings) + { + await this.contentTasks.BufferContentItemsAsync( + settings.ContentFilePath, + $"twitter|", + settings.ProfileName, + settings.PublicationPeriod, + settings.FromDate, + settings.ToDate, + settings.ItemCount).ConfigureAwait(false); + + return 0; + } + + /// + /// The settings for the command. + /// + public class Settings : CommandSettings + { +#nullable disable annotations + + [CommandOption("-c|--content-file-path ")] + [Description("Content file path.")] + public FilePath ContentFilePath { get; init; } + + [CommandOption("-n|--profile-name ")] + [Description("Twitter profile to Buffer.")] + public string ProfileName { get; init; } + + [CommandOption("-i|--item-count ")] + [Description("Number of content items to buffer. If omitted all content is buffered.")] + public int ItemCount { get; init; } + + [CommandOption("-p|--publication-period ")] + [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] + public PublicationPeriod PublicationPeriod { get; init; } + + [CommandOption("-f|--from-date ")] + [Description("Include content items published on, or after this date. Use YYYY/MM/DD Format. If omitted DateTime.MinValue is used.")] + public DateTime FromDate { get; init; } + + [CommandOption("-t|--to-date ")] + [Description("Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.")] + public DateTime ToDate { get; init; } + +#nullable enable annotations + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommandFactory.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommandFactory.cs deleted file mode 100644 index 5c59146..0000000 --- a/Solutions/Stacker.Cli/Commands/TwitterBufferCommandFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using Stacker.Cli.Contracts.Commands; -using Stacker.Cli.Contracts.Tasks; -using Stacker.Cli.Domain.Publication; -using Stacker.Cli.Formatters; - -namespace Stacker.Cli.Commands; - -public class TwitterBufferCommandFactory : ICommandFactory -{ - private readonly IContentTasks contentTasks; - - public TwitterBufferCommandFactory(IContentTasks contentTasks) - { - this.contentTasks = contentTasks; - } - - public Command Create() - { - var cmd = new Command("buffer", "Uploads content to Buffer to be published via Twitter") - { - Handler = CommandHandler.Create(async (string contentFilePath, string profileName, int itemCount, DateTime fromDate, DateTime toDate, PublicationPeriod publicationPeriod) => - { - await this.contentTasks.BufferContentItemsAsync(contentFilePath, $"twitter|", profileName, publicationPeriod, fromDate, toDate, itemCount).ConfigureAwait(false); - }), - }; - - cmd.Add(new Argument("content-file-path") { Description = "Content file path." }); - cmd.Add(new Argument("profile-name") { Description = "Twitter profile to Buffer." }); - - cmd.AddOption(new("--item-count", "Number of content items to buffer. If omitted all content is buffered.") { Argument = new Argument() }); - cmd.AddOption(new("--publication-period", "Publication period to filter content items by. If specified --from-date and --to-date are ignored.") { Argument = new Argument() }); - cmd.AddOption(new("--from-date", "Include content items published on, or after this date. Use YYYY/MM/DD Format. If omitted DateTime.MinValue is used.") { Argument = new Argument() }); - cmd.AddOption(new("--to-date", "Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.") { Argument = new Argument() }); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/TwitterCommandFactory.cs b/Solutions/Stacker.Cli/Commands/TwitterCommandFactory.cs deleted file mode 100644 index 1a98160..0000000 --- a/Solutions/Stacker.Cli/Commands/TwitterCommandFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; -using Stacker.Cli.Contracts.Commands; - -namespace Stacker.Cli.Commands; - -public class TwitterCommandFactory : ICommandFactory -{ - private readonly ICommandFactory twitterBufferCommandFactory; - - public TwitterCommandFactory(ICommandFactory twitterBufferCommandFactory) - { - this.twitterBufferCommandFactory = twitterBufferCommandFactory; - } - - public Command Create() - { - var cmd = new Command("twitter", "Twitter functionality."); - - cmd.AddCommand(this.twitterBufferCommandFactory.Create()); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/WordPressCommandFactory.cs b/Solutions/Stacker.Cli/Commands/WordPressCommandFactory.cs deleted file mode 100644 index bb64105..0000000 --- a/Solutions/Stacker.Cli/Commands/WordPressCommandFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; -using Stacker.Cli.Contracts.Commands; - -namespace Stacker.Cli.Commands; - -public class WordPressCommandFactory : ICommandFactory -{ - private readonly ICommandFactory wordpressExportCommandFactory; - - public WordPressCommandFactory(ICommandFactory wordpressExportCommandFactory) - { - this.wordpressExportCommandFactory = wordpressExportCommandFactory; - } - - public Command Create() - { - var cmd = new Command("wordpress", "WordPress functionality."); - - cmd.AddCommand(this.wordpressExportCommandFactory.Create()); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportCommandFactory.cs b/Solutions/Stacker.Cli/Commands/WordPressExportCommandFactory.cs deleted file mode 100644 index df0f29b..0000000 --- a/Solutions/Stacker.Cli/Commands/WordPressExportCommandFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; -using Stacker.Cli.Contracts.Commands; - -namespace Stacker.Cli.Commands; - -public class WordPressExportCommandFactory : ICommandFactory -{ - private readonly ICommandFactory universalExportCommandFactory; - private readonly ICommandFactory markdownExportCommandFactory; - - public WordPressExportCommandFactory( - ICommandFactory universalExportCommandFactory, - ICommandFactory markdownExportCommandFactory) - { - this.universalExportCommandFactory = universalExportCommandFactory; - this.markdownExportCommandFactory = markdownExportCommandFactory; - } - - public Command Create() - { - var cmd = new Command("export", "Perform operations on WordPress export files."); - cmd.AddCommand(this.universalExportCommandFactory.Create()); - cmd.AddCommand(this.markdownExportCommandFactory.Create()); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportMarkDownCommandFactory.cs b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs similarity index 57% rename from Solutions/Stacker.Cli/Commands/WordPressExportMarkDownCommandFactory.cs rename to Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs index 5d3bda6..d02d331 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportMarkDownCommandFactory.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs @@ -1,12 +1,12 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // using System; using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Invocation; +using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; @@ -14,141 +14,142 @@ using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; + +using Spectre.Console; +using Spectre.Console.Cli; +using Spectre.IO; + using Stacker.Cli.Cleaners; using Stacker.Cli.Configuration; -using Stacker.Cli.Contracts.Commands; using Stacker.Cli.Contracts.Configuration; using Stacker.Cli.Domain.Universal; using Stacker.Cli.Domain.WordPress; using Stacker.Cli.Serialization; using Stacker.Cli.Tasks; + using YamlDotNet.Serialization; +using Environment = System.Environment; +using Path = System.IO.Path; + namespace Stacker.Cli.Commands; -public class WordPressExportMarkDownCommandFactory : ICommandFactory +public class WordPressExportMarkdownCommand : AsyncCommand { private readonly IDownloadTasks downloadTasks; private readonly IStackerSettingsManager settingsManager; private readonly ContentItemCleaner cleanerManager; private readonly IYamlSerializerFactory serializerFactory; private ISerializer serializer; - private StackerSettings settings; + private StackerSettings stackerSettings; - public WordPressExportMarkDownCommandFactory(IStackerSettingsManager settingsManager, IDownloadTasks downloadTasks, ContentItemCleaner cleanerManager, IYamlSerializerFactory serializerFactory) + public WordPressExportMarkdownCommand(IDownloadTasks downloadTasks, IStackerSettingsManager settingsManager, ContentItemCleaner cleanerManager, IYamlSerializerFactory serializerFactory) { - this.settingsManager = settingsManager; this.downloadTasks = downloadTasks; + this.settingsManager = settingsManager; this.cleanerManager = cleanerManager; this.serializerFactory = serializerFactory; } - public Command Create() + /// + public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] Settings settings) { - var cmd = new Command("markdown", "Convert WordPress export files into a markdown format.") - { - Handler = CommandHandler.Create(async (string wpexportFilePath, string exportFilePath) => - { - this.settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); - - if (!File.Exists(wpexportFilePath)) - { - Console.WriteLine($"File not found {wpexportFilePath}"); + this.stackerSettings = this.settingsManager.LoadSettings(nameof(StackerSettings)); - return; - } + if (!File.Exists(settings.WordPressExportFilePath.FullPath)) + { + AnsiConsole.WriteLine($"File not found {settings.WordPressExportFilePath.FullPath}"); - this.serializer = this.serializerFactory.GetSerializer(); + return 1; + } - BlogSite blogSite = await this.LoadWordPressExportAsync(wpexportFilePath).ConfigureAwait(false); + this.serializer = this.serializerFactory.GetSerializer(); - List feed = this.LoadFeed(blogSite); + BlogSite blogSite = await this.LoadWordPressExportAsync(settings.WordPressExportFilePath.FullPath).ConfigureAwait(false); - var sb = new StringBuilder(); - FileInfo fi = new(exportFilePath); - DirectoryInfo tempHtmlFolder = new(Path.Join(Path.GetTempPath(), "stacker", "html")); - DirectoryInfo tempMarkdownFolder = new(Path.Join(Path.GetTempPath(), "stacker", "md")); - string inputTempHtmlFilePath; - string outputTempMarkdownFilePath; - string outputFilePath; + List feed = this.LoadFeed(blogSite); - if (!fi.Directory.Exists) - { - fi.Directory.Create(); - } + var sb = new StringBuilder(); + FileInfo fi = new(settings.OutputDirectoryPath.FullPath); + DirectoryInfo tempHtmlFolder = new(Path.Join(Path.GetTempPath(), "stacker", "html")); + DirectoryInfo tempMarkdownFolder = new(Path.Join(Path.GetTempPath(), "stacker", "md")); - if (!tempHtmlFolder.Exists) - { - tempHtmlFolder.Create(); - } + string inputTempHtmlFilePath; + string outputTempMarkdownFilePath; + string outputFilePath; - if (!tempMarkdownFolder.Exists) - { - tempMarkdownFolder.Create(); - } + if (!fi.Directory.Exists) + { + fi.Directory.Create(); + } - // await this.downloadTasks.DownloadAsync(feed, exportFilePath).ConfigureAwait(false); - foreach (ContentItem ci in feed) - { - ContentItem contentItem = this.cleanerManager.PostDownload(ci); + if (!tempHtmlFolder.Exists) + { + tempHtmlFolder.Create(); + } - sb.AppendLine("---"); - sb.Append(this.CreateYamlHeader(contentItem)); - sb.Append("---"); - sb.Append(Environment.NewLine); - sb.Append(Environment.NewLine); + if (!tempMarkdownFolder.Exists) + { + tempMarkdownFolder.Create(); + } - await using (StreamWriter writer = File.CreateText(Path.Combine(tempHtmlFolder.FullName, contentItem.UniqueId + ".html"))) - { - await writer.WriteAsync(contentItem.Content.Body).ConfigureAwait(false); - } + // await this.downloadTasks.DownloadAsync(feed, exportFilePath).ConfigureAwait(false); + foreach (ContentItem ci in feed) + { + ContentItem contentItem = this.cleanerManager.PostDownload(ci); - inputTempHtmlFilePath = Path.Combine(tempHtmlFolder.FullName, contentItem.UniqueId + ".html"); - outputTempMarkdownFilePath = Path.Combine(tempMarkdownFolder.FullName, contentItem.UniqueId + ".md"); - outputFilePath = Path.Combine(exportFilePath, contentItem.Author.Username.ToLowerInvariant(), contentItem.UniqueId + ".md"); + sb.AppendLine("---"); + sb.Append(this.CreateYamlHeader(contentItem)); + sb.Append("---"); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); - FileInfo outputFile = new(outputFilePath); + await using (StreamWriter writer = File.CreateText(Path.Combine(tempHtmlFolder.FullName, contentItem.UniqueId + ".html"))) + { + await writer.WriteAsync(contentItem.Content.Body).ConfigureAwait(false); + } - if (!outputFile.Directory.Exists) - { - outputFile.Directory.Create(); - } + inputTempHtmlFilePath = Path.Combine(tempHtmlFolder.FullName, contentItem.UniqueId + ".html"); + outputTempMarkdownFilePath = Path.Combine(tempMarkdownFolder.FullName, contentItem.UniqueId + ".md"); + outputFilePath = Path.Combine(settings.OutputDirectoryPath.FullPath, contentItem.Author.Username.ToLowerInvariant(), contentItem.UniqueId + ".md"); - if (this.ExecutePandoc(inputTempHtmlFilePath, outputTempMarkdownFilePath)) - { - sb.Append(await File.ReadAllTextAsync(outputTempMarkdownFilePath).ConfigureAwait(false)); + FileInfo outputFile = new(outputFilePath); - string content = sb.ToString(); + if (!outputFile.Directory.Exists) + { + outputFile.Directory.Create(); + } - Console.WriteLine(outputFilePath); + if (this.ExecutePandoc(inputTempHtmlFilePath, outputTempMarkdownFilePath)) + { + sb.Append(await File.ReadAllTextAsync(outputTempMarkdownFilePath).ConfigureAwait(false)); - try - { - content = this.cleanerManager.PostConvert(content); - } - catch (Exception exception) - { - Console.WriteLine(exception.Message); - } + string content = sb.ToString(); - await using (StreamWriter writer = File.CreateText(outputFilePath)) - { - await writer.WriteAsync(content).ConfigureAwait(false); - } - } + AnsiConsole.WriteLine(outputFilePath); - // Remote the temporary html file. - File.Delete(inputTempHtmlFilePath); + try + { + content = this.cleanerManager.PostConvert(content); + } + catch (Exception exception) + { + AnsiConsole.WriteLine(exception.Message); + } - sb.Clear(); + await using (StreamWriter writer = File.CreateText(outputFilePath)) + { + await writer.WriteAsync(content).ConfigureAwait(false); } - }), - }; + } - cmd.AddArgument(new Argument("wp-export-file-path") { Description = "WordPress Export file path." }); - cmd.AddArgument(new Argument("export-file-path") { Description = "File path for the exported files." }); + // Remote the temporary html file. + File.Delete(inputTempHtmlFilePath); - return cmd; + sb.Clear(); + } + + return 0; } private string CreateYamlHeader(ContentItem contentItem) @@ -177,13 +178,13 @@ private string CreateYamlHeader(ContentItem contentItem) private List LoadFeed(BlogSite blogSite) { - Console.WriteLine($"Processing..."); + AnsiConsole.WriteLine($"Processing..."); var feed = new List(); StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); var posts = blogSite.GetAllPostsInAllPublicationStates().ToList(); - Console.WriteLine($"Total Posts: {posts.Count}"); + AnsiConsole.WriteLine($"Total Posts: {posts.Count}"); // var attachments = posts.Where(x => x.Attachments.Any()); foreach (Post post in posts) @@ -248,7 +249,7 @@ private async Task LoadWordPressExportAsync(string wpexportFilePath) { BlogSite blogSite; - Console.WriteLine($"Reading {wpexportFilePath}"); + AnsiConsole.WriteLine($"Reading {wpexportFilePath}"); using (StreamReader reader = File.OpenText(wpexportFilePath)) { @@ -280,8 +281,8 @@ private bool ExecutePandoc(string inputTempHtmlFilePath, string outputTempMarkdo if (process.ExitCode != 0) { - Console.WriteLine("Failed to convert " + outputTempMarkdownFilePath); - Console.WriteLine(process.StandardError.ReadToEnd()); + AnsiConsole.WriteLine("Failed to convert " + outputTempMarkdownFilePath); + AnsiConsole.WriteLine(process.StandardError.ReadToEnd()); } else { @@ -293,7 +294,7 @@ private bool ExecutePandoc(string inputTempHtmlFilePath, string outputTempMarkdo private bool IsCategoryExcluded(string category) { - return this.settings.WordPressToMarkdown.TagsToRemove.Contains(category); + return this.stackerSettings.WordPressToMarkdown.TagsToRemove.Contains(category); } private string GetHeaderImage(List attachments) @@ -315,6 +316,20 @@ private string GetHeaderImage(List attachments) private bool IsRelevantHost(string url) { - return this.settings.WordPressToMarkdown.Hosts.Any(x => url.Contains(x, StringComparison.InvariantCultureIgnoreCase)); + return this.stackerSettings.WordPressToMarkdown.Hosts.Any(x => url.Contains(x, StringComparison.InvariantCultureIgnoreCase)); + } + + /// + /// The settings for the command. + /// + public class Settings : CommandSettings + { + [CommandOption("-w|--wp-export-file-path ")] + [Description("WordPress Export file path.")] + public FilePath WordPressExportFilePath { get; init; } + + [CommandOption("-o|--output-directory-path ")] + [Description("Directory path for the exported files.")] + public DirectoryPath OutputDirectoryPath { get; init; } } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs new file mode 100644 index 0000000..d1f2a2e --- /dev/null +++ b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +using Newtonsoft.Json; + +using Spectre.Console; +using Spectre.Console.Cli; +using Spectre.IO; + +using Stacker.Cli.Configuration; +using Stacker.Cli.Contracts.Configuration; +using Stacker.Cli.Converters; +using Stacker.Cli.Domain.Universal; +using Stacker.Cli.Domain.WordPress; + +namespace Stacker.Cli.Commands; + +public class WordPressExportUniversalCommand : AsyncCommand +{ + private readonly IStackerSettingsManager settingsManager; + + public WordPressExportUniversalCommand(IStackerSettingsManager settingsManager) + { + this.settingsManager = settingsManager; + } + + /// + public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] Settings settings) + { + if (!File.Exists(settings.WordPressExportFilePath.FullPath)) + { + AnsiConsole.WriteLine($"File not found {settings.WordPressExportFilePath.FullPath}"); + + return 1; + } + + BlogSite blogSite; + + AnsiConsole.WriteLine($"Reading {settings.WordPressExportFilePath.FullPath}"); + + using (StreamReader reader = File.OpenText(settings.WordPressExportFilePath.FullPath)) + { + XDocument document = await XDocument.LoadAsync(reader, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); + blogSite = new BlogSite(document); + } + + AnsiConsole.WriteLine($"Processing..."); + + StackerSettings stackerSettings = this.settingsManager.LoadSettings(nameof(StackerSettings)); + List posts = blogSite.GetAllPosts().ToList(); + List validPosts = posts.FilterByValid(stackerSettings).ToList(); + List promotablePosts = validPosts.FilterByPromotable().ToList(); + WordPressTagToHashTagConverter hashTagConverter = new(); + List feed = new(); + + AnsiConsole.WriteLine($"Total Posts: {posts.Count()}"); + AnsiConsole.WriteLine($"Valid Posts: {validPosts.Count()}"); + AnsiConsole.WriteLine($"Promotable Posts: {promotablePosts.Count()}"); + + foreach (Post post in promotablePosts) + { + User user = stackerSettings.Users.Find(u => string.Equals(u.Email, post.Author.Email, StringComparison.InvariantCultureIgnoreCase)); + + feed.Add(new ContentItem + { + Author = new AuthorDetails + { + DisplayName = post.Author.DisplayName, + Email = post.Author.Email, + TwitterHandle = user.Twitter, + }, + Content = new ContentDetails + { + Body = post.Body, + Excerpt = post.Excerpt, + Link = post.Link, + Title = post.Title, + }, + PublishedOn = post.PublishedAtUtc, + Promote = post.Promote, + PromoteUntil = post.PromoteUntil, + Status = post.Status, + Slug = post.Slug, + Tags = post.Tags.Where(t => t != null).Select(t => t.Name), + }); + } + + await using (StreamWriter writer = File.CreateText(settings.UniversalFilePath.FullPath)) + { + await writer.WriteAsync(JsonConvert.SerializeObject(feed, Formatting.Indented)).ConfigureAwait(false); + } + + AnsiConsole.WriteLine($"Content written to {settings.UniversalFilePath.FullPath}"); + + return 0; + } + + /// + /// The settings for the command. + /// + public class Settings : CommandSettings + { + [CommandOption("-w|--wp-export-file-path ")] + [Description("WordPress Export file path.")] + public FilePath WordPressExportFilePath { get; init; } + + [CommandOption("-f|--universal-file-path ")] + [Description("Universal file path.")] + public FilePath UniversalFilePath { get; init; } + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommandFactory.cs b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommandFactory.cs deleted file mode 100644 index ba9db3b..0000000 --- a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommandFactory.cs +++ /dev/null @@ -1,110 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.IO; -using System.Linq; -using System.Threading; -using System.Xml.Linq; -using Newtonsoft.Json; -using Stacker.Cli.Configuration; -using Stacker.Cli.Contracts.Commands; -using Stacker.Cli.Contracts.Configuration; -using Stacker.Cli.Converters; -using Stacker.Cli.Domain.Universal; -using Stacker.Cli.Domain.WordPress; - -namespace Stacker.Cli.Commands; - -public class WordPressExportUniversalCommandFactory : ICommandFactory -{ - private readonly IStackerSettingsManager settingsManager; - - public WordPressExportUniversalCommandFactory(IStackerSettingsManager settingsManager) - { - this.settingsManager = settingsManager; - } - - public Command Create() - { - var cmd = new Command("universal", "Convert WordPress export files into a universal format.") - { - Handler = CommandHandler.Create(async (string wpexportFilePath, string universalFilePath) => - { - if (!File.Exists(wpexportFilePath)) - { - Console.WriteLine($"File not found {wpexportFilePath}"); - - return; - } - - BlogSite blogSite; - - Console.WriteLine($"Reading {wpexportFilePath}"); - - using (StreamReader reader = File.OpenText(wpexportFilePath)) - { - XDocument document = await XDocument.LoadAsync(reader, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); - blogSite = new(document); - } - - Console.WriteLine($"Processing..."); - - StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); - var posts = blogSite.GetAllPosts().ToList(); - var validPosts = posts.FilterByValid(settings).ToList(); - var promotablePosts = validPosts.FilterByPromotable().ToList(); - var hashTagConverter = new WordPressTagToHashTagConverter(); - var feed = new List(); - - Console.WriteLine($"Total Posts: {posts.Count()}"); - Console.WriteLine($"Valid Posts: {validPosts.Count()}"); - Console.WriteLine($"Promotable Posts: {promotablePosts.Count()}"); - - foreach (Post post in promotablePosts) - { - User user = settings.Users.Find(u => string.Equals(u.Email, post.Author.Email, StringComparison.InvariantCultureIgnoreCase)); - - feed.Add(new() - { - Author = new() - { - DisplayName = post.Author.DisplayName, - Email = post.Author.Email, - TwitterHandle = user.Twitter, - }, - Content = new() - { - Body = post.Body, - Excerpt = post.Excerpt, - Link = post.Link, - Title = post.Title, - }, - PublishedOn = post.PublishedAtUtc, - Promote = post.Promote, - PromoteUntil = post.PromoteUntil, - Status = post.Status, - Slug = post.Slug, - Tags = post.Tags.Where(t => t != null).Select(t => t.Name), - }); - } - - await using (StreamWriter writer = File.CreateText(universalFilePath)) - { - await writer.WriteAsync(JsonConvert.SerializeObject(feed, Formatting.Indented)).ConfigureAwait(false); - } - - Console.WriteLine($"Content written to {universalFilePath}"); - }), - }; - - cmd.AddArgument(new Argument("wp-export-file-path") { Description = "WordPress Export file path." }); - cmd.AddArgument(new Argument("universal-file-path") { Description = "Universal file path." }); - - return cmd; - } -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Configuration/FileSystemLocalProfileAppEnvironment.cs b/Solutions/Stacker.Cli/Configuration/FileSystemLocalProfileAppEnvironment.cs index 8d51ebf..b02e779 100644 --- a/Solutions/Stacker.Cli/Configuration/FileSystemLocalProfileAppEnvironment.cs +++ b/Solutions/Stacker.Cli/Configuration/FileSystemLocalProfileAppEnvironment.cs @@ -4,6 +4,7 @@ using System; using System.IO; + using Stacker.Cli.Contracts.Configuration; namespace Stacker.Cli.Configuration; diff --git a/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs b/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs index 3a19f87..3f1d37d 100644 --- a/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs +++ b/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs @@ -3,7 +3,9 @@ // using System.IO; + using Newtonsoft.Json; + using Stacker.Cli.Contracts.Configuration; namespace Stacker.Cli.Configuration; diff --git a/Solutions/Stacker.Cli/Contracts/Commands/ICommandFactory.cs b/Solutions/Stacker.Cli/Contracts/Commands/ICommandFactory.cs deleted file mode 100644 index a60dff5..0000000 --- a/Solutions/Stacker.Cli/Contracts/Commands/ICommandFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using System.CommandLine; - -namespace Stacker.Cli.Contracts.Commands; - -public interface ICommandFactory -{ - Command Create(); -} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs b/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs index e52cb0a..2e93cb9 100644 --- a/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs +++ b/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs @@ -3,6 +3,7 @@ // using System.Collections.Generic; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Contracts.Formatters; diff --git a/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs b/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs index 5273cdd..1c3b8af 100644 --- a/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs +++ b/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs @@ -5,6 +5,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; + +using Spectre.IO; + using Stacker.Cli.Contracts.Formatters; using Stacker.Cli.Domain.Publication; using Stacker.Cli.Domain.Universal; @@ -13,8 +16,15 @@ namespace Stacker.Cli.Contracts.Tasks; public interface IContentTasks { - Task BufferContentItemsAsync(string contentFilePath, string profilePrefix, string profileName, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) + Task BufferContentItemsAsync( + FilePath contentFilePath, + string profilePrefix, + string profileName, + PublicationPeriod publicationPeriod, + DateTime fromDate, + DateTime toDate, + int itemCount) where TContentFormatter : class, IContentFormatter, new(); - Task> LoadContentItemsAsync(string contentFilePath, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount); + Task> LoadContentItemsAsync(FilePath contentFilePath, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount); } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Converters/PublicationPeriodConverter.cs b/Solutions/Stacker.Cli/Converters/PublicationPeriodConverter.cs index 975cba6..a5f9099 100644 --- a/Solutions/Stacker.Cli/Converters/PublicationPeriodConverter.cs +++ b/Solutions/Stacker.Cli/Converters/PublicationPeriodConverter.cs @@ -3,8 +3,10 @@ // using System; + using NodaTime; using NodaTime.Calendars; + using Stacker.Cli.Domain.Publication; namespace Stacker.Cli.Converters; @@ -21,24 +23,24 @@ public DateInterval Convert(PublicationPeriod publicationPeriod) { case PublicationPeriod.ThisWeek: LocalDate startOfThisWeek = LocalDate.FromWeekYearWeekAndDay(today.Year, weekNumber, IsoDayOfWeek.Monday); - return new(startOfThisWeek, LocalDate.FromDateTime(DateTime.Today)); + return new DateInterval(startOfThisWeek, LocalDate.FromDateTime(DateTime.Today)); case PublicationPeriod.LastWeek: LocalDate startOfLastWeek = LocalDate.FromWeekYearWeekAndDay(today.Year, rule.GetWeekOfWeekYear(today.PlusWeeks(-1)), IsoDayOfWeek.Monday); - return new(startOfLastWeek, startOfLastWeek.PlusWeeks(1).PlusDays(-1)); + return new DateInterval(startOfLastWeek, startOfLastWeek.PlusWeeks(1).PlusDays(-1)); case PublicationPeriod.ThisMonth: - var startOfThisMonth = LocalDate.FromDateTime(new(today.Year, today.Month, 1)); - return new(startOfThisMonth, today); + var startOfThisMonth = LocalDate.FromDateTime(new DateTime(today.Year, today.Month, 1)); + return new DateInterval(startOfThisMonth, today); case PublicationPeriod.LastMonth: - LocalDate startOfLastMonth = LocalDate.FromDateTime(new(today.Year, today.Month, 1)).PlusMonths(-1); + LocalDate startOfLastMonth = LocalDate.FromDateTime(new DateTime(today.Year, today.Month, 1)).PlusMonths(-1); var endOfLastMonth = new LocalDate(startOfLastMonth.Year, startOfLastMonth.Month, startOfLastMonth.Calendar.GetDaysInMonth(startOfLastMonth.Year, startOfLastMonth.Month)); - return new(startOfLastMonth, endOfLastMonth); + return new DateInterval(startOfLastMonth, endOfLastMonth); case PublicationPeriod.ThisYear: - var startOfThisYear = LocalDate.FromDateTime(new(today.Year, 1, 1)); - return new(startOfThisYear, today); + var startOfThisYear = LocalDate.FromDateTime(new DateTime(today.Year, 1, 1)); + return new DateInterval(startOfThisYear, today); case PublicationPeriod.LastYear: - LocalDate startOfLastYear = LocalDate.FromDateTime(new(today.Year, 1, 1)).PlusYears(-1); - LocalDate endOfLastYear = LocalDate.FromDateTime(new(today.Year, 12, 31)).PlusYears(-1); - return new(startOfLastYear, endOfLastYear); + LocalDate startOfLastYear = LocalDate.FromDateTime(new DateTime(today.Year, 1, 1)).PlusYears(-1); + LocalDate endOfLastYear = LocalDate.FromDateTime(new DateTime(today.Year, 12, 31)).PlusYears(-1); + return new DateInterval(startOfLastYear, endOfLastYear); default: throw new ArgumentOutOfRangeException(nameof(publicationPeriod), publicationPeriod, null); } diff --git a/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs b/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs index 8c48379..4d7eb4a 100644 --- a/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs +++ b/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs @@ -9,6 +9,7 @@ using System.Web; using System.Xml; using System.Xml.Linq; + using Newtonsoft.Json; namespace Stacker.Cli.Domain.WordPress; diff --git a/Solutions/Stacker.Cli/Domain/WordPress/PostExtensions.cs b/Solutions/Stacker.Cli/Domain/WordPress/PostExtensions.cs index 427d665..e00f6ec 100644 --- a/Solutions/Stacker.Cli/Domain/WordPress/PostExtensions.cs +++ b/Solutions/Stacker.Cli/Domain/WordPress/PostExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; + using Stacker.Cli.Configuration; namespace Stacker.Cli.Domain.WordPress; diff --git a/Solutions/Stacker.Cli/Extensions/ServiceCollectionExtensions.cs b/Solutions/Stacker.Cli/Extensions/ServiceCollectionExtensions.cs index c5006df..28cb578 100644 --- a/Solutions/Stacker.Cli/Extensions/ServiceCollectionExtensions.cs +++ b/Solutions/Stacker.Cli/Extensions/ServiceCollectionExtensions.cs @@ -3,11 +3,10 @@ // using Microsoft.Extensions.DependencyInjection; + using Stacker.Cli.Cleaners; -using Stacker.Cli.Commands; using Stacker.Cli.Configuration; using Stacker.Cli.Contracts.Buffer; -using Stacker.Cli.Contracts.Commands; using Stacker.Cli.Contracts.Configuration; using Stacker.Cli.Contracts.Tasks; using Stacker.Cli.Serialization; @@ -22,23 +21,6 @@ public static void ConfigureDependencies(this ServiceCollection serviceCollectio serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddTransient, WordPressCommandFactory>(); - serviceCollection.AddTransient, WordPressExportCommandFactory>(); - serviceCollection.AddTransient, WordPressExportUniversalCommandFactory>(); - serviceCollection.AddTransient, WordPressExportMarkDownCommandFactory>(); - - serviceCollection.AddTransient, EnvironmentCommandFactory>(); - serviceCollection.AddTransient, EnvironmentInitCommandFactory>(); - - serviceCollection.AddTransient, FacebookCommandFactory>(); - serviceCollection.AddTransient, FacebookBufferCommandFactory>(); - - serviceCollection.AddTransient, LinkedInCommandFactory>(); - serviceCollection.AddTransient, LinkedInBufferCommandFactory>(); - - serviceCollection.AddTransient, TwitterCommandFactory>(); - serviceCollection.AddTransient, TwitterBufferCommandFactory>(); - serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); diff --git a/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs b/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs index bbba932..6b3f76c 100644 --- a/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; + using Stacker.Cli.Contracts.Formatters; using Stacker.Cli.Converters; using Stacker.Cli.Domain.Universal; diff --git a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs index e141152..a364923 100644 --- a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; + using Stacker.Cli.Contracts.Formatters; using Stacker.Cli.Converters; using Stacker.Cli.Domain.Universal; diff --git a/Solutions/Stacker.Cli/Infrastructure/Injection/TypeRegistrar.cs b/Solutions/Stacker.Cli/Infrastructure/Injection/TypeRegistrar.cs new file mode 100644 index 0000000..aa12689 --- /dev/null +++ b/Solutions/Stacker.Cli/Infrastructure/Injection/TypeRegistrar.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System; + +using Microsoft.Extensions.DependencyInjection; + +using Spectre.Console.Cli; + +namespace Stacker.Cli.Infrastructure.Injection; + +/// +/// Creates a type resolver from a service collection. +/// +public sealed class TypeRegistrar : ITypeRegistrar +{ + private readonly IServiceCollection builder; + + /// + /// Creates a new instance of . + /// + /// ServiceCollection. + public TypeRegistrar(IServiceCollection builder) + { + this.builder = builder; + } + + /// + /// Builds the type resolver. + /// + /// A new TypeResolver. + public ITypeResolver Build() + { + return new TypeResolver(this.builder.BuildServiceProvider()); + } + + /// + /// Registers a type. + /// + /// Service Contract. + /// Implementation of the type. + public void Register(Type service, Type implementation) + { + this.builder.AddSingleton(service, implementation); + } + + /// + /// Registers an instance. + /// + /// Service Contract. + /// Implementation of the type. + public void RegisterInstance(Type service, object implementation) + { + this.builder.AddSingleton(service, implementation); + } + + /// + /// Registers a lazy type. + /// + /// Service Contract. + /// Function for providing the concrete type. + /// Throws is hte func is null. + public void RegisterLazy(Type service, Func func) + { + if (func is null) + { + throw new ArgumentNullException(nameof(func)); + } + + this.builder.AddSingleton(service, (provider) => func()); + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Infrastructure/Injection/TypeResolver.cs b/Solutions/Stacker.Cli/Infrastructure/Injection/TypeResolver.cs new file mode 100644 index 0000000..6f7fa80 --- /dev/null +++ b/Solutions/Stacker.Cli/Infrastructure/Injection/TypeResolver.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System; + +using Spectre.Console.Cli; + +namespace Stacker.Cli.Infrastructure.Injection; + +#nullable enable annotations + +/// +/// Implementation of that uses the to resolve types. +/// +public sealed class TypeResolver : ITypeResolver, IDisposable +{ + private readonly IServiceProvider provider; + + /// + /// Creates a new instance of . + /// + /// ServiceProvider for resolving types. + /// Thrown if is null. + public TypeResolver(IServiceProvider provider) + { + this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + /// Resolves an instance of the type. + /// + /// Type to resolve. + /// Instance of the Type. + public object? Resolve(Type? type) + { + return type == null ? null : this.provider.GetService(type); + } + + /// + /// Dispose the resolver. + /// + public void Dispose() + { + if (this.provider is IDisposable disposable) + { + disposable.Dispose(); + } + } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Properties/launchSettings.json b/Solutions/Stacker.Cli/Properties/launchSettings.json index e086ba3..8ca3d83 100644 --- a/Solutions/Stacker.Cli/Properties/launchSettings.json +++ b/Solutions/Stacker.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Endjin.Stacker.Cli": { "commandName": "Project", - "commandLineArgs": "wordpress export markdown \"C:\\Users\\HowardvanRooijen\\AppData\\Roaming\\endjin\\stacker\\data\\endjinblog.WordPress.2020-06-23.xml\" \"C:\\Temp\\Blog\"" + "commandLineArgs": "wordpress export markdown" } } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Serialization/ForceQuotedStringValuesEventEmitter.cs b/Solutions/Stacker.Cli/Serialization/ForceQuotedStringValuesEventEmitter.cs index f4fb1dc..8aa0055 100644 --- a/Solutions/Stacker.Cli/Serialization/ForceQuotedStringValuesEventEmitter.cs +++ b/Solutions/Stacker.Cli/Serialization/ForceQuotedStringValuesEventEmitter.cs @@ -3,6 +3,7 @@ // using System.Collections.Generic; + using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.EventEmitters; diff --git a/Solutions/Stacker.Cli/Stacker.Cli.csproj b/Solutions/Stacker.Cli/Stacker.Cli.csproj index db8e014..5dc8b2e 100644 --- a/Solutions/Stacker.Cli/Stacker.Cli.csproj +++ b/Solutions/Stacker.Cli/Stacker.Cli.csproj @@ -45,11 +45,11 @@ - 1701;1702; CS1591;SA1600;SA1124 + 1701;1702; CS1591;SA1600;SA1124;IDE0007 - 1701;1702; CS1591;SA1600;SA1124 + 1701;1702; CS1591;SA1600;SA1124;IDE0007 @@ -59,7 +59,8 @@ - + + diff --git a/Solutions/Stacker.Cli/StackerCli.cs b/Solutions/Stacker.Cli/StackerCli.cs index cc09976..cef7021 100644 --- a/Solutions/Stacker.Cli/StackerCli.cs +++ b/Solutions/Stacker.Cli/StackerCli.cs @@ -2,14 +2,15 @@ // Copyright (c) Endjin Limited. All rights reserved. // -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + +using Spectre.Console.Cli; + using Stacker.Cli.Commands; -using Stacker.Cli.Contracts.Commands; using Stacker.Cli.Extensions; +using Stacker.Cli.Infrastructure.Injection; namespace Stacker.Cli; @@ -23,22 +24,67 @@ public static class StackerCli /// /// Command Line Switches. /// Exit Code. - public static async Task Main(string[] args) + public static Task Main(string[] args) { - var serviceCollection = new ServiceCollection(); - serviceCollection.ConfigureDependencies(); + ServiceCollection registrations = new(); + registrations.ConfigureDependencies(); + + TypeRegistrar registrar = new(registrations); + CommandApp app = new(registrar); + + app.Configure(config => + { + config.Settings.PropagateExceptions = false; + config.CaseSensitivity(CaseSensitivity.None); + config.SetApplicationName("stacker"); + + config.AddExample("wordpress", "export", "markdown", "-w", """C:\temp\wordpress-export.xml""", "-o", """C:\Temp\Blog"""); + + config.AddBranch("facebook", process => + { + process.SetDescription("Facebook functionality."); + process.AddCommand("buffer") + .WithDescription("Uploads content to Buffer to be published via Facebook"); + }); + + config.AddBranch("linkedin", process => + { + process.SetDescription("LinkedIn functionality."); + process.AddCommand("buffer") + .WithDescription("Uploads content to Buffer to be published via LinkedIn"); + }); + + config.AddBranch("twitter", process => + { + process.SetDescription("Twitter functionality."); + process.AddCommand("buffer") + .WithDescription("Uploads content to Buffer to be published via Twitter"); + }); + + config.AddBranch("environment", process => + { + process.SetDescription("Manipulate the stacker environment."); + process.AddCommand("init") + .WithDescription("Initializes the stacker environment."); + }); + + config.AddBranch("wordpress", process => + { + process.SetDescription("WordPress functionality."); + process.AddBranch("export", export => + { + export.SetDescription("Export functionality."); + export.AddCommand("markdown") + .WithDescription("Convert WordPress export files into a markdown format."); - ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + export.AddCommand("universal") + .WithDescription("Convert WordPress export files into a universal format."); + }); + }); - Parser cmd = new CommandLineBuilder() - .AddCommand(serviceProvider.GetRequiredService>().Create()) - .AddCommand(serviceProvider.GetRequiredService>().Create()) - .AddCommand(serviceProvider.GetRequiredService>().Create()) - .AddCommand(serviceProvider.GetRequiredService>().Create()) - .AddCommand(serviceProvider.GetRequiredService>().Create()) - .UseDefaults() - .Build(); + config.ValidateExamples(); + }); - return await cmd.InvokeAsync(args).ConfigureAwait(false); + return app.RunAsync(args); } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs index cad41b0..c43584d 100644 --- a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs @@ -7,8 +7,14 @@ using System.IO; using System.Linq; using System.Threading.Tasks; + using Newtonsoft.Json; + using NodaTime; + +using Spectre.Console; +using Spectre.IO; + using Stacker.Cli.Configuration; using Stacker.Cli.Contracts.Buffer; using Stacker.Cli.Contracts.Configuration; @@ -31,7 +37,7 @@ public ContentTasks(IBufferClient bufferClient, IStackerSettingsManager settings this.settingsManager = settingsManager; } - public async Task BufferContentItemsAsync(string contentFilePath, string profilePrefix, string profileName, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) + public async Task BufferContentItemsAsync(FilePath contentFilePath, string profilePrefix, string profileName, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) where TContentFormatter : class, IContentFormatter, new() { TContentFormatter formatter = new(); @@ -44,8 +50,8 @@ public async Task BufferContentItemsAsync(string contentFileP { string profileId = settings.BufferProfiles[profileKey]; - Console.WriteLine($"Buffer Profile: {profileKey} = {profileId}"); - Console.WriteLine($"Loading: {contentFilePath}"); + AnsiConsole.WriteLine($"Buffer Profile: {profileKey} = {profileId}"); + AnsiConsole.WriteLine($"Loading: {contentFilePath}"); IEnumerable contentItems = await this.LoadContentItemsAsync(contentFilePath, publicationPeriod, fromDate, toDate, itemCount).ConfigureAwait(false); IEnumerable formattedContentItems = formatter.Format("social", profileName, contentItems); @@ -54,13 +60,13 @@ public async Task BufferContentItemsAsync(string contentFileP } else { - Console.WriteLine($"Settings for {profileKey} not found. Please check your Stacker configuration."); + AnsiConsole.WriteLine($"Settings for {profileKey} not found. Please check your Stacker configuration."); } } - public async Task> LoadContentItemsAsync(string contentFilePath, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) + public async Task> LoadContentItemsAsync(FilePath contentFilePath, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) { - IEnumerable content = JsonConvert.DeserializeObject>(await File.ReadAllTextAsync(contentFilePath).ConfigureAwait(false)); + IEnumerable content = JsonConvert.DeserializeObject>(await File.ReadAllTextAsync(contentFilePath.FullPath).ConfigureAwait(false)); if (publicationPeriod != PublicationPeriod.None) { @@ -107,8 +113,8 @@ public async Task> LoadContentItemsAsync(string content itemCount = contentItems.Count; } - Console.WriteLine($"Total Posts: {contentItems.Count}"); - Console.WriteLine($"Promoting first: {itemCount}"); + AnsiConsole.WriteLine($"Total Posts: {contentItems.Count}"); + AnsiConsole.WriteLine($"Promoting first: {itemCount}"); return contentItems.Take(itemCount); } diff --git a/Solutions/Stacker.Cli/Tasks/DownloadTasks.cs b/Solutions/Stacker.Cli/Tasks/DownloadTasks.cs index 2c88345..6906f91 100644 --- a/Solutions/Stacker.Cli/Tasks/DownloadTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/DownloadTasks.cs @@ -9,9 +9,13 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; + using Corvus.Retry; using Corvus.Retry.Policies; using Corvus.Retry.Strategies; + +using Spectre.Console; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Tasks; @@ -27,7 +31,7 @@ public DownloadTasks(IHttpClientFactory httpClientFactory) public async Task DownloadAsync(List feed, string outputPath) { - var downloadFeedBlock = new ActionBlock(context => this.DownloadFeedAsync(context), new() { MaxDegreeOfParallelism = Environment.ProcessorCount }); + var downloadFeedBlock = new ActionBlock(context => this.DownloadFeedAsync(context), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }); foreach (ContentItem contentItem in feed) { @@ -43,7 +47,7 @@ public async Task DownloadAsync(List feed, string outputPath) await downloadFeedBlock.Completion.ConfigureAwait(false); - Console.WriteLine("File Download Completed"); + AnsiConsole.WriteLine("File Download Completed"); } private async Task DownloadFeedAsync(DataflowContext context) @@ -57,7 +61,7 @@ await Retriable.RetryAsync( { context.AlreadyDownloaded = true; - Console.WriteLine("Already Downloaded: " + context.Destination); + AnsiConsole.WriteLine("Already Downloaded: " + context.Destination); return; } @@ -85,7 +89,7 @@ await Retriable.RetryAsync( } } - Console.WriteLine("Downloaded: " + context.Destination); + AnsiConsole.WriteLine("Downloaded: " + context.Destination); response.EnsureSuccessStatusCode(); } @@ -100,7 +104,7 @@ await Retriable.RetryAsync( context.IsFaulted = true; context.FaultError = ex.Message; - Console.WriteLine("Error Downloading: " + context.Destination); + AnsiConsole.WriteLine("Error Downloading: " + context.Destination); } return context; diff --git a/Solutions/Stacker.Cli/Tasks/IDownloadTasks.cs b/Solutions/Stacker.Cli/Tasks/IDownloadTasks.cs index 2a83dd4..a933ede 100644 --- a/Solutions/Stacker.Cli/Tasks/IDownloadTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/IDownloadTasks.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; + using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Tasks; diff --git a/Solutions/Stacker.Cli/packages.lock.json b/Solutions/Stacker.Cli/packages.lock.json index ceda7cf..8fd182e 100644 --- a/Solutions/Stacker.Cli/packages.lock.json +++ b/Solutions/Stacker.Cli/packages.lock.json @@ -72,6 +72,21 @@ "resolved": "0.47.0", "contentHash": "B2t1ha50pgEn1er2j5cv9RnJRX0bGhJDubkWr+KOcX5KjwVD83DvLXBVfixkiawuPFvh/Ggm1Wu07Qk96BbazA==" }, + "Spectre.Console.Cli": { + "type": "Direct", + "requested": "[0.47.0, )", + "resolved": "0.47.0", + "contentHash": "S2cZCbve4fAgRtigNUNZbF+NLQJcAapSqSbbDYqLtqXJcIZ6tKiRTveYe05d+oLY2bAmP7sgnLdzVknGXruy2Q==", + "dependencies": { + "Spectre.Console": "0.47.0" + } + }, + "Spectre.IO": { + "type": "Direct", + "requested": "[0.13.0, )", + "resolved": "0.13.0", + "contentHash": "K4NvyhogxTsqjFHYRmxgmoS3oXaRNRYEa+BaCvKws7a/OCtyRB16BUjXh92ZKa1piQrMc51HmiMdRnAx8nGD/g==" + }, "StyleCop.Analyzers": { "type": "Direct", "requested": "[1.2.0-beta.507, )", @@ -81,16 +96,6 @@ "StyleCop.Analyzers.Unstable": "1.2.0.507" } }, - "System.CommandLine.Experimental": { - "type": "Direct", - "requested": "[0.3.0-alpha.19577.1, )", - "resolved": "0.3.0-alpha.19577.1", - "contentHash": "d6t9G4NGBq7rB2Gvo5WjV4YuIJcplYj1fGeTef77TQJNXaExW1C4kDsyee9kHlHLejxEEtCrPvZ4pfj0IjfgHQ==", - "dependencies": { - "Microsoft.CSharp": "4.4.1", - "system.memory": "4.5.3" - } - }, "System.Threading.Tasks.Dataflow": { "type": "Direct", "requested": "[7.0.0, )", @@ -116,11 +121,6 @@ "resolved": "1.1.1", "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.4.1", - "contentHash": "A5hI3gk6WpcBI0QGZY6/d5CCaYUxJgi7iENn1uYEng+Olo8RfI5ReGVkjXjeu3VR3srLvVYREATXa2M0X7FYJA==" - }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "7.0.0", @@ -173,6 +173,14 @@ "Microsoft.SourceLink.Common": "1.1.1" } }, + "Spectre.Console": { + "type": "Transitive", + "resolved": "0.47.0", + "contentHash": "wz8mszcZr0cSOo8GyoG9e2DFW0SkMT8/n78Q/lIXX7EbCtHNXOoOKWpJ9Str+rCYtmQOGGyDutZzubrUHK/XkA==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.507", @@ -180,8 +188,8 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", From 2c95a4d08fb5886cb9b900211427dc69ca38cbb0 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Tue, 24 Oct 2023 18:50:47 +0100 Subject: [PATCH 2/9] Add examples --- Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs | 2 +- Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs | 2 +- Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs | 2 +- .../Commands/WordPressExportUniversalCommand.cs | 2 +- Solutions/Stacker.Cli/StackerCli.cs | 8 ++++++++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs index b02bf4f..170419d 100644 --- a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs @@ -60,7 +60,7 @@ public class Settings : CommandSettings public int ItemCount { get; init; } [CommandOption("-p|--publication-period ")] - [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] + [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] public PublicationPeriod PublicationPeriod { get; init; } [CommandOption("-f|--from-date ")] diff --git a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs index 98967f4..fb3ec65 100644 --- a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs @@ -61,7 +61,7 @@ public class Settings : CommandSettings public int ItemCount { get; init; } [CommandOption("-p|--publication-period ")] - [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] + [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] public PublicationPeriod PublicationPeriod { get; init; } [CommandOption("-f|--from-date ")] diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs index 09163f0..800c859 100644 --- a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs @@ -61,7 +61,7 @@ public class Settings : CommandSettings public int ItemCount { get; init; } [CommandOption("-p|--publication-period ")] - [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] + [Description("Publication period to filter content items by. If specified --from-date and --to-date are ignored.")] public PublicationPeriod PublicationPeriod { get; init; } [CommandOption("-f|--from-date ")] diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs index d1f2a2e..a801767 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs @@ -115,7 +115,7 @@ public class Settings : CommandSettings [Description("WordPress Export file path.")] public FilePath WordPressExportFilePath { get; init; } - [CommandOption("-f|--universal-file-path ")] + [CommandOption("-o|--universal-file-path ")] [Description("Universal file path.")] public FilePath UniversalFilePath { get; init; } } diff --git a/Solutions/Stacker.Cli/StackerCli.cs b/Solutions/Stacker.Cli/StackerCli.cs index cef7021..1fd615f 100644 --- a/Solutions/Stacker.Cli/StackerCli.cs +++ b/Solutions/Stacker.Cli/StackerCli.cs @@ -38,7 +38,15 @@ public static Task Main(string[] args) config.CaseSensitivity(CaseSensitivity.None); config.SetApplicationName("stacker"); + config.AddExample("linkedin", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin"); + config.AddExample("facebook", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin"); + config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin"); + config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--item-count", "10"); + config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--publication-period", "ThisMonth"); + config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--from-date", "2023/06/01", "--to-date", "2023/06/30"); + config.AddExample("environment", "init"); config.AddExample("wordpress", "export", "markdown", "-w", """C:\temp\wordpress-export.xml""", "-o", """C:\Temp\Blog"""); + config.AddExample("wordpress", "export", "universal", "-w", """C:\temp\wordpress-export.xml""", "-o", """C:\Temp\Blog\export.json"""); config.AddBranch("facebook", process => { From 9eec6b41966fa654f59abe3d0be894698b6a8789 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Fri, 27 Oct 2023 18:05:06 +0100 Subject: [PATCH 3/9] Add the ability to provide, via configuration, priority tags, tags to exclude and tag aliases --- .../Stacker.Cli/Commands/TwitterBufferCommand.cs | 2 +- .../Commands/WordPressExportMarkdownCommand.cs | 2 +- .../Commands/WordPressExportUniversalCommand.cs | 2 +- .../Stacker.Cli/Configuration/StackerSettings.cs | 6 ++++++ Solutions/Stacker.Cli/Configuration/TagAliases.cs | 14 ++++++++++++++ .../Contracts/Formatters/IContentFormatter.cs | 4 ++-- .../Formatters/LongFormContentFormatter.cs | 3 ++- Solutions/Stacker.Cli/Formatters/TweetFormatter.cs | 8 +++++--- 8 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 Solutions/Stacker.Cli/Configuration/TagAliases.cs diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs index 800c859..4a0dce9 100644 --- a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs @@ -29,7 +29,7 @@ public TwitterBufferCommand(IContentTasks contentTasks) /// public override async Task ExecuteAsync([NotNull] CommandContext context, [NotNull] Settings settings) { - await this.contentTasks.BufferContentItemsAsync( + await this.contentTasks.BufferContentItemsAsync( settings.ContentFilePath, $"twitter|", settings.ProfileName, diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs index d02d331..08f89be 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs @@ -220,7 +220,7 @@ private List LoadFeed(BlogSite blogSite) PromoteUntil = post.PromoteUntil, Slug = post.Slug, Status = post.Status, - Tags = post.Tags.Where(t => t != null).Select(t => t.Name), + Tags = post.Tags.Where(t => t != null).Select(t => t.Name).ToList(), }; // Search the body for any missing images. diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs index a801767..d477bbe 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs @@ -92,7 +92,7 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ PromoteUntil = post.PromoteUntil, Status = post.Status, Slug = post.Slug, - Tags = post.Tags.Where(t => t != null).Select(t => t.Name), + Tags = post.Tags.Where(t => t != null).Select(t => t.Name).ToList(), }); } diff --git a/Solutions/Stacker.Cli/Configuration/StackerSettings.cs b/Solutions/Stacker.Cli/Configuration/StackerSettings.cs index a05baf3..08e33bb 100644 --- a/Solutions/Stacker.Cli/Configuration/StackerSettings.cs +++ b/Solutions/Stacker.Cli/Configuration/StackerSettings.cs @@ -17,5 +17,11 @@ public class StackerSettings public Dictionary BufferProfiles { get; set; } + public List ExcludedTags { get; set; } + + public List TagAliases { get; set; } + + public List PriorityTags { get; set; } + public WordPressToMarkdown WordPressToMarkdown { get; set; } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Configuration/TagAliases.cs b/Solutions/Stacker.Cli/Configuration/TagAliases.cs new file mode 100644 index 0000000..a057548 --- /dev/null +++ b/Solutions/Stacker.Cli/Configuration/TagAliases.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Collections.Generic; + +namespace Stacker.Cli.Configuration; + +public class TagAliases +{ + public string Tag { get; set; } + + public List Aliases { get; set; } +} \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs b/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs index 2e93cb9..72ea1c1 100644 --- a/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs +++ b/Solutions/Stacker.Cli/Contracts/Formatters/IContentFormatter.cs @@ -3,12 +3,12 @@ // using System.Collections.Generic; - +using Stacker.Cli.Configuration; using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Contracts.Formatters; public interface IContentFormatter { - IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems); + IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems, StackerSettings settings); } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs b/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs index 6b3f76c..d1e115c 100644 --- a/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; +using Stacker.Cli.Configuration; using Stacker.Cli.Contracts.Formatters; using Stacker.Cli.Converters; using Stacker.Cli.Domain.Universal; @@ -23,7 +24,7 @@ public LongFormContentFormatter(int maxContentLength, string campaignSource) this.maxContentLength = maxContentLength; } - public IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems) + public IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems, StackerSettings settings) { var postings = new List(); var sb = new StringBuilder(); diff --git a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs index a364923..77b142c 100644 --- a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; - +using Stacker.Cli.Configuration; using Stacker.Cli.Contracts.Formatters; using Stacker.Cli.Converters; using Stacker.Cli.Domain.Universal; @@ -15,9 +15,9 @@ namespace Stacker.Cli.Formatters; public class TweetFormatter : IContentFormatter { private const int MaxContentLength = 280; - private string campaignSource = "twitter"; + private readonly string campaignSource = "twitter"; - public IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems) + public IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems, StackerSettings settings) { var tweets = new List(); var sb = new StringBuilder(); @@ -54,6 +54,8 @@ public IEnumerable Format(string campaignMedium, string campaignName, IE int tweetLength = sb.Length + item.Content.Link.Length + 1; // 1 = extra space before link int tagsToInclude = 0; + item.Tags = item.Tags.Except(settings.ExcludedTags).OrderByDescending(word => settings.PriorityTags.IndexOf(word)).ToList(); + foreach (string tag in item.Tags) { // 2 Offset = Space + # From dddddc5df079e1f03342be35ac31fd8198198f79 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Fri, 27 Oct 2023 18:05:37 +0100 Subject: [PATCH 4/9] Convert to List<> to prevent multiple enumerations. --- Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs | 2 +- Solutions/Stacker.Cli/Tasks/ContentTasks.cs | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs b/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs index 8edf617..f44745f 100644 --- a/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs +++ b/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs @@ -38,7 +38,7 @@ public string CleanSlug public string Status { get; internal set; } - public IEnumerable Tags { get; set; } + public List Tags { get; set; } public string UniqueId { diff --git a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs index c43584d..c9541a7 100644 --- a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs @@ -46,17 +46,15 @@ public async Task BufferContentItemsAsync(FilePath contentFil StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); - if (settings.BufferProfiles.ContainsKey(profileKey)) + if (settings.BufferProfiles.TryGetValue(profileKey, out string profile)) { - string profileId = settings.BufferProfiles[profileKey]; - - AnsiConsole.WriteLine($"Buffer Profile: {profileKey} = {profileId}"); + AnsiConsole.WriteLine($"Buffer Profile: {profileKey} = {profile}"); AnsiConsole.WriteLine($"Loading: {contentFilePath}"); IEnumerable contentItems = await this.LoadContentItemsAsync(contentFilePath, publicationPeriod, fromDate, toDate, itemCount).ConfigureAwait(false); - IEnumerable formattedContentItems = formatter.Format("social", profileName, contentItems); + IEnumerable formattedContentItems = formatter.Format("social", profileName, contentItems, settings); - await this.bufferClient.UploadAsync(formattedContentItems, profileId).ConfigureAwait(false); + // await this.bufferClient.UploadAsync(formattedContentItems, profileId).ConfigureAwait(false); } else { From 86bc12e2e82abedfc70aada5c2dc5698b3b00f95 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Sun, 29 Oct 2023 16:09:46 +0000 Subject: [PATCH 5/9] Add ability to filter by tag. --- .../Commands/FacebookBufferCommand.cs | 7 ++- .../Commands/LinkedInBufferCommand.cs | 7 ++- .../Commands/TwitterBufferCommand.cs | 7 ++- .../WordPressExportMarkdownCommand.cs | 36 ++++++------- .../WordPressExportUniversalCommand.cs | 2 +- .../Contracts/Tasks/IContentTasks.cs | 11 +++- ...gConverter.cs => TagToHashTagConverter.cs} | 4 +- .../Domain/Universal/ContentItem.cs | 9 +--- .../Formatters/LongFormContentFormatter.cs | 2 +- .../Stacker.Cli/Formatters/TweetFormatter.cs | 40 +++++++------- Solutions/Stacker.Cli/StackerCli.cs | 1 + Solutions/Stacker.Cli/Tasks/ContentTasks.cs | 53 ++++++++++++++----- 12 files changed, 108 insertions(+), 71 deletions(-) rename Solutions/Stacker.Cli/Converters/{WordPressTagToHashTagConverter.cs => TagToHashTagConverter.cs} (79%) diff --git a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs index 170419d..fc4e58b 100644 --- a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs @@ -35,7 +35,8 @@ await this.contentTasks.BufferContentItemsAsync( settings.PublicationPeriod, settings.FromDate, settings.ToDate, - settings.ItemCount).ConfigureAwait(false); + settings.ItemCount, + settings.FilterByTag).ConfigureAwait(false); return 0; } @@ -55,6 +56,10 @@ public class Settings : CommandSettings [Description("Facebook profile to Buffer.")] public string ProfileName { get; init; } + [CommandOption("-g|--filter-by-tag ")] + [Description("Tag to filter the content items by.")] + public string FilterByTag { get; init; } + [CommandOption("-i|--item-count ")] [Description("Number of content items to buffer. If omitted all content is buffered.")] public int ItemCount { get; init; } diff --git a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs index fb3ec65..8c45a83 100644 --- a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs @@ -36,7 +36,8 @@ await this.contentTasks.BufferContentItemsAsync( settings.PublicationPeriod, settings.FromDate, settings.ToDate, - settings.ItemCount).ConfigureAwait(false); + settings.ItemCount, + settings.FilterByTag).ConfigureAwait(false); return 0; } @@ -56,6 +57,10 @@ public class Settings : CommandSettings [Description("LinkedIn profile to Buffer.")] public string ProfileName { get; init; } + [CommandOption("-g|--filter-by-tag ")] + [Description("Tag to filter the content items by.")] + public string FilterByTag { get; init; } + [CommandOption("-i|--item-count ")] [Description("Number of content items to buffer. If omitted all content is buffered.")] public int ItemCount { get; init; } diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs index 4a0dce9..6c07df6 100644 --- a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs @@ -36,7 +36,8 @@ await this.contentTasks.BufferContentItemsAsync( settings.PublicationPeriod, settings.FromDate, settings.ToDate, - settings.ItemCount).ConfigureAwait(false); + settings.ItemCount, + settings.FilterByTag).ConfigureAwait(false); return 0; } @@ -56,6 +57,10 @@ public class Settings : CommandSettings [Description("Twitter profile to Buffer.")] public string ProfileName { get; init; } + [CommandOption("-g|--filter-by-tag ")] + [Description("Tag to filter the content items by.")] + public string FilterByTag { get; init; } + [CommandOption("-i|--item-count ")] [Description("Number of content items to buffer. If omitted all content is buffered.")] public int ItemCount { get; init; } diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs index 08f89be..4ee210f 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs @@ -137,10 +137,8 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ AnsiConsole.WriteLine(exception.Message); } - await using (StreamWriter writer = File.CreateText(outputFilePath)) - { - await writer.WriteAsync(content).ConfigureAwait(false); - } + await using StreamWriter writer = File.CreateText(outputFilePath); + await writer.WriteAsync(content).ConfigureAwait(false); } // Remote the temporary html file. @@ -156,10 +154,10 @@ private string CreateYamlHeader(ContentItem contentItem) { if (string.IsNullOrEmpty(contentItem.Slug)) { - contentItem.Slug = new(Regex.Replace(contentItem.Content.Title.ToLowerInvariant().Replace(" ", "-"), @"\-+", "-").Where(ch => !Path.GetInvalidFileNameChars().Contains(ch)).ToArray()); + contentItem.Slug = new string(Regex.Replace(contentItem.Content.Title.ToLowerInvariant().Replace(" ", "-"), @"\-+", "-").Where(ch => !Path.GetInvalidFileNameChars().Contains(ch)).ToArray()); } - var frontmatter = new + var frontMatter = new { Title = contentItem.Content.Title.Replace("“", "\"").Replace("”", "\"").Replace("’", "'").Replace("‘", "'"), Date = contentItem.PublishedOn.ToString("O"), @@ -173,12 +171,12 @@ private string CreateYamlHeader(ContentItem contentItem) Attachments = contentItem.Content.Attachments.Select(x => x.Path).Distinct(), }; - return this.serializer.Serialize(frontmatter); + return this.serializer.Serialize(frontMatter); } private List LoadFeed(BlogSite blogSite) { - AnsiConsole.WriteLine($"Processing..."); + AnsiConsole.WriteLine("Processing..."); var feed = new List(); StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); @@ -198,7 +196,7 @@ private List LoadFeed(BlogSite blogSite) var ci = new ContentItem { - Author = new() + Author = new AuthorDetails { DisplayName = post.Author.DisplayName, Email = post.Author.Email, @@ -206,7 +204,7 @@ private List LoadFeed(BlogSite blogSite) Username = post.Author.Username, }, Categories = post.Categories.Select(c => c.Name).Where(x => !this.IsCategoryExcluded(x)), - Content = new() + Content = new ContentDetails { Attachments = post.Attachments.Select(x => new ContentAttachment { Path = x.Path, Url = x.Url }).ToList(), Body = post.Body, @@ -232,7 +230,7 @@ private List LoadFeed(BlogSite blogSite) { if (!ci.Content.Attachments.Any(x => string.Equals(x.Url, match.Groups[1].Value, StringComparison.InvariantCultureIgnoreCase)) && this.IsRelevantHost(match.Groups[1].Value)) { - ci.Content.Attachments.Add(new() { Path = match.Groups[1].Value, Url = match.Groups[1].Value }); + ci.Content.Attachments.Add(new ContentAttachment { Path = match.Groups[1].Value, Url = match.Groups[1].Value }); } } } @@ -245,17 +243,13 @@ private List LoadFeed(BlogSite blogSite) return feed; } - private async Task LoadWordPressExportAsync(string wpexportFilePath) + private async Task LoadWordPressExportAsync(string exportFilePath) { - BlogSite blogSite; - - AnsiConsole.WriteLine($"Reading {wpexportFilePath}"); + AnsiConsole.WriteLine($"Reading {exportFilePath}"); - using (StreamReader reader = File.OpenText(wpexportFilePath)) - { - XDocument document = await XDocument.LoadAsync(reader, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); - blogSite = new(document); - } + using StreamReader reader = File.OpenText(exportFilePath); + XDocument document = await XDocument.LoadAsync(reader, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); + BlogSite blogSite = new(document); return blogSite; } @@ -275,7 +269,7 @@ private bool ExecutePandoc(string inputTempHtmlFilePath, string outputTempMarkdo RedirectStandardInput = true, }; - var process = new System.Diagnostics.Process { StartInfo = psi }; + var process = new Process { StartInfo = psi }; process.Start(); process.WaitForExit(); diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs index d477bbe..3318b6f 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs @@ -61,7 +61,7 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ List posts = blogSite.GetAllPosts().ToList(); List validPosts = posts.FilterByValid(stackerSettings).ToList(); List promotablePosts = validPosts.FilterByPromotable().ToList(); - WordPressTagToHashTagConverter hashTagConverter = new(); + TagToHashTagConverter hashTagConverter = new(); List feed = new(); AnsiConsole.WriteLine($"Total Posts: {posts.Count()}"); diff --git a/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs b/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs index 1c3b8af..ea0feef 100644 --- a/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs +++ b/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs @@ -23,8 +23,15 @@ Task BufferContentItemsAsync( PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, - int itemCount) + int itemCount, + string filterByTag) where TContentFormatter : class, IContentFormatter, new(); - Task> LoadContentItemsAsync(FilePath contentFilePath, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount); + Task> LoadContentItemsAsync( + FilePath contentFilePath, + PublicationPeriod publicationPeriod, + DateTime fromDate, + DateTime toDate, + int itemCount, + string filterByTag); } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Converters/WordPressTagToHashTagConverter.cs b/Solutions/Stacker.Cli/Converters/TagToHashTagConverter.cs similarity index 79% rename from Solutions/Stacker.Cli/Converters/WordPressTagToHashTagConverter.cs rename to Solutions/Stacker.Cli/Converters/TagToHashTagConverter.cs index dd159bb..87d09ec 100644 --- a/Solutions/Stacker.Cli/Converters/WordPressTagToHashTagConverter.cs +++ b/Solutions/Stacker.Cli/Converters/TagToHashTagConverter.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // @@ -6,7 +6,7 @@ namespace Stacker.Cli.Converters; -public class WordPressTagToHashTagConverter +public class TagToHashTagConverter { public string Convert(string wordpressTag) { diff --git a/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs b/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs index f44745f..58915d1 100644 --- a/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs +++ b/Solutions/Stacker.Cli/Domain/Universal/ContentItem.cs @@ -44,14 +44,7 @@ public string UniqueId { get { - if (string.IsNullOrEmpty(this.Slug)) - { - return this.Id; - } - else - { - return this.CleanSlug; - } + return string.IsNullOrEmpty(this.Slug) ? this.Id : this.CleanSlug; } } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs b/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs index d1e115c..be9bde9 100644 --- a/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/LongFormContentFormatter.cs @@ -29,7 +29,7 @@ public IEnumerable Format(string campaignMedium, string campaignName, IE var postings = new List(); var sb = new StringBuilder(); var sbTracking = new StringBuilder(); - var hashTagConverter = new WordPressTagToHashTagConverter(); + var hashTagConverter = new TagToHashTagConverter(); foreach (ContentItem item in feedItems) { diff --git a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs index 77b142c..ea85ec5 100644 --- a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs @@ -7,7 +7,6 @@ using System.Text; using Stacker.Cli.Configuration; using Stacker.Cli.Contracts.Formatters; -using Stacker.Cli.Converters; using Stacker.Cli.Domain.Universal; namespace Stacker.Cli.Formatters; @@ -19,22 +18,21 @@ public class TweetFormatter : IContentFormatter public IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems, StackerSettings settings) { - var tweets = new List(); - var sb = new StringBuilder(); - var sbTracking = new StringBuilder(); - var hashTagConverter = new WordPressTagToHashTagConverter(); + List tweets = new(); + StringBuilder sb = new(); + StringBuilder campaignTracking = new(); foreach (ContentItem item in feedItems) { - sbTracking.Append(" "); - sbTracking.Append(item.Content.Link); - sbTracking.Append("?utm_source="); - sbTracking.Append(this.campaignSource.ToLowerInvariant()); - sbTracking.Append("&utm_medium="); - sbTracking.Append(campaignMedium.ToLowerInvariant()); - sbTracking.Append("&utm_campaign="); - sbTracking.Append(campaignName.ToLowerInvariant()); - sbTracking.AppendLine(); + campaignTracking.Append(' '); + campaignTracking.Append(item.Content.Link); + campaignTracking.Append("?utm_source="); + campaignTracking.Append(this.campaignSource.ToLowerInvariant()); + campaignTracking.Append("&utm_medium="); + campaignTracking.Append(campaignMedium.ToLowerInvariant()); + campaignTracking.Append("&utm_campaign="); + campaignTracking.Append(campaignName.ToLowerInvariant()); + campaignTracking.AppendLine(); sb.Append(item.Content.Title); sb.Append(" by "); @@ -45,18 +43,18 @@ public IEnumerable Format(string campaignMedium, string campaignName, IE } else { - sb.Append("@"); + sb.Append('@'); sb.Append(item.Author.TwitterHandle); } - if (item.Tags != null && item.Tags.Any()) + if (item?.Tags != null && item.Tags.Any()) { int tweetLength = sb.Length + item.Content.Link.Length + 1; // 1 = extra space before link int tagsToInclude = 0; item.Tags = item.Tags.Except(settings.ExcludedTags).OrderByDescending(word => settings.PriorityTags.IndexOf(word)).ToList(); - foreach (string tag in item.Tags) + foreach (string tag in item.Tags.Distinct()) { // 2 Offset = Space + # if (tweetLength + tag.Length + 2 <= MaxContentLength) @@ -69,19 +67,19 @@ public IEnumerable Format(string campaignMedium, string campaignName, IE } } - foreach (string tag in item.Tags.Take(tagsToInclude)) + foreach (string tag in item.Tags.Distinct().Take(tagsToInclude)) { sb.Append(" #"); - sb.Append(hashTagConverter.Convert(tag)); + sb.Append(tag); } } - sb.Append(sbTracking.ToString()); + sb.Append(campaignTracking); tweets.Add(sb.ToString()); sb.Clear(); - sbTracking.Clear(); + campaignTracking.Clear(); } return tweets; diff --git a/Solutions/Stacker.Cli/StackerCli.cs b/Solutions/Stacker.Cli/StackerCli.cs index 1fd615f..00e6d0a 100644 --- a/Solutions/Stacker.Cli/StackerCli.cs +++ b/Solutions/Stacker.Cli/StackerCli.cs @@ -44,6 +44,7 @@ public static Task Main(string[] args) config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--item-count", "10"); config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--publication-period", "ThisMonth"); config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--from-date", "2023/06/01", "--to-date", "2023/06/30"); + config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--filter-by-tag", "PowerBI", "--from-date", "2023/06/01", "--to-date", "2023/06/30"); config.AddExample("environment", "init"); config.AddExample("wordpress", "export", "markdown", "-w", """C:\temp\wordpress-export.xml""", "-o", """C:\Temp\Blog"""); config.AddExample("wordpress", "export", "universal", "-w", """C:\temp\wordpress-export.xml""", "-o", """C:\Temp\Blog\export.json"""); diff --git a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs index c9541a7..6ece19f 100644 --- a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs @@ -37,7 +37,15 @@ public ContentTasks(IBufferClient bufferClient, IStackerSettingsManager settings this.settingsManager = settingsManager; } - public async Task BufferContentItemsAsync(FilePath contentFilePath, string profilePrefix, string profileName, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) + public async Task BufferContentItemsAsync( + FilePath contentFilePath, + string profilePrefix, + string profileName, + PublicationPeriod publicationPeriod, + DateTime fromDate, + DateTime toDate, + int itemCount, + string filterByTag) where TContentFormatter : class, IContentFormatter, new() { TContentFormatter formatter = new(); @@ -51,7 +59,7 @@ public async Task BufferContentItemsAsync(FilePath contentFil AnsiConsole.WriteLine($"Buffer Profile: {profileKey} = {profile}"); AnsiConsole.WriteLine($"Loading: {contentFilePath}"); - IEnumerable contentItems = await this.LoadContentItemsAsync(contentFilePath, publicationPeriod, fromDate, toDate, itemCount).ConfigureAwait(false); + IEnumerable contentItems = await this.LoadContentItemsAsync(contentFilePath, publicationPeriod, fromDate, toDate, itemCount, filterByTag).ConfigureAwait(false); IEnumerable formattedContentItems = formatter.Format("social", profileName, contentItems, settings); // await this.bufferClient.UploadAsync(formattedContentItems, profileId).ConfigureAwait(false); @@ -62,15 +70,21 @@ public async Task BufferContentItemsAsync(FilePath contentFil } } - public async Task> LoadContentItemsAsync(FilePath contentFilePath, PublicationPeriod publicationPeriod, DateTime fromDate, DateTime toDate, int itemCount) + public async Task> LoadContentItemsAsync( + FilePath contentFilePath, + PublicationPeriod publicationPeriod, + DateTime fromDate, + DateTime toDate, + int itemCount, + string filterByTag) { - IEnumerable content = JsonConvert.DeserializeObject>(await File.ReadAllTextAsync(contentFilePath.FullPath).ConfigureAwait(false)); + List content = JsonConvert.DeserializeObject>(await File.ReadAllTextAsync(contentFilePath.FullPath).ConfigureAwait(false)); if (publicationPeriod != PublicationPeriod.None) { DateInterval dateRange = new PublicationPeriodConverter().Convert(publicationPeriod); - content = content.Where(p => (LocalDate.FromDateTime(p.PublishedOn.LocalDateTime) >= dateRange.Start) && (LocalDate.FromDateTime(p.PublishedOn.LocalDateTime) <= dateRange.End)); + content = content.Where(p => (LocalDate.FromDateTime(p.PublishedOn.LocalDateTime) >= dateRange.Start) && (LocalDate.FromDateTime(p.PublishedOn.LocalDateTime) <= dateRange.End)).ToList(); } else { @@ -85,35 +99,50 @@ public async Task> LoadContentItemsAsync(FilePath conte { if (toDate.ToString("HH:mm:ss") == "00:00:00") { - toDate = new(toDate.Year, toDate.Month, toDate.Day, 23, 59, 59); + toDate = new DateTime(toDate.Year, toDate.Month, toDate.Day, 23, 59, 59); } } - content = content.Where(p => p.PublishedOn.LocalDateTime >= fromDate && p.PublishedOn.LocalDateTime <= toDate); + content = content.Where(p => p.PublishedOn.LocalDateTime >= fromDate && p.PublishedOn.LocalDateTime <= toDate).ToList(); } else { // if fromDate isn't specified, but toDate is if (toDate != DateTime.MinValue) { - content = content.Where(p => p.PublishedOn.LocalDateTime >= fromDate && p.PublishedOn.LocalDateTime <= toDate); + content = content.Where(p => p.PublishedOn.LocalDateTime >= fromDate && p.PublishedOn.LocalDateTime <= toDate).ToList(); } } } + StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); + + foreach (ContentItem contentItem in content) + { + // Use TagAliases to convert tags into their canonical form. + contentItem.Tags = contentItem.Tags?.Select(tag => + { + TagAliases matchedAlias = settings.TagAliases.FirstOrDefault(alias => alias.Aliases.Any(a => a == tag)); + return matchedAlias != null ? matchedAlias.Tag : tag.Replace("-", " ").Replace(" ", string.Empty); + }).ToList(); + } + // Sort so that content with the shortest lifespan are first. content = content.OrderBy(p => p.PromoteUntil).ToList(); - var contentItems = content.ToList(); + if (!string.IsNullOrEmpty(filterByTag)) + { + content = content.Where(x => x.Tags.Contains(filterByTag, StringComparer.InvariantCultureIgnoreCase)).ToList(); + } if (itemCount == 0) { - itemCount = contentItems.Count; + itemCount = content.Count; } - AnsiConsole.WriteLine($"Total Posts: {contentItems.Count}"); + AnsiConsole.WriteLine($"Total Posts: {content.Count}"); AnsiConsole.WriteLine($"Promoting first: {itemCount}"); - return contentItems.Take(itemCount); + return content.Take(itemCount); } } \ No newline at end of file From ea21e7b57fc8fb3b2a6bbf3da84bb71264b0278e Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Sun, 29 Oct 2023 19:48:05 +0000 Subject: [PATCH 6/9] Add WhatIf support --- Solutions/Stacker.Cli/BufferClient.cs | 43 ++++++++++--------- .../Commands/FacebookBufferCommand.cs | 7 ++- .../Commands/LinkedInBufferCommand.cs | 7 ++- .../Commands/TwitterBufferCommand.cs | 7 ++- .../Contracts/Buffer/IBufferClient.cs | 2 +- .../Contracts/Tasks/IContentTasks.cs | 3 +- .../Stacker.Cli/Formatters/TweetFormatter.cs | 24 +++++------ Solutions/Stacker.Cli/Tasks/ContentTasks.cs | 14 +++--- 8 files changed, 63 insertions(+), 44 deletions(-) diff --git a/Solutions/Stacker.Cli/BufferClient.cs b/Solutions/Stacker.Cli/BufferClient.cs index 616ba46..bbd2164 100644 --- a/Solutions/Stacker.Cli/BufferClient.cs +++ b/Solutions/Stacker.Cli/BufferClient.cs @@ -46,35 +46,36 @@ public IEnumerable> ConvertToPayload(string content return postData; } - public async Task UploadAsync(IEnumerable content, string profileId) + public async Task UploadAsync(IEnumerable content, string profileId, bool whatIf) { - using (HttpClient client = this.httpClientFactory.CreateClient()) - { - client.BaseAddress = new Uri(BaseUri); - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded")); + using HttpClient client = this.httpClientFactory.CreateClient(); + client.BaseAddress = new Uri(BaseUri); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded")); + + StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); + string updateOperationUrl = $"{UpdateOperation}?access_token={settings.BufferAccessToken}"; - StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); - string updateOperationUrl = $"{UpdateOperation}?access_token={settings.BufferAccessToken}"; + foreach (string item in content) + { + AnsiConsole.MarkupLineInterpolated($"[chartreuse3_1]Buffering:[/] {item}"); - foreach (string item in content) + if (whatIf) { - HttpContent payload = new FormUrlEncodedContent(this.ConvertToPayload(item, new string[] { profileId })); + continue; + } - AnsiConsole.WriteLine($"Buffering: {item}"); + HttpContent payload = new FormUrlEncodedContent(this.ConvertToPayload(item, new string[] { profileId })); - HttpResponseMessage response = await client.PostAsync(updateOperationUrl, payload).ConfigureAwait(false); + HttpResponseMessage response = await client.PostAsync(updateOperationUrl, payload).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - string errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - BufferError error = JsonConvert.DeserializeObject(errorContent); + if (!response.IsSuccessStatusCode) + { + string errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + BufferError error = JsonConvert.DeserializeObject(errorContent); - AnsiConsole.Foreground = ConsoleColor.Red; - AnsiConsole.WriteLine($"Buffering Failed: {error.Message}"); - AnsiConsole.WriteLine(); - AnsiConsole.ResetColors(); - } + AnsiConsole.MarkupLineInterpolated($"[red]Buffering Failed:[/] {error.Message}"); + AnsiConsole.WriteLine(); } } } diff --git a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs index fc4e58b..51bf494 100644 --- a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs @@ -36,7 +36,8 @@ await this.contentTasks.BufferContentItemsAsync( settings.FromDate, settings.ToDate, settings.ItemCount, - settings.FilterByTag).ConfigureAwait(false); + settings.FilterByTag, + settings.WhatIf).ConfigureAwait(false); return 0; } @@ -76,6 +77,10 @@ public class Settings : CommandSettings [Description("Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.")] public DateTime ToDate { get; init; } + [CommandOption("-w|--what-if ")] + [Description("See what the command would do without submitting the content to Buffer.")] + public bool WhatIf { get; set; } + #nullable enable annotations } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs index 8c45a83..fc4cf98 100644 --- a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs @@ -37,7 +37,8 @@ await this.contentTasks.BufferContentItemsAsync( settings.FromDate, settings.ToDate, settings.ItemCount, - settings.FilterByTag).ConfigureAwait(false); + settings.FilterByTag, + settings.WhatIf).ConfigureAwait(false); return 0; } @@ -77,6 +78,10 @@ public class Settings : CommandSettings [Description("Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.")] public DateTime ToDate { get; init; } + [CommandOption("-w|--what-if ")] + [Description("See what the command would do without submitting the content to Buffer.")] + public bool WhatIf { get; set; } + #nullable enable annotations } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs index 6c07df6..7f586af 100644 --- a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs @@ -37,7 +37,8 @@ await this.contentTasks.BufferContentItemsAsync( settings.FromDate, settings.ToDate, settings.ItemCount, - settings.FilterByTag).ConfigureAwait(false); + settings.FilterByTag, + settings.WhatIf).ConfigureAwait(false); return 0; } @@ -77,6 +78,10 @@ public class Settings : CommandSettings [Description("Include content items published on, or before this date. Use YYYY/MM/DD Format. If omitted DateTime.MaxValue is used.")] public DateTime ToDate { get; init; } + [CommandOption("-w|--what-if ")] + [Description("See what the command would do without submitting the content to Buffer.")] + public bool WhatIf { get; set; } + #nullable enable annotations } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Contracts/Buffer/IBufferClient.cs b/Solutions/Stacker.Cli/Contracts/Buffer/IBufferClient.cs index 1493cb4..19c2ed2 100644 --- a/Solutions/Stacker.Cli/Contracts/Buffer/IBufferClient.cs +++ b/Solutions/Stacker.Cli/Contracts/Buffer/IBufferClient.cs @@ -9,5 +9,5 @@ namespace Stacker.Cli.Contracts.Buffer; public interface IBufferClient { - Task UploadAsync(IEnumerable content, string profileId); + Task UploadAsync(IEnumerable content, string profileId, bool whatIf); } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs b/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs index ea0feef..bf3d553 100644 --- a/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs +++ b/Solutions/Stacker.Cli/Contracts/Tasks/IContentTasks.cs @@ -24,7 +24,8 @@ Task BufferContentItemsAsync( DateTime fromDate, DateTime toDate, int itemCount, - string filterByTag) + string filterByTag, + bool whatIf) where TContentFormatter : class, IContentFormatter, new(); Task> LoadContentItemsAsync( diff --git a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs index ea85ec5..ac6bd42 100644 --- a/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs +++ b/Solutions/Stacker.Cli/Formatters/TweetFormatter.cs @@ -19,7 +19,7 @@ public class TweetFormatter : IContentFormatter public IEnumerable Format(string campaignMedium, string campaignName, IEnumerable feedItems, StackerSettings settings) { List tweets = new(); - StringBuilder sb = new(); + StringBuilder content = new(); StringBuilder campaignTracking = new(); foreach (ContentItem item in feedItems) @@ -34,22 +34,22 @@ public IEnumerable Format(string campaignMedium, string campaignName, IE campaignTracking.Append(campaignName.ToLowerInvariant()); campaignTracking.AppendLine(); - sb.Append(item.Content.Title); - sb.Append(" by "); + content.Append(item.Content.Title); + content.Append(" by "); if (string.IsNullOrEmpty(item.Author.TwitterHandle)) { - sb.Append(item.Author.DisplayName); + content.Append(item.Author.DisplayName); } else { - sb.Append('@'); - sb.Append(item.Author.TwitterHandle); + content.Append('@'); + content.Append(item.Author.TwitterHandle); } if (item?.Tags != null && item.Tags.Any()) { - int tweetLength = sb.Length + item.Content.Link.Length + 1; // 1 = extra space before link + int tweetLength = content.Length + (item.Content.Link.Length + 1) + campaignTracking.Length; // 1 = extra space before link int tagsToInclude = 0; item.Tags = item.Tags.Except(settings.ExcludedTags).OrderByDescending(word => settings.PriorityTags.IndexOf(word)).ToList(); @@ -69,16 +69,16 @@ public IEnumerable Format(string campaignMedium, string campaignName, IE foreach (string tag in item.Tags.Distinct().Take(tagsToInclude)) { - sb.Append(" #"); - sb.Append(tag); + content.Append(" #"); + content.Append(tag); } } - sb.Append(campaignTracking); + content.Append(campaignTracking); - tweets.Add(sb.ToString()); + tweets.Add(content.ToString()); - sb.Clear(); + content.Clear(); campaignTracking.Clear(); } diff --git a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs index 6ece19f..bb8b48c 100644 --- a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs @@ -45,7 +45,8 @@ public async Task BufferContentItemsAsync( DateTime fromDate, DateTime toDate, int itemCount, - string filterByTag) + string filterByTag, + bool whatIf) where TContentFormatter : class, IContentFormatter, new() { TContentFormatter formatter = new(); @@ -56,13 +57,13 @@ public async Task BufferContentItemsAsync( if (settings.BufferProfiles.TryGetValue(profileKey, out string profile)) { - AnsiConsole.WriteLine($"Buffer Profile: {profileKey} = {profile}"); - AnsiConsole.WriteLine($"Loading: {contentFilePath}"); + AnsiConsole.MarkupLineInterpolated($"[yellow1]Buffer Profile:[/] {profileKey} = {profile}"); + AnsiConsole.MarkupLineInterpolated($"[yellow1]Loading:[/] {contentFilePath}"); IEnumerable contentItems = await this.LoadContentItemsAsync(contentFilePath, publicationPeriod, fromDate, toDate, itemCount, filterByTag).ConfigureAwait(false); IEnumerable formattedContentItems = formatter.Format("social", profileName, contentItems, settings); - // await this.bufferClient.UploadAsync(formattedContentItems, profileId).ConfigureAwait(false); + await this.bufferClient.UploadAsync(formattedContentItems, profile, whatIf).ConfigureAwait(false); } else { @@ -140,8 +141,9 @@ public async Task> LoadContentItemsAsync( itemCount = content.Count; } - AnsiConsole.WriteLine($"Total Posts: {content.Count}"); - AnsiConsole.WriteLine($"Promoting first: {itemCount}"); + AnsiConsole.MarkupLineInterpolated($"[yellow1]Total Posts:[/] {content.Count}"); + AnsiConsole.MarkupLineInterpolated($"[yellow1]Promoting first:[/] {itemCount}"); + AnsiConsole.WriteLine(); return content.Take(itemCount); } From 69285e1bc07f97aca1d41f36b3bfc884aba7f87d Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Mon, 30 Oct 2023 06:48:56 +0000 Subject: [PATCH 7/9] Replace Newtonsoft.Json with System.Text.Json --- Solutions/Stacker.Cli/BufferClient.cs | 5 ++--- Solutions/Stacker.Cli/BufferError.cs | 11 +++++------ .../Commands/WordPressExportUniversalCommand.cs | 6 +++--- .../Stacker.Cli/Configuration/SettingsManager{T}.cs | 7 +++---- Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs | 5 ++--- Solutions/Stacker.Cli/Profiles.cs | 4 ++-- Solutions/Stacker.Cli/Stacker.Cli.csproj | 1 - Solutions/Stacker.Cli/Tasks/ContentTasks.cs | 5 ++--- Solutions/Stacker.Cli/packages.lock.json | 6 ------ 9 files changed, 19 insertions(+), 31 deletions(-) diff --git a/Solutions/Stacker.Cli/BufferClient.cs b/Solutions/Stacker.Cli/BufferClient.cs index bbd2164..51753a8 100644 --- a/Solutions/Stacker.Cli/BufferClient.cs +++ b/Solutions/Stacker.Cli/BufferClient.cs @@ -7,10 +7,9 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using System.Threading.Tasks; -using Newtonsoft.Json; - using Spectre.Console; using Stacker.Cli.Configuration; @@ -72,7 +71,7 @@ public async Task UploadAsync(IEnumerable content, string profileId, boo if (!response.IsSuccessStatusCode) { string errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - BufferError error = JsonConvert.DeserializeObject(errorContent); + BufferError error = JsonSerializer.Deserialize(errorContent); AnsiConsole.MarkupLineInterpolated($"[red]Buffering Failed:[/] {error.Message}"); AnsiConsole.WriteLine(); diff --git a/Solutions/Stacker.Cli/BufferError.cs b/Solutions/Stacker.Cli/BufferError.cs index 8f13dcd..8d0b39a 100644 --- a/Solutions/Stacker.Cli/BufferError.cs +++ b/Solutions/Stacker.Cli/BufferError.cs @@ -3,22 +3,21 @@ // using System.Collections.Generic; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Stacker.Cli; public class BufferError { - [JsonProperty("success")] + [JsonPropertyName("success")] public bool Success { get; set; } - [JsonProperty("message")] + [JsonPropertyName("message")] public string Message { get; set; } - [JsonProperty("code")] + [JsonPropertyName("code")] public int Code { get; set; } - [JsonProperty("errored_profiles")] + [JsonPropertyName("errored_profiles")] public IEnumerable ErroredProfiles { get; set; } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs index 3318b6f..7a1e428 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportUniversalCommand.cs @@ -8,12 +8,11 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; -using Newtonsoft.Json; - using Spectre.Console; using Spectre.Console.Cli; using Spectre.IO; @@ -98,7 +97,8 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ await using (StreamWriter writer = File.CreateText(settings.UniversalFilePath.FullPath)) { - await writer.WriteAsync(JsonConvert.SerializeObject(feed, Formatting.Indented)).ConfigureAwait(false); + JsonSerializerOptions options = new() { WriteIndented = true }; + await writer.WriteAsync(JsonSerializer.Serialize(feed, options)).ConfigureAwait(false); } AnsiConsole.WriteLine($"Content written to {settings.UniversalFilePath.FullPath}"); diff --git a/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs b/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs index 3f1d37d..1157950 100644 --- a/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs +++ b/Solutions/Stacker.Cli/Configuration/SettingsManager{T}.cs @@ -3,8 +3,7 @@ // using System.IO; - -using Newtonsoft.Json; +using System.Text.Json; using Stacker.Cli.Contracts.Configuration; @@ -24,13 +23,13 @@ public T LoadSettings(string fileName) { string filePath = $"{this.GetLocalFilePath(fileName)}.json"; - return File.Exists(filePath) ? JsonConvert.DeserializeObject(File.ReadAllText(filePath)) : null; + return File.Exists(filePath) ? JsonSerializer.Deserialize(File.ReadAllText(filePath)) : null; } public void SaveSettings(T settings, string fileName) { string filePath = this.GetLocalFilePath(fileName); - string json = JsonConvert.SerializeObject(settings); + string json = JsonSerializer.Serialize(settings); File.WriteAllText($"{filePath}.json", json); } diff --git a/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs b/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs index 4d7eb4a..f1f8c6b 100644 --- a/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs +++ b/Solutions/Stacker.Cli/Domain/WordPress/BlogSite.cs @@ -5,13 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; using System.Web; using System.Xml; using System.Xml.Linq; -using Newtonsoft.Json; - namespace Stacker.Cli.Domain.WordPress; public class BlogSite @@ -411,7 +410,7 @@ private void ParseSsoMetaData(XElement metaKeyElement, XElement postMeta, Dictio try { - Dictionary data = JsonConvert.DeserializeObject>(rawMetaData); + Dictionary data = JsonSerializer.Deserialize>(rawMetaData); foreach ((string key, string value) in data) { diff --git a/Solutions/Stacker.Cli/Profiles.cs b/Solutions/Stacker.Cli/Profiles.cs index c9806f7..71e9e68 100644 --- a/Solutions/Stacker.Cli/Profiles.cs +++ b/Solutions/Stacker.Cli/Profiles.cs @@ -2,12 +2,12 @@ // Copyright (c) Endjin Limited. All rights reserved. // -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Stacker.Cli; public class Profiles { - [JsonProperty("profile_id")] + [JsonPropertyName("profile_id")] public string ProfileId { get; set; } } \ No newline at end of file diff --git a/Solutions/Stacker.Cli/Stacker.Cli.csproj b/Solutions/Stacker.Cli/Stacker.Cli.csproj index 5dc8b2e..896dd25 100644 --- a/Solutions/Stacker.Cli/Stacker.Cli.csproj +++ b/Solutions/Stacker.Cli/Stacker.Cli.csproj @@ -57,7 +57,6 @@ - diff --git a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs index bb8b48c..0ff2e98 100644 --- a/Solutions/Stacker.Cli/Tasks/ContentTasks.cs +++ b/Solutions/Stacker.Cli/Tasks/ContentTasks.cs @@ -6,10 +6,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; -using Newtonsoft.Json; - using NodaTime; using Spectre.Console; @@ -79,7 +78,7 @@ public async Task> LoadContentItemsAsync( int itemCount, string filterByTag) { - List content = JsonConvert.DeserializeObject>(await File.ReadAllTextAsync(contentFilePath.FullPath).ConfigureAwait(false)); + List content = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(contentFilePath.FullPath).ConfigureAwait(false)); if (publicationPeriod != PublicationPeriod.None) { diff --git a/Solutions/Stacker.Cli/packages.lock.json b/Solutions/Stacker.Cli/packages.lock.json index 8fd182e..7c5caf7 100644 --- a/Solutions/Stacker.Cli/packages.lock.json +++ b/Solutions/Stacker.Cli/packages.lock.json @@ -45,12 +45,6 @@ "Microsoft.Extensions.Options": "7.0.0" } }, - "Newtonsoft.Json": { - "type": "Direct", - "requested": "[13.0.3, )", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, "NodaTime": { "type": "Direct", "requested": "[3.1.9, )", From 4744c2187f9087706c9fa9c6c2ba7cfac903af30 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Mon, 30 Oct 2023 06:51:14 +0000 Subject: [PATCH 8/9] Add "What If" example --- Solutions/Stacker.Cli/StackerCli.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Solutions/Stacker.Cli/StackerCli.cs b/Solutions/Stacker.Cli/StackerCli.cs index 00e6d0a..3e0ae9d 100644 --- a/Solutions/Stacker.Cli/StackerCli.cs +++ b/Solutions/Stacker.Cli/StackerCli.cs @@ -43,6 +43,7 @@ public static Task Main(string[] args) config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin"); config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--item-count", "10"); config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--publication-period", "ThisMonth"); + config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--filter-by-tag", "MicrosoftFabric", "--what-if"); config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--from-date", "2023/06/01", "--to-date", "2023/06/30"); config.AddExample("twitter", "buffer", "-c", """c:\temp\content.json""", "-n", "endjin", "--filter-by-tag", "PowerBI", "--from-date", "2023/06/01", "--to-date", "2023/06/30"); config.AddExample("environment", "init"); From 500f5550812ea6327fac182c22ce2d2cd106ecf6 Mon Sep 17 00:00:00 2001 From: Howard van Rooijen Date: Mon, 30 Oct 2023 07:14:14 +0000 Subject: [PATCH 9/9] Clean up classes. --- .../Commands/EnvironmentInitCommand.cs | 2 +- .../Commands/FacebookBufferCommand.cs | 3 ++- .../Commands/LinkedInBufferCommand.cs | 3 ++- .../Commands/TwitterBufferCommand.cs | 3 ++- .../WordPressExportMarkdownCommand.cs | 21 +++++++------------ 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs b/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs index 9c35d18..377cb82 100644 --- a/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs +++ b/Solutions/Stacker.Cli/Commands/EnvironmentInitCommand.cs @@ -32,7 +32,7 @@ public override int Execute(CommandContext context) new StackerSettings { BufferAccessToken = "", - BufferProfiles = new() + BufferProfiles = new Dictionary { { "facebook|", "" }, { "linkedin|", "" }, diff --git a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs index 51bf494..56afe4c 100644 --- a/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/FacebookBufferCommand.cs @@ -19,6 +19,7 @@ namespace Stacker.Cli.Commands; public class FacebookBufferCommand : AsyncCommand { private readonly IContentTasks contentTasks; + private readonly string profilePrefix = "facebook|"; public FacebookBufferCommand(IContentTasks contentTasks) { @@ -30,7 +31,7 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ { await this.contentTasks.BufferContentItemsAsync( settings.ContentFilePath, - $"facebook|", + this.profilePrefix, settings.ProfileName, settings.PublicationPeriod, settings.FromDate, diff --git a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs index fc4cf98..99f4dea 100644 --- a/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/LinkedInBufferCommand.cs @@ -20,6 +20,7 @@ namespace Stacker.Cli.Commands; public class LinkedInBufferCommand : AsyncCommand { private readonly IContentTasks contentTasks; + private readonly string profilePrefix = "linkedin|"; public LinkedInBufferCommand(IContentTasks contentTasks) { @@ -31,7 +32,7 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ { await this.contentTasks.BufferContentItemsAsync( settings.ContentFilePath, - $"linkedin|", + this.profilePrefix, settings.ProfileName, settings.PublicationPeriod, settings.FromDate, diff --git a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs index 7f586af..b53e3c9 100644 --- a/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs +++ b/Solutions/Stacker.Cli/Commands/TwitterBufferCommand.cs @@ -20,6 +20,7 @@ namespace Stacker.Cli.Commands; public class TwitterBufferCommand : AsyncCommand { private readonly IContentTasks contentTasks; + private readonly string profilePrefix = "twitter|"; public TwitterBufferCommand(IContentTasks contentTasks) { @@ -31,7 +32,7 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ { await this.contentTasks.BufferContentItemsAsync( settings.ContentFilePath, - $"twitter|", + this.profilePrefix, settings.ProfileName, settings.PublicationPeriod, settings.FromDate, diff --git a/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs index 4ee210f..f5ae3cb 100644 --- a/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs +++ b/Solutions/Stacker.Cli/Commands/WordPressExportMarkdownCommand.cs @@ -69,7 +69,7 @@ public override async Task ExecuteAsync([NotNull] CommandContext context, [ List feed = this.LoadFeed(blogSite); - var sb = new StringBuilder(); + StringBuilder sb = new(); FileInfo fi = new(settings.OutputDirectoryPath.FullPath); DirectoryInfo tempHtmlFolder = new(Path.Join(Path.GetTempPath(), "stacker", "html")); DirectoryInfo tempMarkdownFolder = new(Path.Join(Path.GetTempPath(), "stacker", "md")); @@ -178,9 +178,9 @@ private List LoadFeed(BlogSite blogSite) { AnsiConsole.WriteLine("Processing..."); - var feed = new List(); + List feed = new(); StackerSettings settings = this.settingsManager.LoadSettings(nameof(StackerSettings)); - var posts = blogSite.GetAllPostsInAllPublicationStates().ToList(); + List posts = blogSite.GetAllPostsInAllPublicationStates().ToList(); AnsiConsole.WriteLine($"Total Posts: {posts.Count}"); @@ -189,12 +189,12 @@ private List LoadFeed(BlogSite blogSite) { User user = settings.Users.Find(u => string.Equals(u.Email, post.Author.Email, StringComparison.InvariantCultureIgnoreCase)); - if (user == null) + if (user is null) { throw new NotImplementedException($"User {post.Author.Email} has not been configured. Update the settings file."); } - var ci = new ContentItem + ContentItem ci = new() { Author = new AuthorDetails { @@ -260,7 +260,7 @@ private bool ExecutePandoc(string inputTempHtmlFilePath, string outputTempMarkdo string arguments = $"-f html+raw_html --to=markdown_github-raw_html --wrap=preserve -o \"{outputTempMarkdownFilePath}\" \"{inputTempHtmlFilePath}\" "; - var psi = new ProcessStartInfo + ProcessStartInfo psi = new() { FileName = "pandoc", Arguments = arguments, @@ -269,7 +269,7 @@ private bool ExecutePandoc(string inputTempHtmlFilePath, string outputTempMarkdo RedirectStandardInput = true, }; - var process = new Process { StartInfo = psi }; + Process process = new() { StartInfo = psi }; process.Start(); process.WaitForExit(); @@ -300,12 +300,7 @@ private string GetHeaderImage(List attachments) string header = attachments.Find(x => x.Contains("header-", StringComparison.InvariantCultureIgnoreCase) || x.Contains("1024px", StringComparison.InvariantCultureIgnoreCase)); - if (!string.IsNullOrEmpty(header)) - { - return header.Trim(); - } - - return string.Empty; + return !string.IsNullOrEmpty(header) ? header.Trim() : string.Empty; } private bool IsRelevantHost(string url)