Add various property validation in EmbedBuilder (#711)
* Add various property validation in EmbedBuilder * Embed URI changes Changes property types for any URLs in Embeds to System.URI. Adding field name/value null/empty checks. * including property names in argumentexceptions * Adds overall embed length check
This commit is contained in:
committed by
RogueException
parent
5f04e2beba
commit
5601d00285
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
@@ -10,7 +11,7 @@ namespace Discord
|
||||
public string Type { get; }
|
||||
|
||||
public string Description { get; internal set; }
|
||||
public string Url { get; internal set; }
|
||||
public Uri Url { get; internal set; }
|
||||
public string Title { get; internal set; }
|
||||
public DateTimeOffset? Timestamp { get; internal set; }
|
||||
public Color? Color { get; internal set; }
|
||||
@@ -30,7 +31,7 @@ namespace Discord
|
||||
internal Embed(string type,
|
||||
string title,
|
||||
string description,
|
||||
string url,
|
||||
Uri url,
|
||||
DateTimeOffset? timestamp,
|
||||
Color? color,
|
||||
EmbedImage? image,
|
||||
@@ -56,6 +57,8 @@ namespace Discord
|
||||
Fields = fields;
|
||||
}
|
||||
|
||||
public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0;
|
||||
|
||||
public override string ToString() => Title;
|
||||
private string DebuggerDisplay => $"{Title} ({Type})";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
@@ -6,11 +7,11 @@ namespace Discord
|
||||
public struct EmbedAuthor
|
||||
{
|
||||
public string Name { get; internal set; }
|
||||
public string Url { get; internal set; }
|
||||
public string IconUrl { get; internal set; }
|
||||
public string ProxyIconUrl { get; internal set; }
|
||||
public Uri Url { get; internal set; }
|
||||
public Uri IconUrl { get; internal set; }
|
||||
public Uri ProxyIconUrl { get; internal set; }
|
||||
|
||||
internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl)
|
||||
internal EmbedAuthor(string name, Uri url, Uri iconUrl, Uri proxyIconUrl)
|
||||
{
|
||||
Name = name;
|
||||
Url = url;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
@@ -6,10 +7,10 @@ namespace Discord
|
||||
public struct EmbedFooter
|
||||
{
|
||||
public string Text { get; internal set; }
|
||||
public string IconUrl { get; internal set; }
|
||||
public string ProxyUrl { get; internal set; }
|
||||
public Uri IconUrl { get; internal set; }
|
||||
public Uri ProxyUrl { get; internal set; }
|
||||
|
||||
internal EmbedFooter(string text, string iconUrl, string proxyUrl)
|
||||
internal EmbedFooter(string text, Uri iconUrl, Uri proxyUrl)
|
||||
{
|
||||
Text = text;
|
||||
IconUrl = iconUrl;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public struct EmbedImage
|
||||
{
|
||||
public string Url { get; }
|
||||
public string ProxyUrl { get; }
|
||||
public Uri Url { get; }
|
||||
public Uri ProxyUrl { get; }
|
||||
public int? Height { get; }
|
||||
public int? Width { get; }
|
||||
|
||||
internal EmbedImage(string url, string proxyUrl, int? height, int? width)
|
||||
internal EmbedImage(Uri url, Uri proxyUrl, int? height, int? width)
|
||||
{
|
||||
Url = url;
|
||||
ProxyUrl = proxyUrl;
|
||||
@@ -19,6 +20,6 @@ namespace Discord
|
||||
}
|
||||
|
||||
private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})";
|
||||
public override string ToString() => Url;
|
||||
public override string ToString() => Url.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
@@ -6,9 +7,9 @@ namespace Discord
|
||||
public struct EmbedProvider
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Url { get; }
|
||||
public Uri Url { get; }
|
||||
|
||||
internal EmbedProvider(string name, string url)
|
||||
internal EmbedProvider(string name, Uri url)
|
||||
{
|
||||
Name = name;
|
||||
Url = url;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public struct EmbedThumbnail
|
||||
{
|
||||
public string Url { get; }
|
||||
public string ProxyUrl { get; }
|
||||
public Uri Url { get; }
|
||||
public Uri ProxyUrl { get; }
|
||||
public int? Height { get; }
|
||||
public int? Width { get; }
|
||||
|
||||
internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width)
|
||||
internal EmbedThumbnail(Uri url, Uri proxyUrl, int? height, int? width)
|
||||
{
|
||||
Url = url;
|
||||
ProxyUrl = proxyUrl;
|
||||
@@ -19,6 +20,6 @@ namespace Discord
|
||||
}
|
||||
|
||||
private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})";
|
||||
public override string ToString() => Url;
|
||||
public override string ToString() => Url.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public struct EmbedVideo
|
||||
{
|
||||
public string Url { get; }
|
||||
public Uri Url { get; }
|
||||
public int? Height { get; }
|
||||
public int? Width { get; }
|
||||
|
||||
internal EmbedVideo(string url, int? height, int? width)
|
||||
internal EmbedVideo(Uri url, int? height, int? width)
|
||||
{
|
||||
Url = url;
|
||||
Height = height;
|
||||
@@ -17,6 +18,6 @@ namespace Discord
|
||||
}
|
||||
|
||||
private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})";
|
||||
public override string ToString() => Url;
|
||||
public override string ToString() => Url.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Discord
|
||||
{
|
||||
public interface IEmbed
|
||||
{
|
||||
string Url { get; }
|
||||
Uri Url { get; }
|
||||
string Type { get; }
|
||||
string Title { get; }
|
||||
string Description { get; }
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Discord.API
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
[JsonProperty("color")]
|
||||
public uint? Color { get; set; }
|
||||
[JsonProperty("timestamp")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
{
|
||||
@@ -7,10 +8,10 @@ namespace Discord.API
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
[JsonProperty("icon_url")]
|
||||
public string IconUrl { get; set; }
|
||||
public Uri IconUrl { get; set; }
|
||||
[JsonProperty("proxy_icon_url")]
|
||||
public string ProxyIconUrl { get; set; }
|
||||
public Uri ProxyIconUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
{
|
||||
@@ -7,8 +8,8 @@ namespace Discord.API
|
||||
[JsonProperty("text")]
|
||||
public string Text { get; set; }
|
||||
[JsonProperty("icon_url")]
|
||||
public string IconUrl { get; set; }
|
||||
public Uri IconUrl { get; set; }
|
||||
[JsonProperty("proxy_icon_url")]
|
||||
public string ProxyIconUrl { get; set; }
|
||||
public Uri ProxyIconUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
@@ -6,9 +7,9 @@ namespace Discord.API
|
||||
internal class EmbedImage
|
||||
{
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
[JsonProperty("proxy_url")]
|
||||
public string ProxyUrl { get; set; }
|
||||
public Uri ProxyUrl { get; set; }
|
||||
[JsonProperty("height")]
|
||||
public Optional<int> Height { get; set; }
|
||||
[JsonProperty("width")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
@@ -8,6 +9,6 @@ namespace Discord.API
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
@@ -6,9 +7,9 @@ namespace Discord.API
|
||||
internal class EmbedThumbnail
|
||||
{
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
[JsonProperty("proxy_url")]
|
||||
public string ProxyUrl { get; set; }
|
||||
public Uri ProxyUrl { get; set; }
|
||||
[JsonProperty("height")]
|
||||
public Optional<int> Height { get; set; }
|
||||
[JsonProperty("width")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Discord.API
|
||||
@@ -6,7 +7,7 @@ namespace Discord.API
|
||||
internal class EmbedVideo
|
||||
{
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
[JsonProperty("height")]
|
||||
public Optional<int> Height { get; set; }
|
||||
[JsonProperty("width")]
|
||||
|
||||
@@ -8,19 +8,42 @@ namespace Discord
|
||||
{
|
||||
private readonly Embed _embed;
|
||||
|
||||
public const int MaxFieldCount = 25;
|
||||
public const int MaxTitleLength = 256;
|
||||
public const int MaxDescriptionLength = 2048;
|
||||
public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here.
|
||||
|
||||
public EmbedBuilder()
|
||||
{
|
||||
_embed = new Embed("rich");
|
||||
Fields = new List<EmbedFieldBuilder>();
|
||||
}
|
||||
|
||||
public string Title { get { return _embed.Title; } set { _embed.Title = value; } }
|
||||
public string Description { get { return _embed.Description; } set { _embed.Description = value; } }
|
||||
public string Url { get { return _embed.Url; } set { _embed.Url = value; } }
|
||||
public string ThumbnailUrl { get { return _embed.Thumbnail?.Url; } set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } }
|
||||
public string ImageUrl { get { return _embed.Image?.Url; } set { _embed.Image = new EmbedImage(value, null, null, null); } }
|
||||
public DateTimeOffset? Timestamp { get { return _embed.Timestamp; } set { _embed.Timestamp = value; } }
|
||||
public Color? Color { get { return _embed.Color; } set { _embed.Color = value; } }
|
||||
public string Title
|
||||
{
|
||||
get => _embed.Title;
|
||||
set
|
||||
{
|
||||
if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title));
|
||||
_embed.Title = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get => _embed.Description;
|
||||
set
|
||||
{
|
||||
if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description));
|
||||
_embed.Description = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri Url { get => _embed.Url; set { _embed.Url = value; } }
|
||||
public Uri ThumbnailUrl { get => _embed.Thumbnail?.Url; set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } }
|
||||
public Uri ImageUrl { get => _embed.Image?.Url; set { _embed.Image = new EmbedImage(value, null, null, null); } }
|
||||
public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } }
|
||||
public Color? Color { get => _embed.Color; set { _embed.Color = value; } }
|
||||
|
||||
public EmbedAuthorBuilder Author { get; set; }
|
||||
public EmbedFooterBuilder Footer { get; set; }
|
||||
@@ -30,8 +53,10 @@ namespace Discord
|
||||
get => _fields;
|
||||
set
|
||||
{
|
||||
if (value != null) _fields = value;
|
||||
else throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(value));
|
||||
|
||||
if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields));
|
||||
if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields));
|
||||
_fields = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,17 +70,17 @@ namespace Discord
|
||||
Description = description;
|
||||
return this;
|
||||
}
|
||||
public EmbedBuilder WithUrl(string url)
|
||||
public EmbedBuilder WithUrl(Uri url)
|
||||
{
|
||||
Url = url;
|
||||
return this;
|
||||
}
|
||||
public EmbedBuilder WithThumbnailUrl(string thumbnailUrl)
|
||||
public EmbedBuilder WithThumbnailUrl(Uri thumbnailUrl)
|
||||
{
|
||||
ThumbnailUrl = thumbnailUrl;
|
||||
return this;
|
||||
}
|
||||
public EmbedBuilder WithImageUrl(string imageUrl)
|
||||
public EmbedBuilder WithImageUrl(Uri imageUrl)
|
||||
{
|
||||
ImageUrl = imageUrl;
|
||||
return this;
|
||||
@@ -107,7 +132,7 @@ namespace Discord
|
||||
.WithIsInline(false)
|
||||
.WithName(name)
|
||||
.WithValue(value);
|
||||
Fields.Add(field);
|
||||
AddField(field);
|
||||
return this;
|
||||
}
|
||||
public EmbedBuilder AddInlineField(string name, object value)
|
||||
@@ -116,11 +141,16 @@ namespace Discord
|
||||
.WithIsInline(true)
|
||||
.WithName(name)
|
||||
.WithValue(value);
|
||||
Fields.Add(field);
|
||||
AddField(field);
|
||||
return this;
|
||||
}
|
||||
public EmbedBuilder AddField(EmbedFieldBuilder field)
|
||||
{
|
||||
if (Fields.Count >= MaxFieldCount)
|
||||
{
|
||||
throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field));
|
||||
}
|
||||
|
||||
Fields.Add(field);
|
||||
return this;
|
||||
}
|
||||
@@ -128,7 +158,7 @@ namespace Discord
|
||||
{
|
||||
var field = new EmbedFieldBuilder();
|
||||
action(field);
|
||||
Fields.Add(field);
|
||||
this.AddField(field);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -140,6 +170,12 @@ namespace Discord
|
||||
for (int i = 0; i < Fields.Count; i++)
|
||||
fields.Add(Fields[i].Build());
|
||||
_embed.Fields = fields.ToImmutable();
|
||||
|
||||
if (_embed.Length > MaxEmbedLength)
|
||||
{
|
||||
throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}");
|
||||
}
|
||||
|
||||
return _embed;
|
||||
}
|
||||
public static implicit operator Embed(EmbedBuilder builder) => builder?.Build();
|
||||
@@ -149,9 +185,32 @@ namespace Discord
|
||||
{
|
||||
private EmbedField _field;
|
||||
|
||||
public string Name { get { return _field.Name; } set { _field.Name = value; } }
|
||||
public object Value { get { return _field.Value; } set { _field.Value = value.ToString(); } }
|
||||
public bool IsInline { get { return _field.Inline; } set { _field.Inline = value; } }
|
||||
public const int MaxFieldNameLength = 256;
|
||||
public const int MaxFieldValueLength = 1024;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _field.Name;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name));
|
||||
if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name));
|
||||
_field.Name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public object Value
|
||||
{
|
||||
get => _field.Value;
|
||||
set
|
||||
{
|
||||
var stringValue = value.ToString();
|
||||
if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value));
|
||||
if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value));
|
||||
_field.Value = stringValue;
|
||||
}
|
||||
}
|
||||
public bool IsInline { get => _field.Inline; set { _field.Inline = value; } }
|
||||
|
||||
public EmbedFieldBuilder()
|
||||
{
|
||||
@@ -182,9 +241,19 @@ namespace Discord
|
||||
{
|
||||
private EmbedAuthor _author;
|
||||
|
||||
public string Name { get { return _author.Name; } set { _author.Name = value; } }
|
||||
public string Url { get { return _author.Url; } set { _author.Url = value; } }
|
||||
public string IconUrl { get { return _author.IconUrl; } set { _author.IconUrl = value; } }
|
||||
public const int MaxAuthorNameLength = 256;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _author.Name;
|
||||
set
|
||||
{
|
||||
if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name));
|
||||
_author.Name = value;
|
||||
}
|
||||
}
|
||||
public Uri Url { get => _author.Url; set { _author.Url = value; } }
|
||||
public Uri IconUrl { get => _author.IconUrl; set { _author.IconUrl = value; } }
|
||||
|
||||
public EmbedAuthorBuilder()
|
||||
{
|
||||
@@ -196,12 +265,12 @@ namespace Discord
|
||||
Name = name;
|
||||
return this;
|
||||
}
|
||||
public EmbedAuthorBuilder WithUrl(string url)
|
||||
public EmbedAuthorBuilder WithUrl(Uri url)
|
||||
{
|
||||
Url = url;
|
||||
return this;
|
||||
}
|
||||
public EmbedAuthorBuilder WithIconUrl(string iconUrl)
|
||||
public EmbedAuthorBuilder WithIconUrl(Uri iconUrl)
|
||||
{
|
||||
IconUrl = iconUrl;
|
||||
return this;
|
||||
@@ -215,8 +284,18 @@ namespace Discord
|
||||
{
|
||||
private EmbedFooter _footer;
|
||||
|
||||
public string Text { get { return _footer.Text; } set { _footer.Text = value; } }
|
||||
public string IconUrl { get { return _footer.IconUrl; } set { _footer.IconUrl = value; } }
|
||||
public const int MaxFooterTextLength = 2048;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => _footer.Text;
|
||||
set
|
||||
{
|
||||
if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text));
|
||||
_footer.Text = value;
|
||||
}
|
||||
}
|
||||
public Uri IconUrl { get => _footer.IconUrl; set { _footer.IconUrl = value; } }
|
||||
|
||||
public EmbedFooterBuilder()
|
||||
{
|
||||
@@ -228,7 +307,7 @@ namespace Discord
|
||||
Text = text;
|
||||
return this;
|
||||
}
|
||||
public EmbedFooterBuilder WithIconUrl(string iconUrl)
|
||||
public EmbedFooterBuilder WithIconUrl(Uri iconUrl)
|
||||
{
|
||||
IconUrl = iconUrl;
|
||||
return this;
|
||||
|
||||
Reference in New Issue
Block a user