* 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 checktags/1.0
| @@ -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; | |||