diff --git a/Commands/Owner/HomeServerCommands/CdnCommands.cs b/Commands/Owner/HomeServerCommands/CdnCommands.cs deleted file mode 100644 index 42cb9b2..0000000 --- a/Commands/Owner/HomeServerCommands/CdnCommands.cs +++ /dev/null @@ -1,233 +0,0 @@ -namespace MechanicalMilkshake.Commands.Owner.HomeServerCommands; - -[Command("cdn")] -[Description("Manage files uploaded to Amazon S3-compatible cloud storage.")] -public partial class Cdn -{ - [Command("upload")] - [Description("Upload a file to Amazon S3-compatible cloud storage.")] - public static async Task Upload(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("name"), Description("The name for the uploaded file.")] - string name = "preserve", - [Parameter("link"), Description("A link to a file to upload.")] - string link = null, - [Parameter("file"), Description("A direct file to upload. This will override a link if both are provided!")] - DiscordAttachment file = null) - { - await ctx.DeferResponseAsync(); - - if (Program.DisabledCommands.Contains("cdn")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - if (file is null && link is null) - { - await ctx.FollowupAsync( - new DiscordFollowupMessageBuilder().WithContent("You must provide a link or file to upload!")); - return; - } - - if (name.Contains(' ')) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - "The name of the file cannot contain spaces! Please try again.")); - } - - if (file is not null) link = file.Url; - - link = link.Replace("<", ""); - link = link.Replace(">", ""); - - string fileName; - - // Get file, where 'link' is the URL - MemoryStream memStream = new(await Program.HttpClient.GetByteArrayAsync(link)); - - try - { - var bucket = Program.ConfigJson.S3.Bucket; - - // Strip the URL down to just the file name - - // Regex partially taken from https://stackoverflow.com/a/26253039 - var fileNamePattern = FileNamePattern(); - - var fileNameAndExtension = fileNamePattern.Match(link).Value; - - // From here on out we can be sure that 'fileNameAndExtension' is in the format 'example.png'. - - var extension = Path.GetExtension(fileNameAndExtension); - - // The user might have included an extension in their desired filename. We should remove it. - // We should not just match `extension` because the user may have provided a different one! - // Let's use regex instead. - var fileExtensionPattern = FileExtensionPattern(); - var userExtension = fileExtensionPattern.Match(name).Value; - if (userExtension != "") - { - name = name.Replace(userExtension, ""); - } - - const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - fileName = name switch - { - "random" or "generate" => new string(Enumerable.Repeat(chars, 10) - .Select(s => s[Program.Random.Next(s.Length)]) - .ToArray()) + extension, - "preserve" => fileNameAndExtension, - _ => name + extension - }; - - var args = new PutObjectArgs() - .WithBucket(bucket) - .WithObject(fileName) - .WithStreamData(memStream) - .WithObjectSize(memStream.Length) - .WithContentType(MimeTypeMap.GetMimeType(extension)); - - await Program.Minio.PutObjectAsync(args); - } - catch (MinioException e) - { - await ctx.FollowupAsync( - new DiscordFollowupMessageBuilder().WithContent( - $"An API error occured while uploading!```\n{e.Message}```")); - return; - } - catch (Exception e) - { - await ctx.FollowupAsync( - new DiscordFollowupMessageBuilder().WithContent( - $"An unexpected error occured while uploading!```\n{e.Message}```")); - return; - } - - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"Upload successful!\n<{Program.ConfigJson.S3.CdnBaseUrl}/{fileName}>")); - } - - [Command("delete")] - [Description("Delete a file from Amazon S3-compatible cloud storage.")] - public static async Task DeleteUpload(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("file"), Description("The file to delete.")] - string fileToDelete) - { - await ctx.DeferResponseAsync(); - - if (Program.DisabledCommands.Contains("cdn")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - fileToDelete = fileToDelete.Replace("<", "").Replace(">", ""); - - var fileName = fileToDelete.Replace($"{Program.ConfigJson.S3.CdnBaseUrl}/", ""); - - try - { - var args = new RemoveObjectArgs() - .WithBucket(Program.ConfigJson.S3.Bucket) - .WithObject(fileName); - - await Program.Minio.RemoveObjectAsync(args); - } - catch (MinioException e) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"An API error occured while attempting to delete the file!```\n{e.Message}```")); - return; - } - catch (Exception e) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"An unexpected error occured while attempting to delete the file!```\n{e.Message}```")); - return; - } - - await ctx.FollowupAsync( - new DiscordFollowupMessageBuilder().WithContent( - "File deleted successfully!\nAttempting to purge Cloudflare cache...")); - - var cloudflareUrlPrefix = Program.ConfigJson.Cloudflare.UrlPrefix; - - // This code is (mostly) taken from https://github.com/Sankra/cloudflare-cache-purger/blob/master/main.csx#L113. - // (Note that I originally found it here: https://github.com/Erisa/Lykos/blob/1f32e03/src/Modules/Owner.cs#L232) - - CloudflareContent content = new([cloudflareUrlPrefix + fileName]); - var cloudflareContentString = JsonConvert.SerializeObject(content); - try - { - using HttpRequestMessage request = - new(HttpMethod.Delete, $"https://api.cloudflare.com/client/v4/zones/{Program.ConfigJson.Cloudflare.ZoneId}/purge_cache/files"); - request.Content = new StringContent(cloudflareContentString, Encoding.UTF8, "application/json"); - request.Headers.Add("Authorization", $"Bearer {Program.ConfigJson.Cloudflare.Token}"); - - var response = await Program.HttpClient.SendAsync(request); - var responseText = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent( - $"File deleted successfully!\nSuccessfully purged the Cloudflare cache for `{fileName}`!")); - else - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent( - $"File deleted successfully!\nAn API error occured when purging the Cloudflare cache: ```json\n{responseText}```")); - } - catch (Exception e) - { - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent( - $"File deleted successfully!\nAn unexpected error occured when purging the Cloudflare cache: ```json\n{e.Message}```")); - } - } - - [Command("check")] - [Description("Check whether a file exists in your S3 bucket. Uses the S3 API to avoid caching.")] - public static async Task CdnPreview(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("name"), Description("The name (or link) of the file to check.")] - string name) - { - if (Program.DisabledCommands.Contains("cdn")) - { - await CommandHelpers.FailOnMissingInfo(ctx, false); - return; - } - - if (name.Contains(Program.ConfigJson.S3.CdnBaseUrl)) - name = name.Replace(Program.ConfigJson.S3.CdnBaseUrl, "").Trim('/'); - - try - { - await Program.Minio.GetObjectAsync(new GetObjectArgs().WithBucket(Program.ConfigJson.S3.Bucket) - .WithObject(name).WithFile(name)); - } - catch (ObjectNotFoundException) - { - await ctx.RespondAsync("That file doesn't exist!"); - return; - } - catch (Exception ex) - { - await ctx.RespondAsync($"I ran into an error trying to check for that file! {ex.GetType()}: {ex.Message}"); - return; - } - - await ctx.RespondAsync("That file exists!"); - } - - [GeneratedRegex(@"[^/\\&\?#]+\.\w*(?=([\?&#].*$|$))")] - private static partial Regex FileNamePattern(); - - [GeneratedRegex(@"\.\w*(?=([\?&#].*$|$))")] - private static partial Regex FileExtensionPattern(); - - // This code is taken from https://github.com/Sankra/cloudflare-cache-purger/blob/master/main.csx#L197, - // minus some minor changes. - // (Note that I originally found it here: https://github.com/Erisa/Lykos/blob/3335c38/src/Modules/Owner.cs#L313) - private readonly struct CloudflareContent(List urls) - { - public List Files { get; } = urls; - } -} \ No newline at end of file diff --git a/Commands/Owner/HomeServerCommands/HastebinCommands.cs b/Commands/Owner/HomeServerCommands/HastebinCommands.cs deleted file mode 100644 index 0468b02..0000000 --- a/Commands/Owner/HomeServerCommands/HastebinCommands.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace MechanicalMilkshake.Commands.Owner.HomeServerCommands; - -[Command("haste")] -[Description("Commands for managing Hastebin content.")] -public class HasteCommands -{ - private static readonly string HasteApiEndpoint = "https://api.cloudflare.com/client/v4/accounts/{0}/storage/kv/namespaces/{1}/values/documents:{2}"; - - [Command("create")] - [Description("Create a new paste.")] - public static async Task HasteCreate(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("content"), Description("The content of the new paste.")] string content, - [Parameter("key"), Description("The name of the key for the new paste. Accepts formats \"documents:abc\" or \"abc\".")] string key = "") - { - await ctx.DeferResponseAsync(); - - if (Program.DisabledCommands.Contains("haste")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - if (string.IsNullOrWhiteSpace(key)) - key = GenerateKey(); - else - key = key.Replace("documents:", ""); - - var hasteUrl = string.Format(HasteApiEndpoint, Program.ConfigJson.Cloudflare.AccountId, Program.ConfigJson.Hastebin.NamespaceId, key); - var request = new HttpRequestMessage(HttpMethod.Put, hasteUrl); - request.Content = new StringContent(content); - request.Headers.Add("Authorization", $"Bearer {Program.ConfigJson.Cloudflare.Token}"); - - var response = await Program.HttpClient.SendAsync(request); - if (response.IsSuccessStatusCode) - await ctx.FollowupAsync($"Successfully created paste: {Program.ConfigJson.Hastebin.Url}/{key}"); - else - await ctx.FollowupAsync($"Failed to create paste! Cloudflare API returned code {response.StatusCode}."); - } - - [Command("delete")] - [Description("Delete a paste.")] - public static async Task HasteDelete(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("key"), Description("The key of the paste to delete. Accepts formats \"documents:abc\" or \"abc\".")] string key) - { - await ctx.DeferResponseAsync(); - - if (Program.DisabledCommands.Contains("haste")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - key = key.Replace("documents:", ""); - key = key.Replace($"{Program.ConfigJson.Hastebin.Url.Trim('/') + "/"}", ""); - - var hasteUrl = string.Format(HasteApiEndpoint, Program.ConfigJson.Cloudflare.AccountId, Program.ConfigJson.Hastebin.NamespaceId, key); - var request = new HttpRequestMessage(HttpMethod.Delete, hasteUrl); - request.Headers.Add("Authorization", $"Bearer {Program.ConfigJson.Cloudflare.Token}"); - - var response = await Program.HttpClient.SendAsync(request); - if (response.IsSuccessStatusCode) - await ctx.FollowupAsync($"Successfully deleted paste: {Program.ConfigJson.Hastebin.Url}/{key}"); - else - await ctx.FollowupAsync($"Failed to delete paste! Cloudflare API returned code {response.StatusCode}."); - } - - // https://github.com/FloatingMilkshake/starbin/blob/aa4726b/functions/documents/index.ts#L27-L44 - private static string GenerateKey() - { - Random random = new(); - const string vowels = "aeiou"; - const string consonants = "bcdfghjklmnpqrstvwxyz"; - const int size = 6; - - var key = ""; - var start = random.Next(2); - for (var i = 0; i < size; i++) - { - key += i % 2 == start ? consonants[random.Next(consonants.Length)] : vowels[random.Next(vowels.Length)]; - } - - return key; - } -} \ No newline at end of file diff --git a/Commands/Owner/HomeServerCommands/LinkCommands.cs b/Commands/Owner/HomeServerCommands/LinkCommands.cs deleted file mode 100644 index 5b35215..0000000 --- a/Commands/Owner/HomeServerCommands/LinkCommands.cs +++ /dev/null @@ -1,269 +0,0 @@ -namespace MechanicalMilkshake.Commands.Owner.HomeServerCommands; - -[Command("link")] -[Description("Set, update, or delete a short link.")] -public class LinkCommands -{ - [Command("set")] - [Description("Set or update a short link.")] - public static async Task SetWorkerLink(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("key"), Description("Set a custom key for the short link.")] - string key, - [Parameter("url"), Description("The URL the short link should point to.")] - string url) - { - await ctx.DeferResponseAsync(); - - if (Program.DisabledCommands.Contains("wl")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - if (url.Contains('<')) url = url.Replace("<", ""); - - if (url.Contains('>')) url = url.Replace(">", ""); - - if (key[0] == '/') key = key[1..]; - - if (Program.ConfigJson.WorkerLinks.BaseUrl is null) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - "Error: No base URL provided! Make sure the baseUrl field under workerLinks in your config.json file is set.")); - return; - } - - var baseUrl = Program.ConfigJson.WorkerLinks.BaseUrl; - - using HttpClient httpClient = new(); - httpClient.BaseAddress = new Uri(baseUrl); - - var request = key is "null" or "random" or "rand" - ? new HttpRequestMessage(HttpMethod.Post, "") - : new HttpRequestMessage(HttpMethod.Put, key); - - if (Program.ConfigJson.WorkerLinks.Secret is null) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - "Error: No secret provided! Make sure the secret field under workerLinks in your config.json file is set.")); - return; - } - - var secret = Program.ConfigJson.WorkerLinks.Secret; - - request.Headers.Add("Authorization", secret); - request.Headers.Add("URL", url); - - HttpResponseMessage response; - try - { - response = await httpClient.SendAsync(request); - } - catch (Exception ex) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"An exception occurred while trying to send the request! `{ex.GetType()}: {ex.Message}`")); - return; - } - - var httpStatusCode = (int)response.StatusCode; - var httpStatus = response.StatusCode.ToString(); - var responseText = await response.Content.ReadAsStringAsync(); - if (responseText.Length > 1947) - { - var hasteUrl = await HastebinHelpers.UploadToHastebinAsync(responseText); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"Worker responded with code: `{httpStatusCode}`...but the full response is too long to post here. It was uploaded to Hastebin here: {hasteUrl}")); - return; - } - - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"Worker responded with code: `{httpStatusCode}` (`{httpStatus}`)\n```json\n{responseText}\n```")); - } - - [Command("delete")] - [Description("Delete a short link.")] - public static async Task DeleteWorkerLink(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("link"), Description("The key or URL of the short link to delete.")] - string url) - { - await ctx.DeferResponseAsync(); - - if (Program.DisabledCommands.Contains("wl")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - if (url[0] == '/') url = url[1..]; - - var baseUrl = Program.ConfigJson.WorkerLinks.BaseUrl; - - using HttpClient httpClient = new(); - httpClient.BaseAddress = new Uri(baseUrl); - - if (!url.Contains(baseUrl)) url = $"{baseUrl}/{url}"; - - if (Program.ConfigJson.WorkerLinks.Secret is null) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - "Error: No secret provided! Make sure the secret field under workerLinks in your config.json file is set.")); - return; - } - - var secret = Program.ConfigJson.WorkerLinks.Secret; - - HttpRequestMessage request = new(HttpMethod.Delete, url); - request.Headers.Add("Authorization", secret); - - HttpResponseMessage response; - try - { - response = await httpClient.SendAsync(request); - } - catch (Exception ex) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"An exception occurred while trying to send the request! `{ex.GetType()}: {ex.Message}`")); - return; - } - - var httpStatusCode = (int)response.StatusCode; - var httpStatus = response.StatusCode.ToString(); - var responseText = await response.Content.ReadAsStringAsync(); - if (responseText.Length > 1947) - { - var hasteUrl = await HastebinHelpers.UploadToHastebinAsync(responseText); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"Worker responded with code: `{httpStatusCode}`...but the full response is too long to post here. It was uploaded to Hastebin here: {hasteUrl}")); - return; - } - - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"Worker responded with code: `{httpStatusCode}` (`{httpStatus}`)\n```json\n{responseText}\n```")); - } - - [Command("list")] - [Description("List all short links.")] - public static async Task ListWorkerLinks(MechanicalMilkshake.SlashCommandContext ctx, - [Parameter("match_keys"), Description("Optionally filter by key.")] string keyFilter = "", - [Parameter("match_values"), Description("Optionally filter by value.")] string valueFilter = "", - [Parameter("exact_match"), Description("Whether to match filters exactly. Defaults to false.")] bool exactMatch = false) - { - await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent("Working on it...")); - - if (Program.DisabledCommands.Contains("wl")) - { - await CommandHelpers.FailOnMissingInfo(ctx, true); - return; - } - - var requestUri = - $"https://api.cloudflare.com/client/v4/accounts/{Program.ConfigJson.Cloudflare.AccountId}/storage/kv/namespaces/{Program.ConfigJson.WorkerLinks.NamespaceId}/keys"; - HttpRequestMessage request = new(HttpMethod.Get, requestUri); - - request.Headers.Add("X-Auth-Key", Program.ConfigJson.WorkerLinks.ApiKey); - request.Headers.Add("X-Auth-Email", Program.ConfigJson.WorkerLinks.Email); - var response = await Program.HttpClient.SendAsync(request); - - var responseText = await response.Content.ReadAsStringAsync(); - - var parsedResponse = JsonConvert.DeserializeObject(responseText); - - var kvListResponse = ""; - - foreach (var item in parsedResponse.Result) - { - var key = item.Name.Replace("/", "%2F"); - - // Check key filter; if key does not match, skip - if (exactMatch) - { - if (!string.IsNullOrWhiteSpace(keyFilter) && key != keyFilter.Replace("/", "%2F")) continue; - } - else - { - if (!string.IsNullOrWhiteSpace(keyFilter) && !key.Contains(keyFilter.Replace("/", "%2F"))) continue; - } - - var valueRequestUri = - $"https://api.cloudflare.com/client/v4/accounts/{Program.ConfigJson.Cloudflare.AccountId}/storage/kv/namespaces/{Program.ConfigJson.WorkerLinks.NamespaceId}/values/{key}"; - HttpRequestMessage valueRequest = new(HttpMethod.Get, valueRequestUri); - - valueRequest.Headers.Add("X-Auth-Key", Program.ConfigJson.WorkerLinks.ApiKey); - valueRequest.Headers.Add("X-Auth-Email", Program.ConfigJson.WorkerLinks.Email); - var valueResponse = await Program.HttpClient.SendAsync(valueRequest); - - var value = await valueResponse.Content.ReadAsStringAsync(); - value = value.Replace(value, $"<{value}>"); - - // Check value filter; if value does not match, skip - if (exactMatch) - { - if (!string.IsNullOrWhiteSpace(valueFilter) && - value.Replace("<", "").Replace(">", "") != valueFilter) continue; - } - else - { - if (!string.IsNullOrWhiteSpace(valueFilter) && !value.Contains(valueFilter)) continue; - } - - kvListResponse += $"**{item.Name}**: {value}\n\n"; - } - - DiscordEmbedBuilder embed = new() - { - Title = string.IsNullOrWhiteSpace(keyFilter) && string.IsNullOrWhiteSpace(valueFilter) - ? "Link List" - : "Matching Links", - Color = Program.BotColor - }; - - if (string.IsNullOrWhiteSpace(kvListResponse)) - { - embed.Description = "No links matched the specified filters."; - embed.Color = DiscordColor.Red; - - await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(embed)); - return; - } - - try - { - var pages = InteractivityExtension.GeneratePagesInEmbed(kvListResponse, SplitType.Line, embed).ToList(); - - var leftSkip = new DiscordButtonComponent(DiscordButtonStyle.Primary, "leftskip", "<<<"); - var left = new DiscordButtonComponent(DiscordButtonStyle.Primary, "left", "<"); - var right = new DiscordButtonComponent(DiscordButtonStyle.Primary, "right", ">"); - var rightSkip = new DiscordButtonComponent(DiscordButtonStyle.Primary, "rightskip", ">>>"); - var stop = new DiscordButtonComponent(DiscordButtonStyle.Danger, "stop", "Stop"); - - if (pages.Count > 1) - await ctx.Interaction.SendPaginatedResponseAsync(false, ctx.User, pages, - new PaginationButtons - { SkipLeft = leftSkip, Left = left, Right = right, SkipRight = rightSkip, Stop = stop }, - deletion: ButtonPaginationBehavior.DeleteMessage, - asEditResponse: true); - else - await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(embed.WithDescription(kvListResponse))); - } - catch (Exception ex) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent( - $"Hmm, I couldn't send the list of links here!" + - $" You can see the full list on Cloudflare's website [here](https://dash.cloudflare.com/" + - $"{Program.ConfigJson.Cloudflare.AccountId}/workers/kv/namespaces/{Program.ConfigJson.WorkerLinks.NamespaceId})." + - $" Exception details are below.\n```\n{ex.GetType()}: {ex.Message}\n```")); - } - } - - public class CloudflareResponse - { - [JsonProperty("result")] public List Result { get; set; } - } - - public class KvEntry - { - [JsonProperty("name")] public string Name { get; set; } - } -} \ No newline at end of file