| @@ -1,13 +1,12 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. | /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. | ||||
| /// </summary> | /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||||
| public class RequireBotPermissionAttribute : PreconditionAttribute | public class RequireBotPermissionAttribute : PreconditionAttribute | ||||
| { | { | ||||
| public GuildPermission? GuildPermission { get; } | public GuildPermission? GuildPermission { get; } | ||||
| @@ -1,6 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -192,5 +192,13 @@ namespace Discord | |||||
| throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); | throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); | ||||
| } | } | ||||
| } | } | ||||
| public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name) | |||||
| { | |||||
| for (var i = 0; i < roles.Length; i++) | |||||
| { | |||||
| if (roles[i] == guildId) | |||||
| throw new ArgumentException($"The everyone role cannot be assigned to a user", name); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -120,6 +120,9 @@ namespace Discord.Rest | |||||
| string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) | string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) | ||||
| { | { | ||||
| var args = new CreateGuildParams(name, region.Id); | var args = new CreateGuildParams(name, region.Id); | ||||
| if (jpegIcon != null) | |||||
| args.Icon = new API.Image(jpegIcon); | |||||
| var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); | var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); | ||||
| return RestGuild.Create(client, model); | return RestGuild.Create(client, model); | ||||
| } | } | ||||
| @@ -392,6 +392,7 @@ namespace Discord.API | |||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
| Preconditions.NotEqual(userId, 0, nameof(userId)); | Preconditions.NotEqual(userId, 0, nameof(userId)); | ||||
| Preconditions.NotEqual(roleId, 0, nameof(roleId)); | Preconditions.NotEqual(roleId, 0, nameof(roleId)); | ||||
| Preconditions.NotEqual(roleId, guildId, nameof(roleId), "The Everyone role cannot be added to a user."); | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| @@ -402,6 +403,7 @@ namespace Discord.API | |||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | Preconditions.NotEqual(guildId, 0, nameof(guildId)); | ||||
| Preconditions.NotEqual(userId, 0, nameof(userId)); | Preconditions.NotEqual(userId, 0, nameof(userId)); | ||||
| Preconditions.NotEqual(roleId, 0, nameof(roleId)); | Preconditions.NotEqual(roleId, 0, nameof(roleId)); | ||||
| Preconditions.NotEqual(roleId, guildId, nameof(roleId), "The Everyone role cannot be removed from a user."); | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| @@ -803,7 +805,7 @@ namespace Discord.API | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}"; | |||||
| string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={Uri.EscapeDataString(args.Reason)}"; | |||||
| await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); | await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) | public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) | ||||
| @@ -988,7 +990,7 @@ namespace Discord.API | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var ids = new BucketIds(guildId: guildId); | var ids = new BucketIds(guildId: guildId); | ||||
| reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}"; | |||||
| reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={Uri.EscapeDataString(reason)}"; | |||||
| await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); | await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) | public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) | ||||
| @@ -1000,6 +1002,8 @@ namespace Discord.API | |||||
| bool isCurrentUser = userId == CurrentUserId; | bool isCurrentUser = userId == CurrentUserId; | ||||
| if (args.RoleIds.IsSpecified) | |||||
| Preconditions.NotEveryoneRole(args.RoleIds.Value, guildId, nameof(args.RoleIds)); | |||||
| if (isCurrentUser && args.Nickname.IsSpecified) | if (isCurrentUser && args.Nickname.IsSpecified) | ||||
| { | { | ||||
| var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); | var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); | ||||
| @@ -249,7 +249,7 @@ namespace Discord | |||||
| get => _field.Name; | get => _field.Name; | ||||
| set | set | ||||
| { | { | ||||
| if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name)); | |||||
| if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); | |||||
| if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", 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; | _field.Name = value; | ||||
| } | } | ||||
| @@ -19,7 +19,7 @@ namespace Discord.Rest | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| public bool IsEveryone => Id == Guild.Id; | public bool IsEveryone => Id == Guild.Id; | ||||
| public string Mention => MentionUtils.MentionRole(Id); | |||||
| public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); | |||||
| internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) | internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| @@ -1,5 +1,6 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using System; | |||||
| using System.IO; | |||||
| using Newtonsoft.Json; | |||||
| using Model = Discord.API.Image; | using Model = Discord.API.Image; | ||||
| namespace Discord.Net.Converters | namespace Discord.Net.Converters | ||||
| @@ -23,10 +24,24 @@ namespace Discord.Net.Converters | |||||
| if (image.Stream != null) | if (image.Stream != null) | ||||
| { | { | ||||
| byte[] bytes = new byte[image.Stream.Length - image.Stream.Position]; | |||||
| image.Stream.Read(bytes, 0, bytes.Length); | |||||
| byte[] bytes; | |||||
| int length; | |||||
| if (image.Stream.CanSeek) | |||||
| { | |||||
| bytes = new byte[image.Stream.Length - image.Stream.Position]; | |||||
| length = image.Stream.Read(bytes, 0, bytes.Length); | |||||
| } | |||||
| else | |||||
| { | |||||
| var cloneStream = new MemoryStream(); | |||||
| image.Stream.CopyTo(cloneStream); | |||||
| bytes = new byte[cloneStream.Length]; | |||||
| cloneStream.Position = 0; | |||||
| cloneStream.Read(bytes, 0, bytes.Length); | |||||
| length = (int)cloneStream.Length; | |||||
| } | |||||
| string base64 = Convert.ToBase64String(bytes); | |||||
| string base64 = Convert.ToBase64String(bytes, 0, length); | |||||
| writer.WriteValue($"data:image/jpeg;base64,{base64}"); | writer.WriteValue($"data:image/jpeg;base64,{base64}"); | ||||
| } | } | ||||
| else if (image.Hash != null) | else if (image.Hash != null) | ||||
| @@ -699,7 +699,12 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| else | else | ||||
| { | |||||
| channel = State.GetChannel(data.Id); | |||||
| if (channel != null) | |||||
| return; //Discord may send duplicate CHANNEL_CREATEs for DMs | |||||
| channel = AddPrivateChannel(data, State) as SocketChannel; | channel = AddPrivateChannel(data, State) as SocketChannel; | ||||
| } | |||||
| if (channel != null) | if (channel != null) | ||||
| await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); | await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); | ||||
| @@ -23,7 +23,7 @@ namespace Discord.WebSocket | |||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| public bool IsEveryone => Id == Guild.Id; | public bool IsEveryone => Id == Guild.Id; | ||||
| public string Mention => MentionUtils.MentionRole(Id); | |||||
| public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); | |||||
| public IEnumerable<SocketGuildUser> Members | public IEnumerable<SocketGuildUser> Members | ||||
| => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); | => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); | ||||
| @@ -0,0 +1,86 @@ | |||||
| using System; | |||||
| using Xunit; | |||||
| namespace Discord | |||||
| { | |||||
| public class ColorTests | |||||
| { | |||||
| [Fact] | |||||
| public void Color_New() | |||||
| { | |||||
| Assert.Equal(0u, new Color().RawValue); | |||||
| Assert.Equal(uint.MinValue, new Color(uint.MinValue).RawValue); | |||||
| Assert.Equal(uint.MaxValue, new Color(uint.MaxValue).RawValue); | |||||
| } | |||||
| public void Color_Default() | |||||
| { | |||||
| Assert.Equal(0u, Color.Default.RawValue); | |||||
| Assert.Equal(0, Color.Default.R); | |||||
| Assert.Equal(0, Color.Default.G); | |||||
| Assert.Equal(0, Color.Default.B); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_FromRgb_Byte() | |||||
| { | |||||
| Assert.Equal(0xFF0000u, new Color((byte)255, (byte)0, (byte)0).RawValue); | |||||
| Assert.Equal(0x00FF00u, new Color((byte)0, (byte)255, (byte)0).RawValue); | |||||
| Assert.Equal(0x0000FFu, new Color((byte)0, (byte)0, (byte)255).RawValue); | |||||
| Assert.Equal(0xFFFFFFu, new Color((byte)255, (byte)255, (byte)255).RawValue); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_FromRgb_Int() | |||||
| { | |||||
| Assert.Equal(0xFF0000u, new Color(255, 0, 0).RawValue); | |||||
| Assert.Equal(0x00FF00u, new Color(0, 255, 0).RawValue); | |||||
| Assert.Equal(0x0000FFu, new Color(0, 0, 255).RawValue); | |||||
| Assert.Equal(0xFFFFFFu, new Color(255, 255, 255).RawValue); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_FromRgb_Int_OutOfRange() | |||||
| { | |||||
| Assert.Throws<ArgumentOutOfRangeException>("r", () => new Color(-1024, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("r", () => new Color(1024, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, -1024, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, 1024, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, -1024)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, 1024)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => new Color(-1024, -1024, -1024)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => new Color(1024, 1024, 1024)); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_FromRgb_Float() | |||||
| { | |||||
| Assert.Equal(0xFF0000u, new Color(1.0f, 0, 0).RawValue); | |||||
| Assert.Equal(0x00FF00u, new Color(0, 1.0f, 0).RawValue); | |||||
| Assert.Equal(0x0000FFu, new Color(0, 0, 1.0f).RawValue); | |||||
| Assert.Equal(0xFFFFFFu, new Color(1.0f, 1.0f, 1.0f).RawValue); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_FromRgb_Float_OutOfRange() | |||||
| { | |||||
| Assert.Throws<ArgumentOutOfRangeException>("r", () => new Color(-2.0f, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("r", () => new Color(2.0f, 0, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, -2.0f, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("g", () => new Color(0, 2.0f, 0)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, -2.0f)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>("b", () => new Color(0, 0, 2.0f)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => new Color(-2.0f, -2.0f, -2.0f)); | |||||
| Assert.Throws<ArgumentOutOfRangeException>(() => new Color(2.0f, 2.0f, 2.0f)); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_Red() | |||||
| { | |||||
| Assert.Equal(0xAF, new Color(0xAF1390).R); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_Green() | |||||
| { | |||||
| Assert.Equal(0x13, new Color(0xAF1390).G); | |||||
| } | |||||
| [Fact] | |||||
| public void Color_Blue() | |||||
| { | |||||
| Assert.Equal(0x90, new Color(0xAF1390).B); | |||||
| } | |||||
| } | |||||
| } | |||||