diff --git a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
index f8a838a3c..46114490f 100644
--- a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
+++ b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
@@ -1,197 +1,206 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Threading.Tasks;
using Newtonsoft.Json;
namespace DisCatSharp.Entities
{
///
/// Represents an interaction that was invoked.
///
public sealed class DiscordInteraction : SnowflakeObject
{
///
/// Gets the type of interaction invoked.
///
[JsonProperty("type")]
public InteractionType Type { get; internal set; }
///
/// Gets the command data for this interaction.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionData Data { get; internal set; }
///
/// Gets the Id of the guild that invoked this interaction.
///
[JsonIgnore]
public ulong? GuildId { get; internal set; }
///
/// Gets the guild that invoked this interaction.
///
[JsonIgnore]
public DiscordGuild Guild
=> (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId);
///
/// Gets the Id of the channel that invoked this interaction.
///
[JsonIgnore]
public ulong ChannelId { get; internal set; }
///
/// Gets the channel that invoked this interaction.
///
[JsonIgnore]
public DiscordChannel Channel
=> (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThread(this.ChannelId) ?? (this.Guild == null ? new DiscordDmChannel { Id = this.ChannelId, Type = ChannelType.Private, Discord = this.Discord } : null);
///
/// Gets the user that invoked this interaction.
/// This can be cast to a if created in a guild.
///
[JsonIgnore]
public DiscordUser User { get; internal set; }
///
/// Gets the continuation token for responding to this interaction.
///
[JsonProperty("token")]
public string Token { get; internal set; }
///
/// Gets the version number for this interaction type.
///
[JsonProperty("version")]
public int Version { get; internal set; }
///
/// Gets the ID of the application that created this interaction.
///
[JsonProperty("application_id")]
public ulong ApplicationId { get; internal set; }
///
/// The message this interaction was created with, if any.
///
[JsonProperty("message")]
internal DiscordMessage Message { get; set; }
///
/// Creates a response to this interaction.
///
/// The type of the response.
/// The data, if any, to send.
public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) =>
this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder);
///
/// Creates a modal response to this interaction.
///
/// The data to send.
public Task CreateInteractionModalResponseAsync(DiscordInteractionModalBuilder builder) =>
this.Discord.ApiClient.CreateInteractionModalResponseAsync(this.Id, this.Token, InteractionResponseType.Modal, builder);
///
/// Gets the original interaction response.
///
/// The origingal message that was sent. This does not work on ephemeral messages.
public Task GetOriginalResponseAsync() =>
this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token);
///
/// Edits the original interaction response.
///
/// The webhook builder.
/// The edited .
public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder)
{
builder.Validate(isInteractionResponse: true);
- if (builder._keepAttachments)
+ if (builder._keepAttachments.HasValue && builder._keepAttachments.Value)
{
var attachments = this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token).Result.Attachments;
if (attachments?.Count > 0)
{
builder._attachments.AddRange(attachments);
}
}
+ else if (builder._keepAttachments.HasValue)
+ {
+ builder._attachments.Clear();
+ }
return await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false);
}
///
/// Deletes the original interaction response.
/// >
public Task DeleteOriginalResponseAsync() =>
this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token);
///
/// Creates a follow up message to this interaction.
///
/// The webhook builder.
/// The created .
public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder)
{
builder.Validate();
return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false);
}
///
/// Gets a follow up message.
///
/// The id of the follow up message.
public Task GetFollowupMessageAsync(ulong messageId) =>
this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId);
///
/// Edits a follow up message.
///
/// The id of the follow up message.
/// The webhook builder.
/// The edited .
public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder)
{
builder.Validate(isFollowup: true);
- if (builder._keepAttachments)
+
+ if (builder._keepAttachments.HasValue && builder._keepAttachments.Value)
{
var attachments = this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId).Result.Attachments;
if (attachments?.Count > 0)
{
builder._attachments.AddRange(attachments);
}
}
+ else if (builder._keepAttachments.HasValue)
+ {
+ builder._attachments.Clear();
+ }
return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder).ConfigureAwait(false);
}
///
/// Deletes a follow up message.
///
/// The id of the follow up message.
public Task DeleteFollowupMessageAsync(ulong messageId) =>
this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId);
}
}
diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs
index d90df282f..6e124e012 100644
--- a/DisCatSharp/Entities/Message/DiscordMessage.cs
+++ b/DisCatSharp/Entities/Message/DiscordMessage.cs
@@ -1,888 +1,888 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace DisCatSharp.Entities
{
///
/// Represents a Discord text message.
///
public class DiscordMessage : SnowflakeObject, IEquatable
{
///
/// Initializes a new instance of the class.
///
internal DiscordMessage()
{
this._attachmentsLazy = new Lazy>(() => new ReadOnlyCollection(this._attachments));
this._embedsLazy = new Lazy>(() => new ReadOnlyCollection(this._embeds));
this._mentionedChannelsLazy = new Lazy>(() => this._mentionedChannels != null
? new ReadOnlyCollection(this._mentionedChannels)
: Array.Empty());
this._mentionedRolesLazy = new Lazy>(() => this._mentionedRoles != null ? new ReadOnlyCollection(this._mentionedRoles) : Array.Empty());
this._mentionedUsersLazy = new Lazy>(() => new ReadOnlyCollection(this._mentionedUsers));
this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this._reactions));
this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this._stickers));
this._jumpLink = new Lazy(() =>
{
var gid = this.Channel != null
? this.Channel is DiscordDmChannel ? "@me" : this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture)
: this.InternalThread.GuildId.Value.ToString(CultureInfo.InvariantCulture);
var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture);
var mid = this.Id.ToString(CultureInfo.InvariantCulture);
return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}");
});
}
///
/// Initializes a new instance of the class.
///
/// The other.
internal DiscordMessage(DiscordMessage other)
: this()
{
this.Discord = other.Discord;
this._attachments = other._attachments; // the attachments cannot change, thus no need to copy and reallocate.
this._embeds = new List(other._embeds);
if (other._mentionedChannels != null)
this._mentionedChannels = new List(other._mentionedChannels);
if (other._mentionedRoles != null)
this._mentionedRoles = new List(other._mentionedRoles);
if (other._mentionedRoleIds != null)
this._mentionedRoleIds = new List(other._mentionedRoleIds);
this._mentionedUsers = new List(other._mentionedUsers);
this._reactions = new List(other._reactions);
this._stickers = new List(other._stickers);
this.Author = other.Author;
this.ChannelId = other.ChannelId;
this.Content = other.Content;
this.EditedTimestampRaw = other.EditedTimestampRaw;
this.Id = other.Id;
this.IsTTS = other.IsTTS;
this.MessageType = other.MessageType;
this.Pinned = other.Pinned;
this.TimestampRaw = other.TimestampRaw;
this.WebhookId = other.WebhookId;
}
///
/// Gets the channel in which the message was sent.
///
[JsonIgnore]
public DiscordChannel Channel
{
get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel;
internal set => this._channel = value;
}
private DiscordChannel _channel;
///
/// Gets the thread in which the message was sent.
///
[JsonIgnore]
private DiscordThreadChannel InternalThread
{
get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread;
set => this._thread = value;
}
private DiscordThreadChannel _thread;
///
/// Gets the ID of the channel in which the message was sent.
///
[JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ChannelId { get; internal set; }
///
/// Gets the components this message was sent with.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Components { get; internal set; }
///
/// Gets the user or member that sent the message.
///
[JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)]
public DiscordUser Author { get; internal set; }
///
/// Gets the message's content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; internal set; }
///
/// Gets the message's creation timestamp.
///
[JsonIgnore]
public DateTimeOffset Timestamp
=> !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : this.CreationTimestamp;
///
/// Gets the message's creation timestamp as raw string.
///
[JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string TimestampRaw { get; set; }
///
/// Gets the message's edit timestamp. Will be null if the message was not edited.
///
[JsonIgnore]
public DateTimeOffset? EditedTimestamp
=> !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
(DateTimeOffset?)dto : null;
///
/// Gets the message's edit timestamp as raw string. Will be null if the message was not edited.
///
[JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string EditedTimestampRaw { get; set; }
///
/// Gets whether this message was edited.
///
[JsonIgnore]
public bool IsEdited
=> !string.IsNullOrWhiteSpace(this.EditedTimestampRaw);
///
/// Gets whether the message is a text-to-speech message.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool IsTTS { get; internal set; }
///
/// Gets whether the message mentions everyone.
///
[JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)]
public bool MentionEveryone { get; internal set; }
///
/// Gets users or members mentioned by this message.
///
[JsonIgnore]
public IReadOnlyList MentionedUsers
=> this._mentionedUsersLazy.Value;
[JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)]
internal List _mentionedUsers;
[JsonIgnore]
internal readonly Lazy> _mentionedUsersLazy;
// TODO this will probably throw an exception in DMs since it tries to wrap around a null List...
// this is probably low priority but need to find out a clean way to solve it...
///
/// Gets roles mentioned by this message.
///
[JsonIgnore]
public IReadOnlyList MentionedRoles
=> this._mentionedRolesLazy.Value;
[JsonIgnore]
internal List _mentionedRoles;
[JsonProperty("mention_roles")]
internal List _mentionedRoleIds;
[JsonIgnore]
private readonly Lazy> _mentionedRolesLazy;
///
/// Gets channels mentioned by this message.
///
[JsonIgnore]
public IReadOnlyList MentionedChannels
=> this._mentionedChannelsLazy.Value;
[JsonIgnore]
internal List _mentionedChannels;
[JsonIgnore]
private readonly Lazy> _mentionedChannelsLazy;
///
/// Gets files attached to this message.
///
[JsonIgnore]
public IReadOnlyList Attachments
=> this._attachmentsLazy.Value;
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
internal List _attachments = new();
[JsonIgnore]
private readonly Lazy> _attachmentsLazy;
///
/// Gets embeds attached to this message.
///
[JsonIgnore]
public IReadOnlyList Embeds
=> this._embedsLazy.Value;
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
internal List _embeds = new();
[JsonIgnore]
private readonly Lazy> _embedsLazy;
///
/// Gets reactions used on this message.
///
[JsonIgnore]
public IReadOnlyList Reactions
=> this._reactionsLazy.Value;
[JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)]
internal List _reactions = new();
[JsonIgnore]
private readonly Lazy> _reactionsLazy;
/*
///
/// Gets the nonce sent with the message, if the message was sent by the client.
///
[JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)]
public ulong? Nonce { get; internal set; }
*/
///
/// Gets whether the message is pinned.
///
[JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)]
public bool Pinned { get; internal set; }
///
/// Gets the id of the webhook that generated this message.
///
[JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? WebhookId { get; internal set; }
///
/// Gets the type of the message.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public MessageType? MessageType { get; internal set; }
///
/// Gets the message activity in the Rich Presence embed.
///
[JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageActivity Activity { get; internal set; }
///
/// Gets the message application in the Rich Presence embed.
///
[JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageApplication Application { get; internal set; }
///
/// Gets the message application id in the Rich Presence embed.
///
[JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ApplicationId { get; internal set; }
///
/// Gets the internal reference.
///
[JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)]
internal InternalDiscordMessageReference? InternalReference { get; set; }
///
/// Gets the original message reference from the crossposted message.
///
[JsonIgnore]
public DiscordMessageReference Reference
=> this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null;
///
/// Gets the bitwise flags for this message.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public MessageFlags? Flags { get; internal set; }
///
/// Gets whether the message originated from a webhook.
///
[JsonIgnore]
public bool WebhookMessage
=> this.WebhookId != null;
///
/// Gets the jump link to this message.
///
[JsonIgnore]
public Uri JumpLink => this._jumpLink.Value;
private readonly Lazy _jumpLink;
///
/// Gets stickers for this message.
///
[JsonIgnore]
public IReadOnlyList Stickers
=> this._stickersLazy.Value;
[JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)]
internal List _stickers = new();
[JsonIgnore]
private readonly Lazy> _stickersLazy;
///
/// Gets the guild id.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
internal ulong? GuildId { get; set; }
///
/// Gets the message object for the referenced message
///
[JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessage ReferencedMessage { get; internal set; }
///
/// Gets whether the message is a response to an interaction.
///
[JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageInteraction Interaction { get; internal set; }
///
/// Gets the thread that was started from this message.
///
[JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)]
public DiscordThreadChannel Thread { get; internal set; }
///
/// Build the message reference.
///
internal DiscordMessageReference InternalBuildMessageReference()
{
var client = this.Discord as DiscordClient;
var guildId = this.InternalReference.Value.GuildId;
var channelId = this.InternalReference.Value.ChannelId;
var messageId = this.InternalReference.Value.MessageId;
var reference = new DiscordMessageReference();
if (guildId.HasValue)
reference.Guild = client._guilds.TryGetValue(guildId.Value, out var g)
? g
: new DiscordGuild
{
Id = guildId.Value,
Discord = client
};
var channel = client.InternalGetCachedChannel(channelId.Value);
if (channel == null)
{
reference.Channel = new DiscordChannel
{
Id = channelId.Value,
Discord = client
};
if (guildId.HasValue)
reference.Channel.GuildId = guildId.Value;
}
else reference.Channel = channel;
if (client.MessageCache != null && client.MessageCache.TryGet(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg))
reference.Message = msg;
else
{
reference.Message = new DiscordMessage
{
ChannelId = this.ChannelId,
Discord = client
};
if (messageId.HasValue)
reference.Message.Id = messageId.Value;
}
return reference;
}
///
/// Gets the mentions.
///
/// An array of IMentions.
private IMention[] GetMentions()
{
var mentions = new List();
if (this.ReferencedMessage != null && this._mentionedUsers.Contains(this.ReferencedMessage.Author))
mentions.Add(new RepliedUserMention()); // Return null to allow all mentions
if (this._mentionedUsers.Any())
mentions.AddRange(this._mentionedUsers.Select(m => (IMention)new UserMention(m)));
if (this._mentionedRoleIds.Any())
mentions.AddRange(this._mentionedRoleIds.Select(r => (IMention)new RoleMention(r)));
return mentions.ToArray();
}
///
/// Populates the mentions.
///
internal void PopulateMentions()
{
var guild = this.Channel?.Guild;
this._mentionedUsers ??= new List();
this._mentionedRoles ??= new List();
this._mentionedChannels ??= new List();
var mentionedUsers = new HashSet(new DiscordUserComparer());
if (guild != null)
{
foreach (var usr in this._mentionedUsers)
{
usr.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
return old;
});
mentionedUsers.Add(guild._members.TryGetValue(usr.Id, out var member) ? member : usr);
}
}
if (!string.IsNullOrWhiteSpace(this.Content))
{
//mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal));
if (guild != null)
{
//this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList();
this._mentionedRoles = this._mentionedRoles.Union(this._mentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList();
this._mentionedChannels = this._mentionedChannels.Union(Utilities.GetChannelMentions(this).Select(xid => guild.GetChannel(xid))).ToList();
}
}
this._mentionedUsers = mentionedUsers.ToList();
}
///
/// Edits the message.
///
/// New content.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New embed.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional embed = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.HasValue ? new[] {embed.Value} : Array.Empty(), this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New content.
/// New embed.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content, Optional embed = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.HasValue ? new[] {embed.Value} : Array.Empty(), this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New content.
/// New embeds.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content, Optional> embeds = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// The builder of the message to edit.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(DiscordMessageBuilder builder)
{
builder.Validate(true);
- return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? new Optional>(builder.Attachments) : builder._keepAttachments ? new Optional>(this.Attachments) : null);
+ return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? new Optional>(builder.Attachments) : builder._keepAttachments.HasValue ? builder._keepAttachments.Value ? new Optional>(this.Attachments) : Array.Empty() : null);
}
///
/// Edits the message embed suppression.
///
/// Suppress embeds.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifySuppressionAsync(bool suppress = false)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, suppress, default, default);
///
/// Clears all attachments from the message.
///
///
public Task ClearAttachmentsAsync()
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, default, default, Array.Empty());
///
/// Edits the message.
///
/// The builder of the message to edit.
///
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
builder.Validate(true);
- return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? new Optional>(builder.Attachments) : builder._keepAttachments ? new Optional>(this.Attachments) : null);
+ return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? new Optional>(builder.Attachments) : builder._keepAttachments.HasValue ? builder._keepAttachments.Value ? new Optional>(this.Attachments) : Array.Empty() : null);
}
///
/// Deletes the message.
///
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAsync(string reason = null)
=> this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason);
///
/// Creates a thread.
/// Depending on the of the parent channel it's either a or a .
///
/// The name of the thread.
/// till it gets archived. Defaults to
/// The per user ratelimit, aka slowdown.
/// The reason.
///
/// Thrown when the client does not have the or permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when the cannot be modified.
public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration auto_archive_duration = ThreadAutoArchiveDuration.OneHour, int? rate_limit_per_user = null, string reason = null)
{
return Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, auto_archive_duration)
? await this.Discord.ApiClient.CreateThreadWithMessageAsync(this.ChannelId, this.Id, name, auto_archive_duration, rate_limit_per_user, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
}
///
/// Pins the message in its channel.
///
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task PinAsync()
=> this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id);
///
/// Unpins the message in its channel.
///
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnpinAsync()
=> this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id);
///
/// Responds to the message. This produces a reply.
///
/// Message content to respond with.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(string content)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(DiscordEmbed embed)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// Message content to respond with.
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(string content, DiscordEmbed embed)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// The Discord message builder.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(DiscordMessageBuilder builder)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false));
///
/// Responds to the message. This produces a reply.
///
/// The Discord message builder.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false));
}
///
/// Creates a reaction to this message.
///
/// The emoji you want to react with, either an emoji or name:id
///
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task CreateReactionAsync(DiscordEmoji emoji)
=> this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString());
///
/// Deletes your own reaction
///
/// Emoji for the reaction you want to remove, either an emoji or name:id
///
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOwnReactionAsync(DiscordEmoji emoji)
=> this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString());
///
/// Deletes another user's reaction.
///
/// Emoji for the reaction you want to remove, either an emoji or name:id.
/// Member you want to remove the reaction for
/// Reason for audit logs.
///
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null)
=> this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason);
///
/// Gets users that reacted with this emoji.
///
/// Emoji to react with.
/// Limit of users to fetch.
/// Fetch users after this user's id.
///
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetReactionsAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null)
=> this.GetReactionsInternalAsync(emoji, limit, after);
///
/// Deletes all reactions for this message.
///
/// Reason for audit logs.
///
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAllReactionsAsync(string reason = null)
=> this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason);
///
/// Deletes all reactions of a specific reaction for this message.
///
/// The emoji to clear, either an emoji or name:id.
///
/// Thrown when the client does not have the permission.
/// Thrown when the emoji does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji)
=> this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString());
///
/// Gets the reactions.
///
/// The emoji to search for.
/// The limit of results.
/// Get the reasctions after snowflake.
private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null)
{
if (limit < 0)
throw new ArgumentException("Cannot get a negative number of reactions' users.");
if (limit == 0)
return Array.Empty();
var users = new List(limit);
var remaining = limit;
var last = after;
int lastCount;
do
{
var fetchSize = remaining > 100 ? 100 : remaining;
var fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Id, this.Id, emoji.ToReactionString(), last, fetchSize).ConfigureAwait(false);
lastCount = fetch.Count;
remaining -= lastCount;
users.AddRange(fetch);
last = fetch.LastOrDefault()?.Id;
} while (remaining > 0 && lastCount > 0);
return new ReadOnlyCollection(users);
}
///
/// Returns a string representation of this message.
///
/// String representation of this message.
public override string ToString() => $"Message {this.Id}; Attachment count: {this._attachments.Count}; Embed count: {this._embeds.Count}; Contents: {this.Content}";
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj) => this.Equals(obj as DiscordMessage);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordMessage e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ChannelId == e.ChannelId));
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
{
var hash = 13;
hash = (hash * 7) + this.Id.GetHashCode();
hash = (hash * 7) + this.ChannelId.GetHashCode();
return hash;
}
///
/// Gets whether the two objects are equal.
///
/// First message to compare.
/// Second message to compare.
/// Whether the two messages are equal.
public static bool operator ==(DiscordMessage e1, DiscordMessage e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.ChannelId == e2.ChannelId));
}
///
/// Gets whether the two objects are not equal.
///
/// First message to compare.
/// Second message to compare.
/// Whether the two messages are not equal.
public static bool operator !=(DiscordMessage e1, DiscordMessage e2)
=> !(e1 == e2);
}
}
diff --git a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs
index b61a00840..95242597a 100644
--- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs
+++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs
@@ -1,460 +1,460 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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; } = false;
///
/// Whether to keep previous attachments.
///
- internal bool _keepAttachments = false;
+ internal bool? _keepAttachments = null;
///
/// Gets the Allowed Mentions for the message to be sent.
///
public List Mentions { get; private set; } = null;
///
/// Gets the Files to be sent in the Message.
///
public IReadOnlyCollection Files => this._files;
internal readonly List _files = new();
///
/// Gets the components that will be attached to the message.
///
public IReadOnlyList Components => this._components;
internal readonly List _components = new(5);
///
/// Gets the Attachments to be sent in the Message.
///
public IReadOnlyList Attachments => this._attachments;
internal readonly List _attachments = new();
///
/// Gets the Reply Message ID.
///
public ulong? ReplyId { get; private set; } = null;
///
/// Gets if the Reply should mention the user.
///
public bool MentionOnReply { get; private set; } = false;
///
/// Gets if the embeds should be suppressed.
///
public bool Suppressed { get; private set; } = false;
///
/// 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._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 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._components.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._files.Any(x => x.FileName == fileName))
throw new ArgumentException("A File with that filename already exists");
if (resetStreamPosition)
this._files.Add(new DiscordMessageFile(fileName, stream, stream.Position, description: description));
else
this._files.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._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;
}
///
/// 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._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 DiscordMessageBuilder ModifyAttachments(IEnumerable attachments)
{
this._attachments.AddRange(attachments);
return this;
}
///
/// Whether to keep the message attachments, if new ones are added.
///
///
- public DiscordMessageBuilder KeepAttachments()
+ public DiscordMessageBuilder KeepAttachments(bool keep)
{
- this._keepAttachments = true;
+ this._keepAttachments = 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._components.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._files.Clear();
this.ReplyId = null;
this.MentionOnReply = false;
this._components.Clear();
this.Suppressed = false;
this.Sticker = null;
this._attachments.Clear();
this._keepAttachments = 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.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/DiscordWebhook.cs b/DisCatSharp/Entities/Webhook/DiscordWebhook.cs
index 070a659a1..21d00fcd7 100644
--- a/DisCatSharp/Entities/Webhook/DiscordWebhook.cs
+++ b/DisCatSharp/Entities/Webhook/DiscordWebhook.cs
@@ -1,283 +1,286 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.IO;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Net;
using Newtonsoft.Json;
namespace DisCatSharp.Entities
{
///
/// Represents information about a Discord webhook.
///
public class DiscordWebhook : SnowflakeObject, IEquatable
{
///
/// Gets the api client.
///
internal DiscordApiClient ApiClient { get; set; }
///
/// Gets the ID of the guild this webhook belongs to.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong GuildId { get; internal set; }
///
/// Gets the ID of the channel this webhook belongs to.
///
[JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ChannelId { get; internal set; }
///
/// Gets the user this webhook was created by.
///
[JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)]
public DiscordUser User { get; internal set; }
///
/// Gets the default name of this webhook.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets hash of the default avatar for this webhook.
///
[JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)]
internal string AvatarHash { get; set; }
///
/// Gets the partial source guild for this webhook (For Channel Follower Webhooks).
///
[JsonProperty("source_guild", NullValueHandling = NullValueHandling.Ignore)]
public DiscordGuild SourceGuild { get; set; }
///
/// Gets the partial source channel for this webhook (For Channel Follower Webhooks).
///
[JsonProperty("source_channel", NullValueHandling = NullValueHandling.Ignore)]
public DiscordChannel SourceChannel { get; set; }
///
/// Gets the url used for executing the webhook.
///
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)]
public string Url { get; set; }
///
/// Gets the default avatar url for this webhook.
///
public string AvatarUrl
=> !string.IsNullOrWhiteSpace(this.AvatarHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id}/{this.AvatarHash}.png?size=1024" : null;
///
/// Gets the secure token of this webhook.
///
[JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)]
public string Token { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordWebhook() { }
///
/// Modifies this webhook.
///
/// New default name for this webhook.
/// New avatar for this webhook.
/// The new channel id to move the webhook to.
/// Reason for audit logs.
/// The modified webhook.
/// Thrown when the client does not have the permission.
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null)
{
var avatarb64 = Optional.FromNoValue();
if (avatar.HasValue && avatar.Value != null)
using (var imgtool = new ImageTool(avatar.Value))
avatarb64 = imgtool.GetBase64();
else if (avatar.HasValue)
avatarb64 = null;
var newChannelId = channelId ?? this.ChannelId;
return this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason);
}
///
/// Gets a previously-sent webhook message.
///
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetMessageAsync(ulong messageId)
=> await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId).ConfigureAwait(false);
///
/// Gets a previously-sent webhook message.
///
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetMessageAsync(ulong messageId, ulong threadId)
=> await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId, threadId).ConfigureAwait(false);
///
/// Permanently deletes this webhook.
///
///
/// Thrown when the client does not have the permission.
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAsync()
=> this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token);
///
/// Executes this webhook with the given .
///
/// Webhook builder filled with data to send.
/// Target thread id (Optional). Defaults to null.
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ExecuteAsync(DiscordWebhookBuilder builder, string thread_id = null)
=> (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder, thread_id);
///
/// Executes this webhook in Slack compatibility mode.
///
/// JSON containing Slack-compatible payload for this webhook.
/// Target thread id (Optional). Defaults to null.
///
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ExecuteSlackAsync(string json, string thread_id = null)
=> (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json, thread_id);
///
/// Executes this webhook in GitHub compatibility mode.
///
/// JSON containing GitHub-compatible payload for this webhook.
/// Target thread id (Optional). Defaults to null.
///
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ExecuteGithubAsync(string json, string thread_id = null)
=> (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json, thread_id);
///
/// Edits a previously-sent webhook message.
///
/// The id of the message to edit.
/// The builder of the message to edit.
/// Target thread id (Optional). Defaults to null.
/// The modified
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task EditMessageAsync(ulong messageId, DiscordWebhookBuilder builder, string thread_id = null)
{
builder.Validate(true);
- if (builder._keepAttachments)
+ if (builder._keepAttachments.HasValue && builder._keepAttachments.Value)
{
builder._attachments.AddRange(this.ApiClient.GetWebhookMessageAsync(this.Id, this.Token, messageId.ToString(), thread_id).Result.Attachments);
+ } else if (builder._keepAttachments.HasValue)
+ {
+ builder._attachments.Clear();
}
return await (this.Discord?.ApiClient ?? this.ApiClient).EditWebhookMessageAsync(this.Id, this.Token, messageId.ToString(), builder, thread_id).ConfigureAwait(false);
}
///
/// Deletes a message that was created by the webhook.
///
/// The id of the message to delete
///
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteMessageAsync(ulong messageId)
=> (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId);
///
/// Deletes a message that was created by the webhook.
///
/// The id of the message to delete
/// Target thread id (Optional). Defaults to null.
///
/// Thrown when the webhook does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteMessageAsync(ulong messageId, ulong threadId)
=> (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId, threadId);
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj) => this.Equals(obj as DiscordWebhook);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordWebhook e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id);
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode() => this.Id.GetHashCode();
///
/// Gets whether the two objects are equal.
///
/// First webhook to compare.
/// Second webhook to compare.
/// Whether the two webhooks are equal.
public static bool operator ==(DiscordWebhook e1, DiscordWebhook e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id);
}
///
/// Gets whether the two objects are not equal.
///
/// First webhook to compare.
/// Second webhook to compare.
/// Whether the two webhooks are not equal.
public static bool operator !=(DiscordWebhook e1, DiscordWebhook e2)
=> !(e1 == e2);
}
}
diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs
index bc41b88bf..4c5f63ba3 100644
--- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs
+++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs
@@ -1,442 +1,442 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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 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;
///
/// Whether to keep previous attachments.
///
- internal bool _keepAttachments = false;
+ internal bool? _keepAttachments = null;
///
/// 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._attachments;
internal readonly List _attachments = 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;
}
///
/// 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._attachments.AddRange(attachments);
return this;
}
///
/// Whether to keep the message attachments, if new ones are added.
///
///
- public DiscordWebhookBuilder KeepAttachments()
+ public DiscordWebhookBuilder KeepAttachments(bool keep)
{
- this._keepAttachments = true;
+ this._keepAttachments = 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._attachments.Clear();
this._components.Clear();
this._keepAttachments = false;
}
///
/// 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.");
}
}
}
}