diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs
index 067c552255..4e9ac1f8ad 100644
--- a/src/Discord.Net.Core/DiscordConfig.cs
+++ b/src/Discord.Net.Core/DiscordConfig.cs
@@ -132,6 +132,21 @@ public class DiscordConfig
///
public const int MaxAuditLogEntriesPerBatch = 100;
+ ///
+ /// Returns the max number of stickers that can be sent on a message.
+ ///
+ public const int MaxStickersPerMessage = 3;
+
+ ///
+ /// Returns the max numbe of files that can be sent on a message.
+ ///
+ public const int MaxFilesPerMessage = 10;
+
+ ///
+ /// Returns the max number of embeds that can be sent on a message.
+ ///
+ public const int MaxEmbedsPerMessage = 10;
+
///
/// Gets or sets how a request should act in the case of an error, by default.
///
@@ -165,23 +180,23 @@ public class DiscordConfig
///
internal bool DisplayInitialLog { get; set; } = true;
- ///
- /// Gets or sets whether or not rate-limits should use the system clock.
- ///
- ///
- /// If set to false, we will use the X-RateLimit-Reset-After header
- /// to determine when a rate-limit expires, rather than comparing the
- /// X-RateLimit-Reset timestamp to the system time.
- ///
- /// This should only be changed to false if the system is known to have
- /// a clock that is out of sync. Relying on the Reset-After header will
- /// incur network lag.
- ///
- /// Regardless of this property, we still rely on the system's wall-clock
- /// to determine if a bucket is rate-limited; we do not use any monotonic
- /// clock. Your system will still need a stable clock.
- ///
- public bool UseSystemClock { get; set; } = true;
+ ///
+ /// Gets or sets whether or not rate-limits should use the system clock.
+ ///
+ ///
+ /// If set to false, we will use the X-RateLimit-Reset-After header
+ /// to determine when a rate-limit expires, rather than comparing the
+ /// X-RateLimit-Reset timestamp to the system time.
+ ///
+ /// This should only be changed to false if the system is known to have
+ /// a clock that is out of sync. Relying on the Reset-After header will
+ /// incur network lag.
+ ///
+ /// Regardless of this property, we still rely on the system's wall-clock
+ /// to determine if a bucket is rate-limited; we do not use any monotonic
+ /// clock. Your system will still need a stable clock.
+ ///
+ public bool UseSystemClock { get; set; } = true;
///
/// Gets or sets whether or not the internal experation check uses the system date
diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
index 60a7c75757..ed1fb264d6 100644
--- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
+++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
@@ -38,6 +38,16 @@ public interface IMessageChannel : IChannel
///
Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
///
+ /// Sends a message to this message channel.
+ ///
+ /// The created from a .
+ /// The options to be used when sending the request.
+ ///
+ /// A task that represents an asynchronous send operation for delivering the message. The task result
+ /// contains the sent message.
+ ///
+ Task SendMessageAsync(Message message, RequestOptions options = null);
+ ///
/// Sends a file to this message channel with an optional caption.
///
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
index 7becca0e07..e0597f6f39 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
@@ -92,7 +92,7 @@ internal void AddComponent(IMessageComponent component, int row)
/// The max values of the placeholder.
/// Whether or not the menu is disabled.
/// The row to add the menu to.
- ///
+ /// The current builder.
public ComponentBuilder WithSelectMenu(string customId, List options,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0)
{
diff --git a/src/Discord.Net.Core/Entities/Messages/Builders/CodeLanguage.cs b/src/Discord.Net.Core/Entities/Messages/Builders/CodeLanguage.cs
new file mode 100644
index 0000000000..7cfddbde6f
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Messages/Builders/CodeLanguage.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents a language in which codeblocks can be formatted.
+ ///
+ public struct CodeLanguage
+ {
+ ///
+ /// Gets the tag of the language.
+ ///
+ public string Tag { get; }
+
+ ///
+ /// Gets the name of the language. if this was constructed with no name provided.
+ ///
+ public string Name { get; } = string.Empty;
+
+ ///
+ /// Gets the CSharp language format.
+ ///
+ public static readonly CodeLanguage CSharp = new("cs", "csharp");
+
+ ///
+ /// Gets the Javascript language format.
+ ///
+ public static readonly CodeLanguage JavaScript = new("js", "javascript");
+
+ ///
+ /// Gets the XML language format.
+ ///
+ public static readonly CodeLanguage XML = new("xml", "xml");
+
+ ///
+ /// Gets the HTML language format.
+ ///
+ public static readonly CodeLanguage HTML = new("html", "html");
+
+ ///
+ /// Gets the CSS markdown format.
+ ///
+ public static readonly CodeLanguage CSS = new("css", "css");
+
+ ///
+ /// Gets a language format that represents none.
+ ///
+ public static readonly CodeLanguage None = new("", "none");
+
+ ///
+ /// Creates a new language format with name & tag.
+ ///
+ /// The tag with which markdown will be formatted.
+ /// The name of the language.
+ public CodeLanguage(string tag, string name)
+ {
+ Tag = tag;
+ Name = name;
+ }
+
+ ///
+ /// Creates a new language format with a tag.
+ ///
+ /// The tag with which markdown will be formatted.
+ public CodeLanguage(string tag)
+ => Tag = tag;
+
+ ///
+ /// Gets the tag of the language.
+ ///
+ ///
+ public static implicit operator string(CodeLanguage language)
+ => language.Tag;
+
+ ///
+ /// Gets a language based on the tag.
+ ///
+ ///
+ public static implicit operator CodeLanguage(string tag)
+ => new(tag);
+
+ ///
+ /// Creates markdown format for this language.
+ ///
+ /// The input string to format.
+ /// A markdown formatted code-block with this language.
+ public string ToMarkdown(string input)
+ => $"```{Tag}\n{input}\n```";
+
+ ///
+ /// Gets the tag of the language.
+ ///
+ ///
+ public override string ToString()
+ => $"{Tag}";
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Messages/Builders/HeaderFormat.cs b/src/Discord.Net.Core/Entities/Messages/Builders/HeaderFormat.cs
new file mode 100644
index 0000000000..029110a1b1
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Messages/Builders/HeaderFormat.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents the format in which a markdown header should be presented.
+ ///
+ public readonly struct HeaderFormat
+ {
+ public string Format { get; }
+
+ ///
+ /// The biggest header type.
+ ///
+ public static readonly HeaderFormat H1 = new("#");
+
+ ///
+ /// An above-average sized header.
+ ///
+ public static readonly HeaderFormat H2 = new("##");
+
+ ///
+ /// An average-sized header.
+ ///
+ public static readonly HeaderFormat H3 = new("###");
+
+ ///
+ /// A subheader.
+ ///
+ public static readonly HeaderFormat H4 = new("####");
+
+ ///
+ /// A smaller subheader.
+ ///
+ public static readonly HeaderFormat H5 = new("#####");
+
+ ///
+ /// Slightly bigger than regular bold markdown.
+ ///
+ public static readonly HeaderFormat H6 = new("######");
+
+ private HeaderFormat(string format)
+ => Format = format;
+
+ ///
+ /// Formats this header into markdown, appending provided string.
+ ///
+ /// The string to turn into a header.
+ /// A markdown formatted header title.
+ public string ToMarkdown(string input)
+ => $"{Format} {input}";
+
+ ///
+ /// Gets the markdown format for this header.
+ ///
+ /// The markdown format for this header.
+ public override string ToString()
+ => $"{Format}";
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Messages/Builders/MessageBuilder.cs b/src/Discord.Net.Core/Entities/Messages/Builders/MessageBuilder.cs
new file mode 100644
index 0000000000..be740fe268
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Messages/Builders/MessageBuilder.cs
@@ -0,0 +1,323 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents a generic message builder that can build 's.
+ ///
+ public class MessageBuilder
+ {
+ private readonly List _files;
+
+ private List _stickers;
+ private List _embeds;
+
+ ///
+ /// Gets or sets the content of this message
+ ///
+ public TextBuilder Content { get; set; }
+
+ ///
+ /// Gets or sets whether or not this message is TTS.
+ ///
+ public bool IsTTS { get; set; }
+
+ ///
+ /// Gets or sets the allowed mentions of this message.
+ ///
+ public AllowedMentions AllowedMentions { get; set; }
+
+ ///
+ /// Gets or sets the message reference (reply to) of this message.
+ ///
+ public MessageReference MessageReference { get; set; }
+
+ ///
+ /// Gets or sets the components of this message.
+ ///
+ public ComponentBuilder Components { get; set; }
+
+ ///
+ /// Gets or sets the embeds of this message.
+ ///
+ public List Embeds
+ {
+ get => _embeds;
+ set
+ {
+ if (value?.Count > DiscordConfig.MaxEmbedsPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(value), $"Embed count must be less than or equal to {DiscordConfig.MaxEmbedsPerMessage}");
+
+ _embeds = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the stickers sent with this message.
+ ///
+ public List Stickers
+ {
+ get => _stickers;
+ set
+ {
+ if (value?.Count > DiscordConfig.MaxStickersPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(value), $"Sticker count must be less than or equal to {DiscordConfig.MaxStickersPerMessage}");
+
+ _stickers = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the files sent with this message.
+ ///
+ public List Files
+ {
+ get => _files;
+ set
+ {
+ if(value?.Count > DiscordConfig.MaxFilesPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(value), $"File count must be less than or equal to {DiscordConfig.MaxFilesPerMessage}");
+ }
+ }
+
+ ///
+ /// Gets or sets the message flags.
+ ///
+ public MessageFlags Flags { get; set; }
+
+ ///
+ /// Creates a new based on the value of .
+ ///
+ /// The message content to create this from.
+ public MessageBuilder(string content)
+ {
+ Content = new TextBuilder(content);
+ }
+
+ public MessageBuilder()
+ {
+ _embeds = new();
+ _stickers = new();
+ _files = new();
+
+ Components = new();
+ }
+
+ ///
+ /// Sets the of this message.
+ ///
+ /// The content of the message.
+ /// The current builder.
+ public virtual MessageBuilder WithContent(TextBuilder builder)
+ {
+ Content = builder;
+ return this;
+ }
+
+ ///
+ /// Sets the of this message.
+ ///
+ /// whether or not this message is tts.
+ /// The current builder.
+ public virtual MessageBuilder WithTTS(bool isTTS)
+ {
+ IsTTS = isTTS;
+ return this;
+ }
+
+ ///
+ /// Sets the of this message.
+ ///
+ /// The embeds to be put in this message.
+ /// The current builder.
+ /// A message can only contain a maximum of embeds.
+ public virtual MessageBuilder WithEmbeds(params EmbedBuilder[] embeds)
+ {
+ Embeds = new(embeds);
+ return this;
+ }
+
+ ///
+ /// Adds an embed builder to the current .
+ ///
+ /// The embed builder to add
+ /// The current builder.
+ /// A message can only contain a maximum of embeds.
+ public virtual MessageBuilder AddEmbed(EmbedBuilder embed)
+ {
+ if (_embeds?.Count >= DiscordConfig.MaxEmbedsPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(embed.Length), $"A message can only contain a maximum of {DiscordConfig.MaxEmbedsPerMessage} embeds");
+
+ _embeds ??= new();
+
+ _embeds.Add(embed);
+
+ return this;
+ }
+
+ ///
+ /// Sets the for this message.
+ ///
+ /// The allowed mentions for this message.
+ /// The current builder.
+ public virtual MessageBuilder WithAllowedMentions(AllowedMentions allowedMentions)
+ {
+ AllowedMentions = allowedMentions;
+ return this;
+ }
+
+ ///
+ /// Sets the for this message.
+ ///
+ /// The message reference (reply-to) for this message.
+ /// The current builder.
+ public virtual MessageBuilder WithMessageReference(MessageReference reference)
+ {
+ MessageReference = reference;
+ return this;
+ }
+
+ ///
+ /// Sets the for this current message.
+ ///
+ /// The message to set as a reference.
+ /// The current builder.
+ public virtual MessageBuilder WithMessageReference(IMessage message)
+ {
+ if (message != null)
+ MessageReference = new MessageReference(message.Id, message.Channel?.Id, ((IGuildChannel)message.Channel)?.GuildId);
+ return this;
+ }
+
+ ///
+ /// Sets the for this message.
+ ///
+ /// The component builder to set.
+ /// The current builder.
+ public virtual MessageBuilder WithComponentBuilder(ComponentBuilder builder)
+ {
+ Components = builder;
+ return this;
+ }
+
+ ///
+ /// Adds a button to the current .
+ ///
+ /// The button builder to add.
+ /// The optional row to place the button on.
+ /// The current builder.
+ public virtual MessageBuilder WithButton(ButtonBuilder button, int row = 0)
+ {
+ Components ??= new();
+ Components.WithButton(button, row);
+ return this;
+ }
+
+ ///
+ /// Adds a button to the current .
+ ///
+ /// The label text for the newly added button.
+ /// The style of this newly added button.
+ /// A to be used with this button.
+ /// The custom id of the newly added button.
+ /// A URL to be used only if the is a Link.
+ /// Whether or not the newly created button is disabled.
+ /// The row the button should be placed on.
+ /// The current builder.
+ public virtual MessageBuilder WithButton(
+ string label = null,
+ string customId = null,
+ ButtonStyle style = ButtonStyle.Primary,
+ IEmote emote = null,
+ string url = null,
+ bool disabled = false,
+ int row = 0)
+ {
+ Components ??= new();
+ Components.WithButton(label, customId, style, emote, url, disabled, row);
+ return this;
+ }
+
+ ///
+ /// Adds a select menu to the current .
+ ///
+ /// The select menu builder to add.
+ /// The optional row to place the select menu on.
+ /// The current builder.
+ public virtual MessageBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0)
+ {
+ Components ??= new();
+ Components.WithSelectMenu(menu, row);
+ return this;
+ }
+
+ ///
+ /// Adds a select menu to the current .
+ ///
+ /// The custom id of the menu.
+ /// The options of the menu.
+ /// The placeholder of the menu.
+ /// The min values of the placeholder.
+ /// The max values of the placeholder.
+ /// Whether or not the menu is disabled.
+ /// The row to add the menu to.
+ /// The current builder.
+ public virtual MessageBuilder WithSelectMenu(string customId, List options,
+ string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0)
+ {
+ Components ??= new();
+ Components.WithSelectMenu(customId, options, placeholder, minValues, maxValues, disabled, row);
+ return this;
+ }
+
+ ///
+ /// Sets the of this message.
+ ///
+ /// The file collection to set.
+ /// The current builder.
+ public virtual MessageBuilder WithFiles(IEnumerable files)
+ {
+ Files = new List(files);
+ return this;
+ }
+
+ ///
+ /// Adds a file to the current collection .
+ ///
+ /// The file to add.
+ /// The current builder.
+ public virtual MessageBuilder AddFile(FileAttachment file)
+ {
+ Files.Add(file);
+ return this;
+ }
+
+ ///
+ /// Builds this message builder to a that can be sent across the discord api.
+ ///
+ /// A that can be sent to the discord api.
+ public Message Build()
+ {
+ var embeds = _embeds != null && _embeds.Count > 0
+ ? _embeds.Select(x => x.Build()).ToImmutableArray()
+ : ImmutableArray