diff --git a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs
index 87bf6d452..847e5e264 100644
--- a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs
+++ b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs
@@ -1,313 +1,315 @@
// 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;
namespace DisCatSharp.Entities
{
///
/// Constructs a followup message to an interaction.
///
public sealed class DiscordFollowupMessageBuilder
{
///
/// Whether this followup message is text-to-speech.
///
public bool IsTTS { get; set; }
///
/// Whether this followup message should be ephemeral.
///
public bool IsEphemeral { get; set; }
///
/// Indicates this message is emphemeral.
///
internal int? Flags
=> this.IsEphemeral ? 64 : null;
///
/// Message to send on followup message.
///
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;
///
/// Embeds to send on followup message.
///
public IReadOnlyList Embeds => this._embeds;
private readonly List _embeds = new();
///
/// Files to send on this followup message.
///
public IReadOnlyList Files => this._files;
private readonly List _files = new();
///
/// Components to send on this followup message.
///
public IReadOnlyList Components => this._components;
private readonly List _components = new();
///
/// Mentions to send on this followup message.
///
public IReadOnlyList Mentions => this._mentions;
private readonly List _mentions = new();
///
/// Appends a collection of components to the message.
///
/// The collection of components to add.
/// The builder to chain calls with.
/// contained more than 5 components.
public DiscordFollowupMessageBuilder 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 DiscordFollowupMessageBuilder 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;
}
///
/// Appends a collection of components to the message.
///
/// The collection of components to add.
/// The builder to chain calls with.
/// contained more than 5 components.
public DiscordFollowupMessageBuilder AddComponents(IEnumerable components)
{
var compArr = components.ToArray();
var count = compArr.Length;
if (count > 5)
throw new ArgumentException("Cannot add more than 5 components per action row!");
var arc = new DiscordActionRowComponent(compArr);
this._components.Add(arc);
return this;
}
///
/// Indicates if the followup message must use text-to-speech.
///
/// Text-to-speech
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder WithTTS(bool tts)
{
this.IsTTS = tts;
return this;
}
///
/// Sets the message to send with the followup message..
///
/// Message to send.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder WithContent(string content)
{
this.Content = content;
return this;
}
///
/// Adds an embed to the followup message.
///
/// Embed to add.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder AddEmbed(DiscordEmbed embed)
{
this._embeds.Add(embed);
return this;
}
///
/// Adds the given embeds to the followup message.
///
/// Embeds to add.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder AddEmbeds(IEnumerable embeds)
{
this._embeds.AddRange(embeds);
return this;
}
///
/// Adds a file to the followup message.
///
/// 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.
/// The builder to chain calls with.
- public DiscordFollowupMessageBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false)
+ public DiscordFollowupMessageBuilder 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));
+ this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(filename, data, null));
+ 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.
/// The builder to chain calls with.
- public DiscordFollowupMessageBuilder AddFile(FileStream stream, bool resetStreamPosition = false)
+ public DiscordFollowupMessageBuilder 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));
+ this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(stream.Name, stream, null));
+ this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description));
return this;
}
///
/// Adds the given files to the followup message.
///
/// 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.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder 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;
}
///
/// Adds the mention to the mentions to parse, etc. with the followup message.
///
/// Mention to add.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder AddMention(IMention mention)
{
this._mentions.Add(mention);
return this;
}
///
/// Adds the mentions to the mentions to parse, etc. with the followup message.
///
/// Mentions to add.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder AddMentions(IEnumerable mentions)
{
this._mentions.AddRange(mentions);
return this;
}
///
/// Sets the followup message to be ephemeral.
///
/// Ephemeral.
/// The builder to chain calls with.
public DiscordFollowupMessageBuilder AsEphemeral(bool ephemeral)
{
this.IsEphemeral = ephemeral;
return this;
}
///
/// Clears all message components on this builder.
///
public void ClearComponents()
=> this._components.Clear();
///
/// Allows for clearing the Followup 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.Clear();
this._files.Clear();
this.IsEphemeral = false;
this._components.Clear();
}
///
/// Validates the builder.
///
internal void Validate()
{
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.");
}
}
}
diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs
index 6182f5054..541f811f3 100644
--- a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs
+++ b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs
@@ -1,351 +1,353 @@
// 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;
namespace DisCatSharp.Entities
{
///
/// Constructs an interaction response.
///
public sealed class DiscordInteractionResponseBuilder
{
///
/// Whether this interaction response is text-to-speech.
///
public bool IsTTS { get; set; }
///
/// Whether this interaction response should be ephemeral.
///
public bool IsEphemeral { get; set; }
///
/// Content of the message to send.
///
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;
///
/// Embeds to send on this interaction response.
///
public IReadOnlyList Embeds => this._embeds;
private readonly List _embeds = new();
///
/// Files to send on this interaction response.
///
public IReadOnlyList Files => this._files;
private readonly List _files = new();
///
/// Components to send on this interaction response.
///
public IReadOnlyList Components => this._components;
private readonly List _components = new();
///
/// The choices to send on this interaction response.
/// Mutually exclusive with content, embed, and components.
///
public IReadOnlyList Choices => this._choices;
private readonly List _choices = new();
///
/// Mentions to send on this interaction response.
///
public IEnumerable Mentions => this._mentions;
private readonly List _mentions = new();
///
/// Constructs a new empty interaction response builder.
///
public DiscordInteractionResponseBuilder() { }
///
/// Constructs a new based on an existing .
///
/// The builder to copy.
public DiscordInteractionResponseBuilder(DiscordMessageBuilder builder)
{
this._content = builder.Content;
this._mentions = builder.Mentions;
this._embeds.AddRange(builder.Embeds);
this._components.AddRange(builder.Components);
}
///
/// Appends a collection of components to the builder. Each call will append to a new row.
///
/// The components to append. Up to five.
/// The current builder to chain calls with.
/// Thrown when passing more than 5 components.
public DiscordInteractionResponseBuilder 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 DiscordInteractionResponseBuilder 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;
}
///
/// Appends a collection of components to the builder. Each call will append to a new row.
///
/// The components to append. Up to five.
/// The current builder to chain calls with.
/// Thrown when passing more than 5 components.
public DiscordInteractionResponseBuilder AddComponents(IEnumerable components)
{
var compArr = components.ToArray();
var count = compArr.Length;
if (count > 5)
throw new ArgumentException("Cannot add more than 5 components per action row!");
var arc = new DiscordActionRowComponent(compArr);
this._components.Add(arc);
return this;
}
///
/// Indicates if the interaction response will be text-to-speech.
///
/// Text-to-speech
public DiscordInteractionResponseBuilder WithTTS(bool tts)
{
this.IsTTS = tts;
return this;
}
///
/// Sets the interaction response to be ephemeral.
///
/// Ephemeral.
public DiscordInteractionResponseBuilder AsEphemeral(bool ephemeral)
{
this.IsEphemeral = ephemeral;
return this;
}
///
/// Sets the content of the message to send.
///
/// Content to send.
public DiscordInteractionResponseBuilder WithContent(string content)
{
this.Content = content;
return this;
}
///
/// Adds an embed to send with the interaction response.
///
/// Embed to add.
public DiscordInteractionResponseBuilder AddEmbed(DiscordEmbed embed)
{
if (embed != null)
this._embeds.Add(embed); // Interactions will 400 silently //
return this;
}
///
/// Adds the given embeds to send with the interaction response.
///
/// Embeds to add.
public DiscordInteractionResponseBuilder AddEmbeds(IEnumerable embeds)
{
this._embeds.AddRange(embeds);
return this;
}
///
/// Adds a file to the interaction response.
///
/// 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.
/// The builder to chain calls with.
- public DiscordInteractionResponseBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false)
+ public DiscordInteractionResponseBuilder 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));
+ this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(filename, data, null));
+ 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.
/// The builder to chain calls with.
- public DiscordInteractionResponseBuilder AddFile(FileStream stream, bool resetStreamPosition = false)
+ public DiscordInteractionResponseBuilder 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));
+ this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(stream.Name, stream, null));
+ this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description));
return this;
}
///
/// Adds the given files to the interaction response builder.
///
/// 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.
/// The builder to chain calls with.
public DiscordInteractionResponseBuilder 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;
}
///
/// Adds the mention to the mentions to parse, etc. with the interaction response.
///
/// Mention to add.
public DiscordInteractionResponseBuilder AddMention(IMention mention)
{
this._mentions.Add(mention);
return this;
}
///
/// Adds the mentions to the mentions to parse, etc. with the interaction response.
///
/// Mentions to add.
public DiscordInteractionResponseBuilder AddMentions(IEnumerable mentions)
{
this._mentions.AddRange(mentions);
return this;
}
///
/// Adds a single auto-complete choice to the builder.
///
/// The choice to add.
/// The current builder to chain calls with.
public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordApplicationCommandAutocompleteChoice choice)
{
this._choices.Add(choice);
return this;
}
///
/// Adds auto-complete choices to the builder.
///
/// The choices to add.
/// The current builder to chain calls with.
public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable choices)
{
this._choices.AddRange(choices);
return this;
}
///
/// Adds auto-complete choices to the builder.
///
/// The choices to add.
/// The current builder to chain calls with.
public DiscordInteractionResponseBuilder AddAutoCompleteChoices(params DiscordApplicationCommandAutocompleteChoice[] choices)
=> this.AddAutoCompleteChoices((IEnumerable)choices);
///
/// Clears all message components on this builder.
///
public void ClearComponents()
=> this._components.Clear();
///
/// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response.
///
public void Clear()
{
this.Content = "";
this._embeds.Clear();
this.IsTTS = false;
this.IsEphemeral = false;
this._mentions.Clear();
this._components.Clear();
this._choices.Clear();
this._files.Clear();
}
}
}
diff --git a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs
index 44fc22fc0..1b382a1fe 100644
--- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs
+++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs
@@ -1,430 +1,432 @@
// 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;
///
/// 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.
///
internal List Attachments { get; private set; } = null;
///
/// 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)
+ 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));
+ this._files.Add(new DiscordMessageFile(fileName, stream, stream.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(fileName, stream, null));
+ 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)
+ 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));
+ this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(stream.Name, stream, null));
+ 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;
}
///
/// 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();
}
///
/// 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/Message/DiscordMessageFile.cs b/DisCatSharp/Entities/Message/DiscordMessageFile.cs
index 670ca96fb..28d96b8a1 100644
--- a/DisCatSharp/Entities/Message/DiscordMessageFile.cs
+++ b/DisCatSharp/Entities/Message/DiscordMessageFile.cs
@@ -1,74 +1,81 @@
// 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.IO;
namespace DisCatSharp.Entities
{
///
/// Represents the File that should be sent to Discord from the .
///
public class DiscordMessageFile
{
///
/// Initializes a new instance of the class.
///
/// The file name.
/// The stream.
/// The reset position to.
/// The file type.
/// The content type.
- internal DiscordMessageFile(string fileName, Stream stream, long? resetPositionTo, string fileType = null, string contentType = null)
+ /// The description.
+ internal DiscordMessageFile(string fileName, Stream stream, long? resetPositionTo, string fileType = null, string contentType = null, string description = null)
{
this.FileName = fileName;
this.FileType = fileType;
this.ContentType = contentType;
this.Stream = stream;
this.ResetPositionTo = resetPositionTo;
+ this.Description = description;
}
///
/// Gets the FileName of the File.
///
public string FileName { get; internal set; }
+ ///
+ /// Gets the description of the File.
+ ///
+ public string Description { get; internal set; }
+
///
/// Gets the stream of the File.
///
public Stream Stream { get; internal set; }
///
/// Gets or sets the file type.
///
internal string FileType { get; set; }
///
/// Gets or sets the content type.
///
internal string ContentType { get; set; }
///
/// Gets the position the File should be reset to.
///
internal long? ResetPositionTo { get; set; }
}
}
diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs
index 44161710a..743958bc3 100644
--- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs
+++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs
@@ -1,413 +1,415 @@
// 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;
///
/// 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 { get; }
private 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.
- public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false)
+ /// 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));
+ this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(filename, data, null));
+ 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)
+ 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));
+ this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description));
else
- this._files.Add(new DiscordMessageFile(stream.Name, stream, null));
+ 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;
}
///
/// 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();
}
///
/// 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.");
}
}
}
}
diff --git a/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs
index 1ba6d1095..3c53219a9 100644
--- a/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs
+++ b/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs
@@ -1,142 +1,154 @@
// 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.Collections.Generic;
using DisCatSharp.Entities;
using Newtonsoft.Json;
namespace DisCatSharp.Net.Abstractions
{
///
/// Represents a webhook payload.
///
internal sealed class RestWebhookPayload
{
///
/// Gets or sets the name.
///
[JsonProperty("name")]
public string Name { get; set; }
///
/// Gets or sets the avatar base64.
///
[JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)]
public string AvatarBase64 { get; set; }
///
/// Gets or sets the channel id.
///
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
///
/// Gets whether an avatar is set.
///
[JsonProperty]
public bool AvatarSet { get; set; }
///
/// Gets whether the avatar should be serialized.
///
public bool ShouldSerializeAvatarBase64()
=> this.AvatarSet;
}
///
/// Represents a webhook execute payload.
///
internal sealed class RestWebhookExecutePayload
{
///
/// Gets or sets the content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; set; }
///
/// Gets or sets the username.
///
[JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)]
public string Username { get; set; }
///
/// Gets or sets the avatar url.
///
[JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)]
public string AvatarUrl { get; set; }
///
/// Whether this message is tts.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsTTS { get; set; }
///
/// Gets or sets the embeds.
///
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Embeds { get; set; }
///
/// Gets or sets the mentions.
///
[JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMentions Mentions { get; set; }
+
+ ///
+ /// Gets or sets the components.
+ ///
+ [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
+ public IEnumerable Components { get; set; }
+
+ ///
+ /// Gets or sets the attachments.
+ ///
+ [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
+ public List Attachments { get; set; }
}
///
/// Represents a webhook message edit payload.
///
internal sealed class RestWebhookMessageEditPayload
{
///
/// Gets or sets the content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public Optional Content { get; set; }
///
/// Gets or sets the embeds.
///
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Embeds { get; set; }
///
/// Gets or sets the mentions.
///
[JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Mentions { get; set; }
///
/// Gets or sets the attachments.
///
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Attachments { get; set; }
///
/// Gets or sets the components.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Components { get; set; }
}
}
diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs
index 1c34058af..d7a512ab6 100644
--- a/DisCatSharp/Net/Rest/DiscordApiClient.cs
+++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs
@@ -1,5217 +1,5263 @@
// 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.Net;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Net
{
///
/// Represents a discord api client.
///
public sealed class DiscordApiClient
{
///
/// The audit log reason header name.
///
private const string REASON_HEADER_NAME = "X-Audit-Log-Reason";
///
/// Gets the discord client.
///
internal BaseDiscordClient Discord { get; }
///
/// Gets the rest client.
///
internal RestClient Rest { get; }
///
/// Initializes a new instance of the class.
///
/// The client.
internal DiscordApiClient(BaseDiscordClient client)
{
this.Discord = client;
this.Rest = new RestClient(client);
}
///
/// Initializes a new instance of the class.
///
/// The proxy.
/// The timeout.
/// If true, use relative rate limit.
/// The logger.
internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client
{
this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger);
}
///
/// Builds the query string.
///
/// The values.
/// If true, post.
/// A string.
private static string BuildQueryString(IDictionary values, bool post = false)
{
if (values == null || values.Count == 0)
return string.Empty;
var vals_collection = values.Select(xkvp =>
$"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}");
var vals = string.Join("&", vals_collection);
return !post ? $"?{vals}" : vals;
}
///
/// Prepares the message.
///
/// The msg_raw.
/// A DiscordMessage.
private DiscordMessage PrepareMessage(JToken msg_raw)
{
var author = msg_raw["author"].ToObject();
var ret = msg_raw.ToDiscordObject();
ret.Discord = this.Discord;
this.PopulateMessage(author, ret);
var referencedMsg = msg_raw["referenced_message"];
if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString()))
{
author = referencedMsg["author"].ToObject();
ret.ReferencedMessage.Discord = this.Discord;
this.PopulateMessage(author, ret.ReferencedMessage);
}
if (ret.Channel != null)
return ret;
var channel = !ret.GuildId.HasValue
? new DiscordDmChannel
{
Id = ret.ChannelId,
Discord = this.Discord,
Type = ChannelType.Private
}
: new DiscordChannel
{
Id = ret.ChannelId,
GuildId = ret.GuildId,
Discord = this.Discord
};
ret.Channel = channel;
return ret;
}
///
/// Populates the message.
///
/// The author.
/// The ret.
private void PopulateMessage(TransportUser author, DiscordMessage ret)
{
var guild = ret.Channel?.Guild;
//If this is a webhook, it shouldn't be in the user cache.
if (author.IsBot && int.Parse(author.Discriminator) == 0)
{
ret.Author = new DiscordUser(author) { Discord = this.Discord };
}
else
{
if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr))
{
this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord };
}
if (guild != null)
{
if (!guild.Members.TryGetValue(author.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this.Discord, _guild_id = guild.Id };
ret.Author = mbr;
}
else
{
ret.Author = usr;
}
}
ret.PopulateMentions();
if (ret._reactions == null)
ret._reactions = new List();
foreach (var xr in ret._reactions)
xr.Emoji.Discord = this.Discord;
}
///
/// Executes a rest request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The payload.
/// The ratelimit wait override.
/// A Task.
internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null)
{
var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart rest request for stickers.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The file.
/// The sticker name.
/// The sticker tag.
/// The sticker description.
/// The ratelimit wait override.
/// A Task.
private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null,
DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null)
{
var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The values.
/// The files.
/// The ratelimit wait override.
/// A Task.
private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null,
IReadOnlyCollection files = null, double? ratelimitWaitOverride = null)
{
var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride);
if (this.Discord != null)
this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req);
return req.WaitForCompletionAsync();
}
#region Guild
///
/// Searches the members async.
///
/// The guild_id.
/// The name.
/// The limit.
/// A Task.
internal async Task> SearchMembersAsync(ulong guild_id, string name, int? limit)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var querydict = new Dictionary
{
["query"] = name,
["limit"] = limit.ToString()
};
var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JArray.Parse(res.Response);
var tms = json.ToObject>();
var mbrs = new List();
foreach (var xtm in tms)
{
var usr = new DiscordUser(xtm.User) { Discord = this.Discord };
this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discord = usr.Discord;
old.AvatarHash = usr.AvatarHash;
return old;
});
mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, _guild_id = guild_id });
}
return mbrs;
}
///
/// Gets the guild ban async.
///
/// The guild_id.
/// The user_id.
/// A Task.
internal async Task GetGuildBanAsync(ulong guild_id, ulong user_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, user_id}, out var path);
var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var ban = json.ToObject();
return ban;
}
///
/// Creates the guild async.
///
/// The name.
/// The region_id.
/// The iconb64.
/// The verification_level.
/// The default_message_notifications.
/// The system_channel_flags.
internal async Task CreateGuildAsync(string name, string region_id, Optional iconb64, VerificationLevel? verification_level,
DefaultMessageNotifications? default_message_notifications, SystemChannelFlags? system_channel_flags)
{
var pld = new RestGuildCreatePayload
{
Name = name,
RegionId = region_id,
DefaultMessageNotifications = default_message_notifications,
VerificationLevel = verification_level,
IconBase64 = iconb64,
SystemChannelFlags = system_channel_flags
};
var route = $"{Endpoints.GUILDS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var raw_members = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false);
return guild;
}
///
/// Creates the guild from template async.
///
/// The template_code.
/// The name.
/// The iconb64.
internal async Task CreateGuildFromTemplateAsync(string template_code, string name, Optional iconb64)
{
var pld = new RestGuildCreateFromTemplatePayload
{
Name = name,
IconBase64 = iconb64
};
var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { template_code }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var raw_members = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false);
return guild;
}
///
/// Deletes the guild async.
///
/// The guild_id.
internal async Task DeleteGuildAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false);
if (this.Discord is DiscordClient dc)
{
var gld = dc._guilds[guild_id];
await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false);
}
}
///
/// Modifies the guild.
///
/// The guild id.
/// The name.
/// The verification level.
/// The default message notifications.
/// The mfa level.
/// The explicit content filter.
/// The afk channel id.
/// The afk timeout.
/// The iconb64.
/// The owner id.
/// The splashb64.
/// The system channel id.
/// The system channel flags.
/// The public updates channel id.
/// The rules channel id.
/// The description.
/// The banner base64.
/// The discovery base64.
/// The preferred locale.
/// Whether the premium progress bar should be enabled.
/// The reason.
internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional verificationLevel,
Optional defaultMessageNotifications, Optional mfaLevel,
Optional explicitContentFilter, Optional afkChannelId,
Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64,
Optional systemChannelId, Optional systemChannelFlags,
Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description,
Optional bannerb64, Optional discorverySplashb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string reason)
{
var pld = new RestGuildModifyPayload
{
Name = name,
VerificationLevel = verificationLevel,
DefaultMessageNotifications = defaultMessageNotifications,
MfaLevel = mfaLevel,
ExplicitContentFilter = explicitContentFilter,
AfkChannelId = afkChannelId,
AfkTimeout = afkTimeout,
IconBase64 = iconb64,
SplashBase64 = splashb64,
BannerBase64 = bannerb64,
DiscoverySplashBase64 = discorverySplashb64,
OwnerId = ownerId,
SystemChannelId = systemChannelId,
SystemChannelFlags = systemChannelFlags,
RulesChannelId = rulesChannelId,
PublicUpdatesChannelId = publicUpdatesChannelId,
PreferredLocale = preferredLocale,
Description = description,
PremiumProgressBarEnabled = premiumProgressBarEnabled
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
foreach (var r in guild._roles.Values)
r._guild_id = guild.Id;
if (this.Discord is DiscordClient dc)
await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false);
return guild;
}
///
/// Modifies the guild community settings.
///
/// The guild id.
/// The guild features.
/// The rules channel id.
/// The public updates channel id.
/// The preferred locale.
/// The description.
/// The default message notifications.
/// The explicit content filter.
/// The verification level.
/// The reason.
internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string reason)
{
var pld = new RestGuildCommunityModifyPayload
{
VerificationLevel = verificationLevel,
DefaultMessageNotifications = defaultMessageNotifications,
ExplicitContentFilter = explicitContentFilter,
RulesChannelId = rulesChannelId,
PublicUpdatesChannelId = publicUpdatesChannelId,
PreferredLocale = preferredLocale,
Description = description ?? Optional.FromNoValue(),
Features = features
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
foreach (var r in guild._roles.Values)
r._guild_id = guild.Id;
if (this.Discord is DiscordClient dc)
await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false);
return guild;
}
///
/// Gets the guild bans async.
///
/// The guild_id.
/// A Task.
internal async Task> GetGuildBansAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var bans_raw = JsonConvert.DeserializeObject>(res.Response).Select(xb =>
{
if (!this.Discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr))
{
usr = new DiscordUser(xb.RawUser) { Discord = this.Discord };
usr = this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
return old;
});
}
xb.User = usr;
return xb;
});
var bans = new ReadOnlyCollection(new List(bans_raw));
return bans;
}
///
/// Creates the guild ban async.
///
/// The guild_id.
/// The user_id.
/// The delete_message_days.
/// The reason.
/// A Task.
internal Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason)
{
if (delete_message_days < 0 || delete_message_days > 7)
throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(delete_message_days));
var urlparams = new Dictionary
{
["delete_message_days"] = delete_message_days.ToString(CultureInfo.InvariantCulture)
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path);
var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers);
}
///
/// Removes the guild ban async.
///
/// The guild_id.
/// The user_id.
/// The reason.
/// A Task.
internal Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers);
}
///
/// Leaves the guild async.
///
/// The guild_id.
/// A Task.
internal Task LeaveGuildAsync(ulong guild_id)
{
var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route);
}
///
/// Adds the guild member async.
///
/// The guild_id.
/// The user_id.
/// The access_token.
/// The nick.
/// The roles.
/// If true, muted.
/// If true, deafened.
/// A Task.
internal async Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened)
{
var pld = new RestGuildMemberAddPayload
{
AccessToken = access_token,
Nickname = nick ?? "",
Roles = roles ?? new List(),
Deaf = deafened,
Mute = muted
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var tm = JsonConvert.DeserializeObject(res.Response);
return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id };
}
///
/// Lists the guild members async.
///
/// The guild_id.
/// The limit.
/// The after.
/// A Task.
internal async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after)
{
var urlparams = new Dictionary();
if (limit != null && limit > 0)
urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture);
if (after != null)
urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var members_raw = JsonConvert.DeserializeObject>(res.Response);
return new ReadOnlyCollection(members_raw);
}
///
/// Adds the guild member role async.
///
/// The guild_id.
/// The user_id.
/// The role_id.
/// The reason.
/// A Task.
internal Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id, role_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers);
}
///
/// Removes the guild member role async.
///
/// The guild_id.
/// The user_id.
/// The role_id.
/// The reason.
/// A Task.
internal Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id, role_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers);
}
///
/// Modifies the guild channel position async.
///
/// The guild_id.
/// The pld.
/// The reason.
/// A Task.
internal Task ModifyGuildChannelPositionAsync(ulong guild_id, IEnumerable pld, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
}
///
/// Modifies the guild channel parent async.
///
/// The guild_id.
/// The pld.
/// The reason.
/// A Task.
internal Task ModifyGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
}
///
/// Detaches the guild channel parent async.
///
/// The guild_id.
/// The pld.
/// The reason.
/// A Task.
internal Task DetachGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
}
///
/// Modifies the guild role position async.
///
/// The guild_id.
/// The pld.
/// The reason.
/// A Task.
internal Task ModifyGuildRolePositionAsync(ulong guild_id, IEnumerable pld, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
}
///
/// Gets the audit logs async.
///
/// The guild_id.
/// The limit.
/// The after.
/// The before.
/// The responsible.
/// The action_type.
/// A Task.
internal async Task GetAuditLogsAsync(ulong guild_id, int limit, ulong? after, ulong? before, ulong? responsible, int? action_type)
{
var urlparams = new Dictionary
{
["limit"] = limit.ToString(CultureInfo.InvariantCulture)
};
if (after != null)
urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture);
if (before != null)
urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture);
if (responsible != null)
urlparams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture);
if (action_type != null)
urlparams["action_type"] = action_type?.ToString(CultureInfo.InvariantCulture);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var audit_log_data_raw = JsonConvert.DeserializeObject(res.Response);
return audit_log_data_raw;
}
///
/// Gets the guild vanity url async.
///
/// The guild_id.
/// A Task.
internal async Task GetGuildVanityUrlAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var invite = JsonConvert.DeserializeObject(res.Response);
return invite;
}
///
/// Gets the guild widget async.
///
/// The guild_id.
/// A Task.
internal async Task GetGuildWidgetAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawChannels = (JArray)json["channels"];
var ret = json.ToDiscordObject();
ret.Discord = this.Discord;
ret.Guild = this.Discord.Guilds[guild_id];
ret.Channels = ret.Guild == null
? rawChannels.Select(r => new DiscordChannel
{
Id = (ulong)r["id"],
Name = r["name"].ToString(),
Position = (int)r["position"]
}).ToList()
: rawChannels.Select(r =>
{
var c = ret.Guild.GetChannel((ulong)r["id"]);
c.Position = (int)r["position"];
return c;
}).ToList();
return ret;
}
///
/// Gets the guild widget settings async.
///
/// The guild_id.
/// A Task.
internal async Task GetGuildWidgetSettingsAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var ret = JsonConvert.DeserializeObject(res.Response);
ret.Guild = this.Discord.Guilds[guild_id];
return ret;
}
///
/// Modifies the guild widget settings async.
///
/// The guild_id.
/// If true, is enabled.
/// The channel id.
/// The reason.
/// A Task.
internal async Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? isEnabled, ulong? channelId, string reason)
{
var pld = new RestGuildWidgetSettingsPayload
{
Enabled = isEnabled,
ChannelId = channelId
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var ret = JsonConvert.DeserializeObject(res.Response);
ret.Guild = this.Discord.Guilds[guild_id];
return ret;
}
///
/// Gets the guild templates async.
///
/// The guild_id.
/// A Task.
internal async Task> GetGuildTemplatesAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var templates_raw = JsonConvert.DeserializeObject>(res.Response);
return new ReadOnlyCollection(new List(templates_raw));
}
///
/// Creates the guild template async.
///
/// The guild_id.
/// The name.
/// The description.
/// A Task.
internal async Task CreateGuildTemplateAsync(ulong guild_id, string name, string description)
{
var pld = new RestGuildTemplateCreateOrModifyPayload
{
Name = name,
Description = description
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var ret = JsonConvert.DeserializeObject(res.Response);
return ret;
}
///
/// Syncs the guild template async.
///
/// The guild_id.
/// The template_code.
/// A Task.
internal async Task SyncGuildTemplateAsync(ulong guild_id, string template_code)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, template_code }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false);
var template_raw = JsonConvert.DeserializeObject(res.Response);
return template_raw;
}
///
/// Modifies the guild template async.
///
/// The guild_id.
/// The template_code.
/// The name.
/// The description.
/// A Task.
internal async Task ModifyGuildTemplateAsync(ulong guild_id, string template_code, string name, string description)
{
var pld = new RestGuildTemplateCreateOrModifyPayload
{
Name = name,
Description = description
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, template_code }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var template_raw = JsonConvert.DeserializeObject(res.Response);
return template_raw;
}
///
/// Deletes the guild template async.
///
/// The guild_id.
/// The template_code.
/// A Task.
internal async Task DeleteGuildTemplateAsync(ulong guild_id, string template_code)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, template_code }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false);
var template_raw = JsonConvert.DeserializeObject(res.Response);
return template_raw;
}
///
/// Gets the guild membership screening form async.
///
/// The guild_id.
/// A Task.
internal async Task GetGuildMembershipScreeningFormAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var screening_raw = JsonConvert.DeserializeObject(res.Response);
return screening_raw;
}
///
/// Modifies the guild membership screening form async.
///
/// The guild_id.
/// The enabled.
/// The fields.
/// The description.
/// A Task.
internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Optional enabled, Optional fields, Optional description)
{
var pld = new RestGuildMembershipScreeningFormModifyPayload
{
Enabled = enabled,
Description = description,
Fields = fields
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var screening_raw = JsonConvert.DeserializeObject(res.Response);
return screening_raw;
}
///
/// Gets the guild welcome screen async.
///
/// The guild_id.
/// A Task.
internal async Task GetGuildWelcomeScreenAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route);
var ret = JsonConvert.DeserializeObject(res.Response);
return ret;
}
///
/// Modifies the guild welcome screen async.
///
/// The guild_id.
/// The enabled.
/// The welcome channels.
/// The description.
/// A Task.
internal async Task ModifyGuildWelcomeScreenAsync(ulong guild_id, Optional enabled, Optional> welcomeChannels, Optional description)
{
var pld = new RestGuildWelcomeScreenModifyPayload
{
Enabled = enabled,
WelcomeChannels = welcomeChannels,
Description = description
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld));
var ret = JsonConvert.DeserializeObject(res.Response);
return ret;
}
///
/// Updates the current user voice state async.
///
/// The guild_id.
/// The channel id.
/// If true, suppress.
/// The request to speak timestamp.
/// A Task.
internal async Task UpdateCurrentUserVoiceStateAsync(ulong guild_id, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp)
{
var pld = new RestGuildUpdateCurrentUserVoiceStatePayload
{
ChannelId = channelId,
Suppress = suppress,
RequestToSpeakTimestamp = requestToSpeakTimestamp
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/@me";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld));
}
///
/// Updates the user voice state async.
///
/// The guild_id.
/// The user_id.
/// The channel id.
/// If true, suppress.
/// A Task.
internal async Task UpdateUserVoiceStateAsync(ulong guild_id, ulong user_id, ulong channelId, bool? suppress)
{
var pld = new RestGuildUpdateUserVoiceStatePayload
{
ChannelId = channelId,
Suppress = suppress
};
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld));
}
#endregion
#region Guild Scheduled Events
///
/// Creates a scheduled event.
///
internal async Task CreateGuildScheduledEventAsync(ulong guild_id, ulong? channel_id, DiscordScheduledEventEntityMetadata metadata, string name, DateTimeOffset scheduled_start_time, DateTimeOffset? scheduled_end_time, string description, ScheduledEventEntityType type, string reason = null)
{
var pld = new RestGuildScheduledEventCreatePayload
{
ChannelId = channel_id,
EntityMetadata = metadata,
Name = name,
ScheduledStartTime = scheduled_start_time,
ScheduledEndTime = scheduled_end_time,
Description = description,
EntityType = type
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers[REASON_HEADER_NAME] = reason;
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld));
var scheduled_event = JsonConvert.DeserializeObject(res.Response);
var guild = this.Discord.Guilds[guild_id];
scheduled_event.Discord = this.Discord;
if (scheduled_event.Creator != null)
scheduled_event.Creator.Discord = this.Discord;
if (this.Discord is DiscordClient dc)
await dc.OnGuildScheduledEventCreateEventAsync(scheduled_event, guild);
return scheduled_event;
}
///
/// Modifies a scheduled event.
///
internal async Task ModifyGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, Optional channel_id, Optional metadata, Optional name, Optional scheduled_start_time, Optional scheduled_end_time, Optional description, Optional type, Optional status, string reason = null)
{
var pld = new RestGuildSheduledEventModifyPayload
{
ChannelId = channel_id,
EntityMetadata = metadata,
Name = name,
ScheduledStartTime = scheduled_start_time,
ScheduledEndTime = scheduled_end_time,
Description = description,
EntityType = type,
Status = status
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers[REASON_HEADER_NAME] = reason;
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, scheduled_event_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
var scheduled_event = JsonConvert.DeserializeObject(res.Response);
var guild = this.Discord.Guilds[guild_id];
scheduled_event.Discord = this.Discord;
if (scheduled_event.Creator != null)
{
scheduled_event.Creator.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) =>
{
old.Username = scheduled_event.Creator.Username;
old.Discriminator = scheduled_event.Creator.Discriminator;
old.AvatarHash = scheduled_event.Creator.AvatarHash;
old.Flags = scheduled_event.Creator.Flags;
return old;
});
}
if (this.Discord is DiscordClient dc)
await dc.OnGuildScheduledEventUpdateEventAsync(scheduled_event, guild);
return scheduled_event;
}
///
/// Modifies a scheduled event.
///
internal async Task ModifyGuildScheduledEventStatusAsync(ulong guild_id, ulong scheduled_event_id, ScheduledEventStatus status, string reason = null)
{
var pld = new RestGuildSheduledEventModifyPayload
{
Status = status
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers[REASON_HEADER_NAME] = reason;
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, scheduled_event_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
var scheduled_event = JsonConvert.DeserializeObject(res.Response);
var guild = this.Discord.Guilds[guild_id];
scheduled_event.Discord = this.Discord;
if (scheduled_event.Creator != null)
{
scheduled_event.Creator.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) =>
{
old.Username = scheduled_event.Creator.Username;
old.Discriminator = scheduled_event.Creator.Discriminator;
old.AvatarHash = scheduled_event.Creator.AvatarHash;
old.Flags = scheduled_event.Creator.Flags;
return old;
});
}
if (this.Discord is DiscordClient dc)
await dc.OnGuildScheduledEventUpdateEventAsync(scheduled_event, guild);
return scheduled_event;
}
///
/// Gets a scheduled event.
///
/// The guild_id.
/// The event id.
/// Whether to include user count.
internal async Task GetGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, bool? with_user_count)
{
var urlparams = new Dictionary();
if (with_user_count.HasValue)
urlparams["with_user_count"] = with_user_count?.ToString();
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, scheduled_event_id }, out var path);
var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route);
var scheduled_event = JsonConvert.DeserializeObject(res.Response);
var guild = this.Discord.Guilds[guild_id];
scheduled_event.Discord = this.Discord;
if (scheduled_event.Creator != null)
{
scheduled_event.Creator.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) =>
{
old.Username = scheduled_event.Creator.Username;
old.Discriminator = scheduled_event.Creator.Discriminator;
old.AvatarHash = scheduled_event.Creator.AvatarHash;
old.Flags = scheduled_event.Creator.Flags;
return old;
});
}
return scheduled_event;
}
///
/// Gets the guilds scheduled events.
///
/// The guild_id.
/// Whether to include the count of users subscribed to the scheduled event.
internal async Task> ListGuildScheduledEventsAsync(ulong guild_id, bool? with_user_count)
{
var urlparams = new Dictionary();
if (with_user_count.HasValue)
urlparams["with_user_count"] = with_user_count?.ToString();
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route);
var events = new Dictionary();
var events_raw = JsonConvert.DeserializeObject>(res.Response);
var guild = this.Discord.Guilds[guild_id];
foreach (var ev in events_raw)
{
ev.Discord = this.Discord;
if(ev.Creator != null)
{
ev.Creator.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(ev.Creator.Id, ev.Creator, (id, old) =>
{
old.Username = ev.Creator.Username;
old.Discriminator = ev.Creator.Discriminator;
old.AvatarHash = ev.Creator.AvatarHash;
old.Flags = ev.Creator.Flags;
return old;
});
}
events.Add(ev.Id, ev);
}
return new ReadOnlyDictionary(new Dictionary(events));
}
///
/// Deletes a guild sheduled event.
///
/// The guild_id.
/// The sheduled event id.
/// The reason.
internal Task DeleteGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, scheduled_event_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers);
}
///
/// Gets the users who RSVP'd to a sheduled event.
/// Optional with member objects.
/// This endpoint is paginated.
///
/// The guild_id.
/// The sheduled event id.
/// The limit how many users to receive from the event.
/// Get results before the given id.
/// Get results after the given id.
/// Wether to include guild member data. attaches guild_member property to the user object.
internal async Task> GetGuildScheduledEventRSPVUsersAsync(ulong guild_id, ulong scheduled_event_id, int? limit, ulong? before, ulong? after, bool? with_member)
{
var urlparams = new Dictionary();
if (limit != null && limit > 0)
urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture);
if (before != null)
urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture);
if (after != null)
urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture);
if (with_member != null)
urlparams["with_member"] = with_member.Value.ToString(CultureInfo.InvariantCulture);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id{Endpoints.USERS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, scheduled_event_id }, out var path);
var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var rspv_users = JsonConvert.DeserializeObject>(res.Response);
Dictionary rspv = new();
foreach (var rspv_user in rspv_users)
{
rspv_user.Discord = this.Discord;
rspv_user.GuildId = guild_id;
rspv_user.User.Discord = this.Discord;
rspv_user.User = this.Discord.UserCache.AddOrUpdate(rspv_user.User.Id, rspv_user.User, (id, old) =>
{
old.Username = rspv_user.User.Username;
old.Discriminator = rspv_user.User.Discriminator;
old.AvatarHash = rspv_user.User.AvatarHash;
old.BannerHash = rspv_user.User.BannerHash;
old._bannerColor = rspv_user.User._bannerColor;
return old;
});
/*if (with_member.HasValue && with_member.Value && rspv_user.Member != null)
{
rspv_user.Member.Discord = this.Discord;
}*/
rspv.Add(rspv_user.User.Id, rspv_user);
}
return new ReadOnlyDictionary(new Dictionary(rspv));
}
#endregion
#region Channel
///
/// Creates the guild channel async.
///
/// The guild_id.
/// The name.
/// The type.
/// The parent.
/// The topic.
/// The bitrate.
/// The user_limit.
/// The overwrites.
/// If true, nsfw.
/// The per user rate limit.
/// The quality mode.
/// The reason.
/// A Task.
internal async Task CreateGuildChannelAsync(ulong guild_id, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? user_limit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, string reason)
{
var restoverwrites = new List();
if (overwrites != null)
foreach (var ow in overwrites)
restoverwrites.Add(ow.Build());
var pld = new RestChannelCreatePayload
{
Name = name,
Type = type,
Parent = parent,
Topic = topic,
Bitrate = bitrate,
UserLimit = user_limit,
PermissionOverwrites = restoverwrites,
Nsfw = nsfw,
PerUserRateLimit = perUserRateLimit,
QualityMode = qualityMode
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var ret = JsonConvert.DeserializeObject(res.Response);
ret.Discord = this.Discord;
foreach (var xo in ret._permissionOverwrites)
{
xo.Discord = this.Discord;
xo._channel_id = ret.Id;
}
return ret;
}
///
/// Modifies the channel async.
///
/// The channel_id.
/// The name.
/// The position.
/// The topic.
/// If true, nsfw.
/// The parent.
/// The bitrate.
/// The user_limit.
/// The per user rate limit.
/// The rtc region.
/// The quality mode.
/// The default auto archive duration.
/// The type.
/// The permission overwrites.
/// The banner.
/// The reason.
internal Task ModifyChannelAsync(ulong channel_id, string name, int? position, Optional topic, bool? nsfw, Optional parent, int? bitrate, int? user_limit, Optional perUserRateLimit, Optional rtcRegion, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? autoArchiveDuration, Optional type, IEnumerable permissionOverwrites, Optional bannerb64, string reason)
{
List restoverwrites = null;
if (permissionOverwrites != null)
{
restoverwrites = new List();
foreach (var ow in permissionOverwrites)
restoverwrites.Add(ow.Build());
}
var pld = new RestChannelModifyPayload
{
Name = name,
Position = position,
Topic = topic,
Nsfw = nsfw,
Parent = parent,
Bitrate = bitrate,
UserLimit = user_limit,
PerUserRateLimit = perUserRateLimit,
RtcRegion = rtcRegion,
QualityMode = qualityMode,
DefaultAutoArchiveDuration = autoArchiveDuration,
Type = type,
PermissionOverwrites = restoverwrites,
BannerBase64 = bannerb64
};
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.CHANNELS}/:channel_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
}
///
/// Gets the channel async.
///
/// The channel_id.
/// A Task.
internal async Task GetChannelAsync(ulong channel_id)
{
var route = $"{Endpoints.CHANNELS}/:channel_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var ret = JsonConvert.DeserializeObject(res.Response);
ret.Discord = this.Discord;
foreach (var xo in ret._permissionOverwrites)
{
xo.Discord = this.Discord;
xo._channel_id = ret.Id;
}
return ret;
}
///
/// Deletes the channel async.
///
/// The channel_id.
/// The reason.
/// A Task.
internal Task DeleteChannelAsync(ulong channel_id, string reason)
{
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var route = $"{Endpoints.CHANNELS}/:channel_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers);
}
///
/// Gets the message async.
///
/// The channel_id.
/// The message_id.
/// A Task.
internal async Task GetMessageAsync(ulong channel_id, ulong message_id)
{
var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var ret = this.PrepareMessage(JObject.Parse(res.Response));
return ret;
}
///
/// Creates the message async.
///
/// The channel_id.
/// The content.
/// The embeds.
/// The sticker.
/// The reply message id.
/// If true, mention reply.
/// If true, fail on invalid reply.
/// A Task.
internal async Task CreateMessageAsync(ulong channel_id, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply)
{
if (content != null && content.Length > 2000)
throw new ArgumentException("Message content length cannot exceed 2000 characters.");
if (!embeds?.Any() ?? true)
{
if (content == null && sticker == null)
throw new ArgumentException("You must specify message content, a sticker or an embed.");
if (content.Length == 0)
throw new ArgumentException("Message content must not be empty.");
}
if (embeds != null)
foreach (var embed in embeds)
if (embed.Timestamp != null)
embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();
var pld = new RestChannelMessageCreatePayload
{
HasContent = content != null,
Content = content,
StickersIds = sticker is null ? Array.Empty() : new[] {sticker.Id},
IsTTS = false,
HasEmbed = embeds?.Any() ?? false,
Embeds = embeds
};
if (replyMessageId != null)
pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply };
if (replyMessageId != null)
pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply);
var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var ret = this.PrepareMessage(JObject.Parse(res.Response));
return ret;
}
///
/// Creates the message async.
///
/// The channel_id.
/// The builder.
/// A Task.
internal async Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder)
{
builder.Validate();
if (builder.Embeds != null)
foreach (var embed in builder.Embeds)
if (embed?.Timestamp != null)
embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();
var pld = new RestChannelMessageCreatePayload
{
HasContent = builder.Content != null,
Content = builder.Content,
StickersIds = builder.Sticker is null ? Array.Empty() : new[] {builder.Sticker.Id},
IsTTS = builder.IsTTS,
HasEmbed = builder.Embeds != null,
Embeds = builder.Embeds,
Components = builder.Components
};
if (builder.ReplyId != null)
pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply };
pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply);
if (builder.Files.Count == 0)
{
var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var ret = this.PrepareMessage(JObject.Parse(res.Response));
return ret;
}
else
{
+ ulong file_id = 0;
+ List attachments = new(builder.Files.Count);
+ foreach(var file in builder.Files)
+ {
+ DiscordAttachment att = new()
+ {
+ Id = file_id,
+ Discord = this.Discord,
+ Description = file.Description,
+ FileName = file.FileName
+ };
+ attachments.Add(att);
+ file_id++;
+ }
+ pld.Attachments = attachments;
+
+ this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld));
+
var values = new Dictionary
{
["payload_json"] = DiscordJson.SerializeObject(pld)
};
var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
- var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false);
+ try
+ {
- var ret = this.PrepareMessage(JObject.Parse(res.Response));
+ var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false);
+
+ var ret = this.PrepareMessage(JObject.Parse(res.Response));
+
+ foreach (var file in builder._files.Where(x => x.ResetPositionTo.HasValue))
+ {
+ file.Stream.Position = file.ResetPositionTo.Value;
+ }
- foreach (var file in builder._files.Where(x => x.ResetPositionTo.HasValue))
+ return ret;
+ } catch(BadRequestException ex)
{
- file.Stream.Position = file.ResetPositionTo.Value;
+ this.Discord.Logger.LogError(ex.Message);
+ this.Discord.Logger.LogError(ex.StackTrace);
+ this.Discord.Logger.LogDebug(ex.WebResponse.Response);
+ return null;
}
-
- return ret;
}
}
///
/// Gets the guild channels async.
///
/// The guild_id.
/// A Task.
internal async Task> GetGuildChannelsAsync(ulong guild_id)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var channels_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; });
foreach (var ret in channels_raw)
foreach (var xo in ret._permissionOverwrites)
{
xo.Discord = this.Discord;
xo._channel_id = ret.Id;
}
return new ReadOnlyCollection(new List(channels_raw));
}
///
/// Creates the stage instance async.
///
/// The channel_id.
/// The topic.
/// Whether everyone should be notified about the stage.
/// The privacy_level.
/// The reason.
internal async Task CreateStageInstanceAsync(ulong channel_id, string topic, bool send_start_notification, StagePrivacyLevel privacy_level, string reason)
{
var pld = new RestStageInstanceCreatePayload
{
ChannelId = channel_id,
Topic = topic,
PrivacyLevel = privacy_level,
SendStartNotification = send_start_notification
};
var route = $"{Endpoints.STAGE_INSTANCES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path);
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var stageInstance = JsonConvert.DeserializeObject(res.Response);
return stageInstance;
}
///
/// Gets the stage instance async.
///
/// The channel_id.
internal async Task GetStageInstanceAsync(ulong channel_id)
{
var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var stageInstance = JsonConvert.DeserializeObject(res.Response);
return stageInstance;
}
///
/// Modifies the stage instance async.
///
/// The channel_id.
/// The topic.
/// The privacy_level.
/// The reason.
internal Task ModifyStageInstanceAsync(ulong channel_id, Optional topic, Optional privacy_level, string reason)
{
var pld = new RestStageInstanceModifyPayload
{
Topic = topic,
PrivacyLevel = privacy_level
};
var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path);
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld));
}
///
/// Deletes the stage instance async.
///
/// The channel_id.
/// The reason.
internal Task DeleteStageInstanceAsync(ulong channel_id, string reason)
{
var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path);
var headers = Utilities.GetBaseHeaders();
if (!string.IsNullOrWhiteSpace(reason))
headers.Add(REASON_HEADER_NAME, reason);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers);
}
///
/// Gets the channel messages async.
///
/// The channel id.
/// The limit.
/// The before.
/// The after.
/// The around.
/// A Task.
internal async Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around)
{
var urlparams = new Dictionary();
if (around != null)
urlparams["around"] = around?.ToString(CultureInfo.InvariantCulture);
if (before != null)
urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture);
if (after != null)
urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture);
if (limit > 0)
urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture);
var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path);
var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var msgs_raw = JArray.Parse(res.Response);
var msgs = new List