diff --git a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs index 8d470b611..92c396e51 100644 --- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs +++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs @@ -1,459 +1,459 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Entities; /// /// Constructs a Message to be sent. /// public sealed class DiscordMessageBuilder { /// /// Gets or Sets the Message to be sent. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Gets or sets the embed for the builder. This will always set the builder to have one embed. /// public DiscordEmbed Embed { get => this._embeds.Count > 0 ? this._embeds[0] : null; set { this._embeds.Clear(); this._embeds.Add(value); } } /// /// Gets the Sticker to be send. /// public DiscordSticker Sticker { get; set; } /// /// Gets the Embeds to be sent. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Gets or Sets if the message should be TTS. /// public bool IsTts { get; set; } /// /// Whether to keep previous attachments. /// internal bool? KeepAttachmentsInternal; /// /// Gets the Allowed Mentions for the message to be sent. /// public List Mentions { get; private set; } /// /// Gets the Files to be sent in the Message. /// public IReadOnlyCollection Files => this.FilesInternal; internal readonly List FilesInternal = new(); /// /// Gets the components that will be attached to the message. /// public IReadOnlyList Components => this.ComponentsInternal; internal readonly List ComponentsInternal = new(5); /// /// Gets the Attachments to be sent in the Message. /// public IReadOnlyList Attachments => this.AttachmentsInternal; internal readonly List AttachmentsInternal = new(); /// /// Gets the Reply Message ID. /// public ulong? ReplyId { get; private set; } /// /// Gets if the Reply should mention the user. /// public bool MentionOnReply { get; private set; } /// /// Gets if the embeds should be suppressed. /// public bool Suppressed { get; private set; } /// /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. /// If set to false, invalid replies are send as a regular message. /// Defaults to false. /// public bool FailOnInvalidReply { get; set; } /// /// Sets the Content of the Message. /// /// The content to be set. /// The current builder to be chained. public DiscordMessageBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds a sticker to the message. Sticker must be from current guild. /// /// The sticker to add. /// The current builder to be chained. public DiscordMessageBuilder WithSticker(DiscordSticker sticker) { this.Sticker = sticker; return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. /// No components were passed. public DiscordMessageBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordMessageBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this.ComponentsInternal.Add(ar); return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. /// No components were passed. public DiscordMessageBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this.ComponentsInternal.Add(comp); return this; } /// /// Sets if the message should be TTS. /// /// If TTS should be set. /// The current builder to be chained. public DiscordMessageBuilder HasTts(bool isTts) { this.IsTts = isTts; return this; } /// /// Sets the embed for the current builder. /// /// The embed that should be set. /// The current builder to be chained. public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) { if (embed == null) return this; this.Embed = embed; return this; } /// /// Appends an embed to the current builder. /// /// The embed that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbed(DiscordEmbed embed) { if (embed == null) return this; //Providing null embeds will produce a 400 response from Discord.// this._embeds.Add(embed); return this; } /// /// Appends several embeds to the current builder. /// /// The embeds that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mention that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) { if (this.Mentions != null) this.Mentions.Add(allowedMention); else this.Mentions = new List { allowedMention }; return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mentions that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) { if (this.Mentions != null) this.Mentions.AddRange(allowedMentions); else this.Mentions = allowedMentions.ToList(); return this; } /// /// Sets if the message has files to be sent. /// /// The fileName that the file should be sent as. /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The current builder to be chained. public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.FileName == fileName)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(fileName, stream, stream.Position, description: description)); else this.FilesInternal.Add(new DiscordMessageFile(fileName, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The current builder to be chained. public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this.FilesInternal.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Files that should be sent. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The current builder to be chained. public DiscordMessageBuilder WithFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this.FilesInternal.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this.FilesInternal.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Modifies the given attachments on edit. /// /// Attachments to edit. /// public DiscordMessageBuilder ModifyAttachments(IEnumerable attachments) { this.AttachmentsInternal.AddRange(attachments); return this; } /// /// Whether to keep the message attachments, if new ones are added. /// /// public DiscordMessageBuilder KeepAttachments(bool keep) { this.KeepAttachmentsInternal = keep; return this; } /// /// Sets if the message is a reply /// /// The ID of the message to reply to. /// If we should mention the user in the reply. /// Whether sending a reply that references an invalid message should be /// The current builder to be chained. public DiscordMessageBuilder WithReply(ulong messageId, bool mention = false, bool failOnInvalidReply = false) { this.ReplyId = messageId; this.MentionOnReply = mention; this.FailOnInvalidReply = failOnInvalidReply; if (mention) { this.Mentions ??= new List(); this.Mentions.Add(new RepliedUserMention()); } return this; } /// /// Sends the Message to a specific channel /// /// The channel the message should be sent to. /// The current builder to be chained. public Task SendAsync(DiscordChannel channel) => channel.SendMessageAsync(this); /// /// Sends the modified message. /// Note: Message replies cannot be modified. To clear the reply, simply pass to . /// /// The original Message to modify. /// The current builder to be chained. public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); /// /// Clears all message components on this builder. /// public void ClearComponents() => this.ComponentsInternal.Clear(); /// /// Allows for clearing the Message Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this.Mentions = null; this.FilesInternal.Clear(); this.ReplyId = null; this.MentionOnReply = false; this.ComponentsInternal.Clear(); this.Suppressed = false; this.Sticker = null; this.AttachmentsInternal.Clear(); this.KeepAttachmentsInternal = false; } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. internal void Validate(bool isModify = false) { if (this._embeds.Count > 10) throw new ArgumentException("A message can only have up to 10 embeds."); if (!isModify) { - if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && this.Sticker is null) - throw new ArgumentException("You must specify content, an embed, a sticker or at least one file."); + if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && this.Sticker is null && (!this.Embeds?.Any() ?? true)) + throw new ArgumentException("You must specify content, an embed, a sticker, a component or at least one file."); if (this.Components.Count > 5) throw new InvalidOperationException("You can only have 5 action rows per message."); if (this.Components.Any(c => c.Components.Count > 5)) throw new InvalidOperationException("Action rows can only have 5 components"); } } } diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs index 1f0ae1792..c1d110702 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs @@ -1,462 +1,462 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; namespace DisCatSharp.Entities; /// /// Constructs ready-to-send webhook requests. /// public sealed class DiscordWebhookBuilder { /// /// Username to use for this webhook request. /// public Optional Username { get; set; } /// /// Avatar url to use for this webhook request. /// public Optional AvatarUrl { get; set; } /// /// Whether this webhook request is text-to-speech. /// public bool IsTts { get; set; } /// /// Message to send on this webhook request. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Name of the new thread. /// Only works if the webhook is send in a . /// public string ThreadName { get; set; } /// /// Whether to keep previous attachments. /// internal bool? KeepAttachmentsInternal; /// /// Embeds to send on this webhook request. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this webhook request. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Mentions to send on this webhook request. /// public IReadOnlyList Mentions => this._mentions; private readonly List _mentions = new(); /// /// Gets the components. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// Attachments to keep on this webhook request. /// public IEnumerable Attachments => this.AttachmentsInternal; internal readonly List AttachmentsInternal = new(); /// /// Constructs a new empty webhook request builder. /// public DiscordWebhookBuilder() { } // I still see no point in initializing collections with empty collections. // /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. /// No components were passed. public DiscordWebhookBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the builder /// /// The rows of components to add, holding up to five each. /// public DiscordWebhookBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. /// No components were passed. public DiscordWebhookBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this._components.Add(comp); return this; } /// /// Sets the username for this webhook builder. /// /// Username of the webhook public DiscordWebhookBuilder WithUsername(string username) { this.Username = username; return this; } /// /// Sets the avatar of this webhook builder from its url. /// /// Avatar url of the webhook public DiscordWebhookBuilder WithAvatarUrl(string avatarUrl) { this.AvatarUrl = avatarUrl; return this; } /// /// Indicates if the webhook must use text-to-speech. /// /// Text-to-speech public DiscordWebhookBuilder WithTts(bool tts) { this.IsTts = tts; return this; } /// /// Sets the message to send at the execution of the webhook. /// /// Message to send. public DiscordWebhookBuilder WithContent(string content) { this.Content = content; return this; } /// /// Sets the thread name to create at the execution of the webhook. /// Only works for . /// /// The thread name. public DiscordWebhookBuilder WithThreadName(string name) { this.ThreadName = name; return this; } /// /// Adds an embed to send at the execution of the webhook. /// /// Embed to add. public DiscordWebhookBuilder AddEmbed(DiscordEmbed embed) { if (embed != null) this._embeds.Add(embed); return this; } /// /// Adds the given embeds to send at the execution of the webhook. /// /// Embeds to add. public DiscordWebhookBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to send at the execution of the webhook. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to send at the execution of the webhook. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. public DiscordWebhookBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Modifies the given attachments on edit. /// /// Attachments to edit. /// public DiscordWebhookBuilder ModifyAttachments(IEnumerable attachments) { this.AttachmentsInternal.AddRange(attachments); return this; } /// /// Whether to keep the message attachments, if new ones are added. /// /// public DiscordWebhookBuilder KeepAttachments(bool keep) { this.KeepAttachmentsInternal = keep; return this; } /// /// Adds the mention to the mentions to parse, etc. at the execution of the webhook. /// /// Mention to add. public DiscordWebhookBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. at the execution of the webhook. /// /// Mentions to add. public DiscordWebhookBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Executes a webhook. /// /// The webhook that should be executed. /// The message sent public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this).ConfigureAwait(false); /// /// Executes a webhook. /// /// The webhook that should be executed. /// Target thread id. /// The message sent public async Task SendAsync(DiscordWebhook webhook, ulong threadId) => await webhook.ExecuteAsync(this, threadId.ToString()).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await this.ModifyAsync(webhook, message.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// Target thread. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message, DiscordThreadChannel thread) => await this.ModifyAsync(webhook, message.Id, thread.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// Target thread id. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId, ulong threadId) => await webhook.EditMessageAsync(messageId, this, threadId.ToString()).ConfigureAwait(false); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Webhook Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this._mentions.Clear(); this._files.Clear(); this.AttachmentsInternal.Clear(); this._components.Clear(); this.KeepAttachmentsInternal = false; this.ThreadName = null; } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. /// Tells the method to perform the follow up message validation. /// Tells the method to perform the interaction response validation. internal void Validate(bool isModify = false, bool isFollowup = false, bool isInteractionResponse = false) { if (isModify) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a message."); } else if (isFollowup) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a follow up message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a follow up message."); } else if (isInteractionResponse) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of an interaction response."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of an interaction response."); } else { - if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) - throw new ArgumentException("You must specify content, an embed, or at least one file."); + if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any() && !this.Components.Any()) + throw new ArgumentException("You must specify content, an embed, a component, or at least one file."); } } }