From 8d533930faf98665c3f13827f66e102c866d4926 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 10 Nov 2017 15:26:58 -0500 Subject: [PATCH 01/27] Move CommandExecuted invoking up a scope for generic Task commands Resovles #870 --- src/Discord.Net.Commands/Info/CommandInfo.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 9ca7ffff3..6bb621f94 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -199,10 +199,13 @@ namespace Discord.Commands return result; } else + { await task.ConfigureAwait(false); + var result = ExecuteResult.FromSuccess(); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + } var executeResult = ExecuteResult.FromSuccess(); - await Module.Service._commandExecutedEvent.InvokeAsync(this, context, executeResult).ConfigureAwait(false); return executeResult; } catch (Exception ex) From 9979a027d54c355fb344eee975ded1ed67a6d291 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 10 Nov 2017 15:39:33 -0500 Subject: [PATCH 02/27] Change default InviteAge to 24 hours Resolves #859 --- src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs | 2 +- src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs | 2 +- src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs | 2 +- .../Entities/Channels/SocketGuildChannel.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 3d08a8c51..c7cf0b3c2 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -20,7 +20,7 @@ namespace Discord /// The time (in seconds) until the invite expires. Set to null to never expire. /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// If true, a user accepting this invite will be kicked from the guild after closing their client. - Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); /// Returns a collection of all invites to this channel. Task> GetInvitesAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 5e335446f..1ce1c8368 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -119,7 +119,7 @@ namespace Discord.Rest public async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); public override string ToString() => Name; diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs index 48eb8ec3e..401263555 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -51,7 +51,7 @@ namespace Discord.Rpc public async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); public override string ToString() => Name; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 16453b9fb..8e24a5196 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -113,7 +113,7 @@ namespace Discord.WebSocket public async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); public new virtual SocketGuildUser GetUser(ulong id) => null; From cf8de42b624074f736ec15f76f20056a8d1f784a Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Mon, 20 Nov 2017 11:54:10 -0800 Subject: [PATCH 03/27] Add GuildPermissions and ChannelPermissions Unit Tests (#873) * initial commit * Add GuildPermission tests for constructor and modify parameters * Fixed GuildPermission All value. Previous value had an additional digit that would still resolve to correct permission flags, but raw value would be incorrect. This matches the result of the sum of all GuildPermission flags * Added raw value check to guild permission modify tests * Add ChannelPermissions tests --- .../Entities/Permissions/GuildPermissions.cs | 2 +- .../Tests.ChannelPermissions.cs | 324 ++++++++++++++++++ .../Tests.GuildPermissions.cs | 304 ++++++++++++++++ 3 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 test/Discord.Net.Tests/Tests.ChannelPermissions.cs create mode 100644 test/Discord.Net.Tests/Tests.GuildPermissions.cs diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 4ee3b0fc6..a880e62ca 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -11,7 +11,7 @@ namespace Discord /// Gets a GuildPermissions that grants all guild permissions for webhook users. public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); /// Gets a GuildPermissions that grants all guild permissions. - public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_11111111110011_111111); + public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111110011_111111); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs new file mode 100644 index 000000000..c5b22e277 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -0,0 +1,324 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + [Fact] + public async Task TestChannelPermission() + { + var perm = new ChannelPermissions(); + + // check initial values + Assert.Equal((ulong)0, perm.RawValue); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // permissions list empty by default + Assert.Empty(perm.ToList()); + + // test modify with no parameters + var copy = perm.Modify(); + Assert.Equal((ulong)0, copy.RawValue); + + // test the values that are returned by ChannelPermission.All + Assert.Equal((ulong)0, ChannelPermissions.None.RawValue); + + // for text channels + ulong textChannel = (ulong)( ChannelPermission.CreateInstantInvite + | ChannelPermission.ManageChannels + | ChannelPermission.AddReactions + | ChannelPermission.ReadMessages + | ChannelPermission.SendMessages + | ChannelPermission.SendTTSMessages + | ChannelPermission.ManageMessages + | ChannelPermission.EmbedLinks + | ChannelPermission.AttachFiles + | ChannelPermission.ReadMessageHistory + | ChannelPermission.MentionEveryone + | ChannelPermission.UseExternalEmojis + | ChannelPermission.ManageRoles + | ChannelPermission.ManageWebhooks); + + Assert.Equal(textChannel, ChannelPermissions.Text.RawValue); + + // voice channels + ulong voiceChannel = (ulong)( + ChannelPermission.CreateInstantInvite + | ChannelPermission.ManageChannels + | ChannelPermission.Connect + | ChannelPermission.Speak + | ChannelPermission.MuteMembers + | ChannelPermission.DeafenMembers + | ChannelPermission.MoveMembers + | ChannelPermission.UseVAD + | ChannelPermission.ManageRoles); + + Assert.Equal(voiceChannel, ChannelPermissions.Voice.RawValue); + + // DM Channels + ulong dmChannel = (ulong)( + ChannelPermission.ReadMessages + | ChannelPermission.SendMessages + | ChannelPermission.EmbedLinks + | ChannelPermission.AttachFiles + | ChannelPermission.ReadMessageHistory + | ChannelPermission.UseExternalEmojis + | ChannelPermission.Connect + | ChannelPermission.Speak + | ChannelPermission.UseVAD + ); + Assert.Equal(dmChannel, ChannelPermissions.DM.RawValue); + + // group channel + ulong groupChannel = (ulong)( + ChannelPermission.SendMessages + | ChannelPermission.EmbedLinks + | ChannelPermission.AttachFiles + | ChannelPermission.SendTTSMessages + | ChannelPermission.Connect + | ChannelPermission.Speak + | ChannelPermission.UseVAD + ); + Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); + } + + public async Task TestChannelPermissionModify() + { + // test channel permission modify + + var perm = new ChannelPermissions(); + + // ensure that the permission is initially false + Assert.False(perm.CreateInstantInvite); + + // ensure that when modified it works + perm = perm.Modify(createInstantInvite: true); + Assert.True(perm.CreateInstantInvite); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.CreateInstantInvite); + + // set false again, move on to next permission + perm = perm.Modify(createInstantInvite: false); + Assert.False(perm.CreateInstantInvite); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageChannel); + + perm = perm.Modify(manageChannel: true); + Assert.True(perm.ManageChannel); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageChannels); + + perm = perm.Modify(manageChannel: false); + Assert.False(perm.ManageChannel); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.AddReactions); + + perm = perm.Modify(addReactions: true); + Assert.True(perm.AddReactions); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.AddReactions); + + perm = perm.Modify(addReactions: false); + Assert.False(perm.AddReactions); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ReadMessages); + + perm = perm.Modify(readMessages: true); + Assert.True(perm.ReadMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessages); + + perm = perm.Modify(readMessages: false); + Assert.False(perm.ReadMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.SendMessages); + + perm = perm.Modify(sendMessages: true); + Assert.True(perm.SendMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendMessages); + + perm = perm.Modify(sendMessages: false); + Assert.False(perm.SendMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.SendTTSMessages); + + perm = perm.Modify(sendTTSMessages: true); + Assert.True(perm.SendTTSMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendTTSMessages); + + perm = perm.Modify(sendTTSMessages: false); + Assert.False(perm.SendTTSMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageMessages); + + perm = perm.Modify(manageMessages: true); + Assert.True(perm.ManageMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageMessages); + + perm = perm.Modify(manageMessages: false); + Assert.False(perm.ManageMessages); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.EmbedLinks); + + perm = perm.Modify(embedLinks: true); + Assert.True(perm.EmbedLinks); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.EmbedLinks); + + perm = perm.Modify(embedLinks: false); + Assert.False(perm.EmbedLinks); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.AttachFiles); + + perm = perm.Modify(attachFiles: true); + Assert.True(perm.AttachFiles); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.AttachFiles); + + perm = perm.Modify(attachFiles: false); + Assert.False(perm.AttachFiles); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ReadMessageHistory); + + perm = perm.Modify(readMessageHistory: true); + Assert.True(perm.ReadMessageHistory); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessageHistory); + + perm = perm.Modify(readMessageHistory: false); + Assert.False(perm.ReadMessageHistory); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.MentionEveryone); + + perm = perm.Modify(mentionEveryone: true); + Assert.True(perm.MentionEveryone); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MentionEveryone); + + perm = perm.Modify(mentionEveryone: false); + Assert.False(perm.MentionEveryone); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.UseExternalEmojis); + + perm = perm.Modify(useExternalEmojis: true); + Assert.True(perm.UseExternalEmojis); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseExternalEmojis); + + perm = perm.Modify(useExternalEmojis: false); + Assert.False(perm.UseExternalEmojis); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.Connect); + + perm = perm.Modify(connect: true); + Assert.True(perm.Connect); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Connect); + + perm = perm.Modify(connect: false); + Assert.False(perm.Connect); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.Speak); + + perm = perm.Modify(speak: true); + Assert.True(perm.Speak); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Speak); + + perm = perm.Modify(speak: false); + Assert.False(perm.Speak); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.MuteMembers); + + perm = perm.Modify(muteMembers: true); + Assert.True(perm.MuteMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MuteMembers); + + perm = perm.Modify(muteMembers: false); + Assert.False(perm.MuteMembers); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.DeafenMembers); + + perm = perm.Modify(deafenMembers: true); + Assert.True(perm.DeafenMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.DeafenMembers); + + perm = perm.Modify(deafenMembers: false); + Assert.False(perm.DeafenMembers); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.MoveMembers); + + perm = perm.Modify(moveMembers: true); + Assert.True(perm.MoveMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MoveMembers); + + perm = perm.Modify(moveMembers: false); + Assert.False(perm.MoveMembers); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.UseVAD); + + perm = perm.Modify(useVoiceActivation: true); + Assert.True(perm.UseVAD); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseVAD); + + perm = perm.Modify(useVoiceActivation: false); + Assert.False(perm.UseVAD); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageRoles); + + perm = perm.Modify(manageRoles: true); + Assert.True(perm.ManageRoles); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageRoles); + + perm = perm.Modify(manageRoles: false); + Assert.False(perm.ManageRoles); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + + // individual permission test + Assert.False(perm.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: true); + Assert.True(perm.ManageWebhooks); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: false); + Assert.False(perm.ManageWebhooks); + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + } + + [Fact] + public async Task TestChannelTypeResolution() + { + ITextChannel someChannel = null; + // null channels will throw exception + Assert.Throws(() => ChannelPermissions.All(someChannel)); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs new file mode 100644 index 000000000..a79706b9c --- /dev/null +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -0,0 +1,304 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + [Fact] + public async Task TestGuildPermission() + { + // Test Guild Permission Constructors + var perm = new GuildPermissions(); + + // the default raw value is 0 + Assert.Equal((ulong)0, perm.RawValue); + // also check that it is the same as none + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // permissions list is empty by default + Assert.Empty(perm.ToList()); + Assert.NotNull(perm.ToList()); + + // Test modify with no parameters + var copy = perm.Modify(); + // ensure that the raw values match + Assert.Equal((ulong)0, copy.RawValue); + + // test GuildPermissions.All + ulong sumOfAllGuildPermissions = 0; + foreach(var v in Enum.GetValues(typeof(GuildPermission))) + { + sumOfAllGuildPermissions |= (ulong)v; + } + + // assert that the raw values match + Assert.Equal(sumOfAllGuildPermissions, GuildPermissions.All.RawValue); + Assert.Equal((ulong)0, GuildPermissions.None.RawValue); + + // assert that GuildPermissions.All contains the same number of permissions as the + // GuildPermissions enum + Assert.Equal(Enum.GetValues(typeof(GuildPermission)).Length, GuildPermissions.All.ToList().Count); + + // assert that webhook has the same raw value + ulong webHookPermissions = (ulong)( + GuildPermission.SendMessages | GuildPermission.SendTTSMessages | GuildPermission.EmbedLinks | + GuildPermission.AttachFiles); + Assert.Equal(webHookPermissions, GuildPermissions.Webhook.RawValue); + } + + [Fact] + public async Task TestGuildPermissionModify() + { + var perm = new GuildPermissions(); + + // tests each of the parameters of Modify one by one + + // test modify with each of the parameters + // test initially false state + Assert.False(perm.CreateInstantInvite); + + // ensure that when we modify it the parameter works + perm = perm.Modify(createInstantInvite: true); + Assert.True(perm.CreateInstantInvite); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.CreateInstantInvite); + + // set it false again, then move on to the next permission + perm = perm.Modify(createInstantInvite: false); + Assert.False(perm.CreateInstantInvite); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(kickMembers: true); + Assert.True(perm.KickMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.KickMembers); + + perm = perm.Modify(kickMembers: false); + Assert.False(perm.KickMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(banMembers: true); + Assert.True(perm.BanMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.BanMembers); + + perm = perm.Modify(banMembers: false); + Assert.False(perm.BanMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(administrator: true); + Assert.True(perm.Administrator); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Administrator); + + perm = perm.Modify(administrator: false); + Assert.False(perm.Administrator); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageChannels: true); + Assert.True(perm.ManageChannels); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageChannels); + + perm = perm.Modify(manageChannels: false); + Assert.False(perm.ManageChannels); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageGuild: true); + Assert.True(perm.ManageGuild); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageGuild); + + perm = perm.Modify(manageGuild: false); + Assert.False(perm.ManageGuild); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(addReactions: true); + Assert.True(perm.AddReactions); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.AddReactions); + + perm = perm.Modify(addReactions: false); + Assert.False(perm.AddReactions); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(viewAuditLog: true); + Assert.True(perm.ViewAuditLog); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ViewAuditLog); + + perm = perm.Modify(viewAuditLog: false); + Assert.False(perm.ViewAuditLog); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(readMessages: true); + Assert.True(perm.ReadMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessages); + + perm = perm.Modify(readMessages: false); + Assert.False(perm.ReadMessages); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + + // individual permission test + perm = perm.Modify(sendMessages: true); + Assert.True(perm.SendMessages); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendMessages); + + perm = perm.Modify(sendMessages: false); + Assert.False(perm.SendMessages); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(embedLinks: true); + Assert.True(perm.EmbedLinks); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.EmbedLinks); + + perm = perm.Modify(embedLinks: false); + Assert.False(perm.EmbedLinks); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(attachFiles: true); + Assert.True(perm.AttachFiles); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.AttachFiles); + + perm = perm.Modify(attachFiles: false); + Assert.False(perm.AttachFiles); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(readMessageHistory: true); + Assert.True(perm.ReadMessageHistory); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessageHistory); + + perm = perm.Modify(readMessageHistory: false); + Assert.False(perm.ReadMessageHistory); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(mentionEveryone: true); + Assert.True(perm.MentionEveryone); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MentionEveryone); + + perm = perm.Modify(mentionEveryone: false); + Assert.False(perm.MentionEveryone); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(useExternalEmojis: true); + Assert.True(perm.UseExternalEmojis); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseExternalEmojis); + + perm = perm.Modify(useExternalEmojis: false); + Assert.False(perm.UseExternalEmojis); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(connect: true); + Assert.True(perm.Connect); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Connect); + + perm = perm.Modify(connect: false); + Assert.False(perm.Connect); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(speak: true); + Assert.True(perm.Speak); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.Speak); + + perm = perm.Modify(speak: false); + Assert.False(perm.Speak); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(muteMembers: true); + Assert.True(perm.MuteMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MuteMembers); + + perm = perm.Modify(muteMembers: false); + Assert.False(perm.MuteMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(deafenMembers: true); + Assert.True(perm.DeafenMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.DeafenMembers); + + perm = perm.Modify(deafenMembers: false); + Assert.False(perm.DeafenMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(moveMembers: true); + Assert.True(perm.MoveMembers); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.MoveMembers); + + perm = perm.Modify(moveMembers: false); + Assert.False(perm.MoveMembers); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(useVoiceActivation: true); + Assert.True(perm.UseVAD); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseVAD); + + perm = perm.Modify(useVoiceActivation: false); + Assert.False(perm.UseVAD); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(changeNickname: true); + Assert.True(perm.ChangeNickname); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ChangeNickname); + + perm = perm.Modify(changeNickname: false); + Assert.False(perm.ChangeNickname); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageNicknames: true); + Assert.True(perm.ManageNicknames); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageNicknames); + + perm = perm.Modify(manageNicknames: false); + Assert.False(perm.ManageNicknames); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageRoles: true); + Assert.True(perm.ManageRoles); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageRoles); + + perm = perm.Modify(manageRoles: false); + Assert.False(perm.ManageRoles); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageWebhooks: true); + Assert.True(perm.ManageWebhooks); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageWebhooks); + + perm = perm.Modify(manageWebhooks: false); + Assert.False(perm.ManageWebhooks); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + // individual permission test + perm = perm.Modify(manageEmojis: true); + Assert.True(perm.ManageEmojis); + Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageEmojis); + + perm = perm.Modify(manageEmojis: false); + Assert.False(perm.ManageEmojis); + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + + } + + } +} From b4bf046ad483d1daa6f5a8b73f249602ca489e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Brawa=C5=84ski?= Date: Mon, 20 Nov 2017 21:02:42 +0100 Subject: [PATCH 04/27] Implemented emoji endpoints (#835) * Implemented emoji endpoints. * Fixed: now using API entities for REST client. * Removed emoji listing endpoint, as per @foxbot's request. --- .../Entities/Emotes/EmoteProperties.cs | 10 +++++ .../Entities/Guilds/IGuild.cs | 9 ++++ .../API/Rest/CreateGuildEmoteParams.cs | 16 +++++++ .../API/Rest/ModifyGuildEmoteParams.cs | 14 ++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 44 +++++++++++++++++++ .../Entities/Guilds/GuildHelper.cs | 41 +++++++++++++++++ .../Entities/Guilds/RestGuild.cs | 10 +++++ .../Entities/Guilds/SocketGuild.cs | 10 +++++ 8 files changed, 154 insertions(+) create mode 100644 src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs create mode 100644 src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs diff --git a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs new file mode 100644 index 000000000..be24d306c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Discord +{ + public class EmoteProperties + { + public Optional Name { get; set; } + public Optional> Roles { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 3ded9e038..6b2d24cc6 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -117,5 +117,14 @@ namespace Discord Task DownloadUsersAsync(); /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + + /// Gets a specific emote from this guild. + Task GetEmoteAsync(ulong id, RequestOptions options = null); + /// Creates a new emote in this guild. + Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); + /// Modifies an existing emote in this guild. + Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); + /// Deletes an existing emote from this guild. + Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs new file mode 100644 index 000000000..308199820 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateGuildEmoteParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("image")] + public Image Image { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs new file mode 100644 index 000000000..a2295dd5d --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildEmoteParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 6d551aa95..4e65b19d2 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1066,6 +1066,50 @@ namespace Discord.API return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } + //Guild emoji + public async Task GetGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + } + + public async Task CreateGuildEmoteAsync(ulong guildId, Rest.CreateGuildEmoteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + Preconditions.NotNull(args.Image.Stream, nameof(args.Image)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options); + } + + public async Task ModifyGuildEmoteAsync(ulong guildId, ulong emoteId, ModifyGuildEmoteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options); + } + + public async Task DeleteGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + } + //Users public async Task GetUserAsync(ulong userId, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 2fa29928c..58b7ed7f9 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -253,5 +253,46 @@ namespace Discord.Rest model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); return model.Pruned; } + + //Emotes + public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options); + return emote.ToEntity(); + } + public static async Task CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional> roles, + RequestOptions options) + { + var apiargs = new CreateGuildEmoteParams + { + Name = name, + Image = image.ToModel() + }; + if (roles.IsSpecified) + apiargs.RoleIds = roles.Value?.Select(xr => xr.Id)?.ToArray(); + + var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options); + return emote.ToEntity(); + } + public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, + RequestOptions options) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + + var props = new EmoteProperties(); + func(props); + + var apiargs = new ModifyGuildEmoteParams + { + Name = props.Name + }; + if (props.Roles.IsSpecified) + apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id)?.ToArray(); + + var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options); + return emote.ToEntity(); + } + public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + => client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index aee305951..de4b89e39 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -260,6 +260,16 @@ namespace Discord.Rest public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + //Emotes + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetEmoteAsync(this, Discord, id, options); + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) + => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + //IGuild bool IGuild.Available => Available; IAudioClient IGuild.AudioClient => null; diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 6001e4799..b639a9cf7 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -433,6 +433,16 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + //Emotes + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetEmoteAsync(this, Discord, id, options); + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) + => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + //Voice States internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { From e5dfb6c3e54f09e51223b5706ce96ddec97c8291 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 21 Nov 2017 16:33:46 -0500 Subject: [PATCH 05/27] Fix null channel being passed in RequirePermission preconditions (#886) * Fix null channel being passed in RequirePermission preconditions * c#7 pattern matching --- .../Preconditions/RequireBotPermissionAttribute.cs | 6 ++---- .../Preconditions/RequireUserPermissionAttribute.cs | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 6be142a45..104252799 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -57,13 +57,11 @@ namespace Discord.Commands if (ChannelPermission.HasValue) { - var guildChannel = context.Channel as IGuildChannel; - ChannelPermissions perms; - if (guildChannel != null) + if (context.Channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); else - perms = ChannelPermissions.All(guildChannel); + perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 0179aa0ac..14121f35b 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -56,13 +56,11 @@ namespace Discord.Commands if (ChannelPermission.HasValue) { - var guildChannel = context.Channel as IGuildChannel; - ChannelPermissions perms; - if (guildChannel != null) + if (context.Channel is IGuildChannel guildChannel) perms = guildUser.GetPermissions(guildChannel); else - perms = ChannelPermissions.All(guildChannel); + perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); From c461201fa5ea617d3edb0ded41d2e75e9352779a Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Nov 2017 19:39:26 -0500 Subject: [PATCH 06/27] Fix async warnings --- test/Discord.Net.Tests/Tests.ChannelPermissions.cs | 6 +++--- test/Discord.Net.Tests/Tests.GuildPermissions.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs index c5b22e277..92234e88b 100644 --- a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public async Task TestChannelPermission() + public void TestChannelPermission() { var perm = new ChannelPermissions(); @@ -84,7 +84,7 @@ namespace Discord Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); } - public async Task TestChannelPermissionModify() + public void TestChannelPermissionModify() { // test channel permission modify @@ -314,7 +314,7 @@ namespace Discord } [Fact] - public async Task TestChannelTypeResolution() + public void TestChannelTypeResolution() { ITextChannel someChannel = null; // null channels will throw exception diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs index a79706b9c..dc51600cf 100644 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public async Task TestGuildPermission() + public void TestGuildPermission() { // Test Guild Permission Constructors var perm = new GuildPermissions(); @@ -49,7 +49,7 @@ namespace Discord } [Fact] - public async Task TestGuildPermissionModify() + public void TestGuildPermissionModify() { var perm = new GuildPermissions(); From bbad052ec7b5dc89e43825064aaf7d0e31bdbbf3 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Nov 2017 20:09:07 -0500 Subject: [PATCH 07/27] Update invite link thank you @SinisterRectus :slight_smile: --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b58d4579..bd0ef20c7 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) -[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTVjAMPx) +[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) An unofficial .NET API Wrapper for the Discord client (http://discordapp.com). -Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). +Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR). ## Installation ### Stable (NuGet) From 39b5d0e74cc416a0d84d741c1d5585155c1d2075 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Nov 2017 20:13:10 -0500 Subject: [PATCH 08/27] Bumped version to 2.0.0-beta --- Discord.Net.targets | 4 ++-- appveyor.yml | 2 +- src/Discord.Net/Discord.Net.nuspec | 38 +++++++++++++++--------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 95eccd790..3f623c619 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ - 2.0.0-alpha - + 2.0.0 + beta RogueException discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/appveyor.yml b/appveyor.yml index d94e2ad68..3bf70c09c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,7 +34,7 @@ after_build: if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" } - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 309532615..f904f4126 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.0.0-alpha$suffix$ + 2.0.0-beta$suffix$ Discord.Net Discord.Net Contributors RogueException @@ -13,28 +13,28 @@ false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 678a7238e6172ae539a5746fb5bc1b2c734258a0 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 7 Dec 2017 16:47:01 -0500 Subject: [PATCH 09/27] Allow users to opt-in to proxies (#888) * Allow users to opt-in to proxies * Allow opting in to proxies on the WebSocket --- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 4 ++-- .../Net/DefaultRestClientProvider.cs | 23 +++++++++++------- .../Net/DefaultWebSocketClient.cs | 7 ++++-- .../Net/DefaultWebSocketClientProvider.cs | 24 ++++++++++++------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index a54107829..637099fd6 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -22,7 +22,7 @@ namespace Discord.Net.Rest private CancellationToken _cancelToken; private bool _isDisposed; - public DefaultRestClient(string baseUrl) + public DefaultRestClient(string baseUrl, bool useProxy = false) { _baseUrl = baseUrl; @@ -30,7 +30,7 @@ namespace Discord.Net.Rest { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies = false, - UseProxy = false + UseProxy = useProxy, }); SetHeader("accept-encoding", "gzip, deflate"); diff --git a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs index 311a53562..e0e776549 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -4,16 +4,21 @@ namespace Discord.Net.Rest { public static class DefaultRestClientProvider { - public static readonly RestClientProvider Instance = url => + public static readonly RestClientProvider Instance = Create(); + + public static RestClientProvider Create(bool useProxy = false) { - try - { - return new DefaultRestClient(url); - } - catch (PlatformNotSupportedException ex) + return url => { - throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); - } - }; + try + { + return new DefaultRestClient(url, useProxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); + } + }; + } } } diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 282ae210a..a250acec9 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -23,18 +24,20 @@ namespace Discord.Net.WebSockets private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; private ClientWebSocket _client; + private IWebProxy _proxy; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private bool _isDisposed, _isDisconnecting; - public DefaultWebSocketClient() + public DefaultWebSocketClient(IWebProxy proxy = null) { _lock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _headers = new Dictionary(); + _proxy = proxy; } private void Dispose(bool disposing) { @@ -70,7 +73,7 @@ namespace Discord.Net.WebSockets _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; _client = new ClientWebSocket(); - _client.Options.Proxy = null; + _client.Options.Proxy = _proxy; _client.Options.KeepAliveInterval = TimeSpan.Zero; foreach (var header in _headers) { diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index 04b3f8388..68bd67c5b 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -1,21 +1,27 @@ using System; +using System.Net; namespace Discord.Net.WebSockets { public static class DefaultWebSocketProvider { #if DEFAULTWEBSOCKET - public static readonly WebSocketProvider Instance = () => + public static readonly WebSocketProvider Instance = Create(); + + public static WebSocketProvider Create(IWebProxy proxy = null) { - try - { - return new DefaultWebSocketClient(); - } - catch (PlatformNotSupportedException ex) + return () => { - throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); - } - }; + try + { + return new DefaultWebSocketClient(proxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); + } + }; + } #else public static readonly WebSocketProvider Instance = () => { From 34b4e5a6d2f39a43ab3121f997cc3c92aca25165 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 23 Dec 2017 14:58:35 -0500 Subject: [PATCH 10/27] Refactor Games, support reading Rich Presences (#877) * Add API-level support for Rich Presences * Add library-level support for Game presences * Add model conversions for outgoing+incoming rich presences * Refactored Game into Activities * Integrated Activities with user entities rebase hell from 5f3cb947a92f4fd01cc4df47ca548180036b47f3 * Fix JSON converters for Activities * Finish rebase, activity should be set on BaseSocketClient * Use ApplicationId to define a rich presence * Added SetActivityAsync to Base and Sharded Socket clients * Remove public parameterless Game constructor * Remove GameAssets, refactored to GameAsset * Hide constructors for types that should be read-only * Revert changes to Discord.Net.sln got damned visual studio caching * Refactor GameParty to use dedicated current/capacity values Per feedback from @khionu --- src/Discord.Net.Core/CDN.cs | 6 ++ .../Entities/Activities/Game.cs | 19 +++++ .../Entities/Activities/GameAsset.cs | 15 ++++ .../Entities/Activities/GameParty.cs | 11 +++ .../Entities/Activities/GameSecrets.cs | 16 ++++ .../Entities/Activities/GameTimestamps.cs | 16 ++++ .../Entities/Activities/IActivity.cs | 13 +++ .../Entities/Activities/RichGame.cs | 22 +++++ .../Entities/Activities/StreamingGame.cs | 21 +++++ src/Discord.Net.Core/Entities/Users/Game.cs | 24 ------ .../Entities/Users/IPresence.cs | 4 +- src/Discord.Net.Rest/API/Common/Game.cs | 16 ++++ src/Discord.Net.Rest/API/Common/GameAssets.cs | 16 ++++ src/Discord.Net.Rest/API/Common/GameParty.cs | 12 +++ .../API/Common/GameSecrets.cs | 14 ++++ .../API/Common/GameTimestamps.cs | 15 ++++ .../API/UnixTimestampAttribute.cs | 7 ++ src/Discord.Net.Rest/Discord.Net.Rest.csproj | 3 +- .../Entities/Users/RestUser.cs | 2 +- .../Net/Converters/DiscordContractResolver.cs | 6 ++ .../Net/Converters/UnixTimestampConverter.cs | 28 +++++++ src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 2 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 3 +- .../DiscordShardedClient.cs | 13 ++- .../DiscordSocketClient.cs | 38 +++++---- .../Entities/Users/SocketPresence.cs | 10 +-- .../Entities/Users/SocketUser.cs | 2 +- .../Extensions/EntityExtensions.cs | 80 ++++++++++++++++++- 28 files changed, 376 insertions(+), 58 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/Game.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameAsset.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameParty.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameSecrets.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/IActivity.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/RichGame.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/StreamingGame.cs delete mode 100644 src/Discord.Net.Core/Entities/Users/Game.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameAssets.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameParty.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameSecrets.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameTimestamps.cs create mode 100644 src/Discord.Net.Rest/API/UnixTimestampAttribute.cs create mode 100644 src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d3ade3722..415c0c30d 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -22,6 +22,12 @@ namespace Discord public static string GetEmojiUrl(ulong emojiId) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) + { + string extension = FormatToExtension(format, ""); + return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; + } + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs new file mode 100644 index 000000000..f2b7e8eb6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Game : IActivity + { + public string Name { get; internal set; } + + internal Game() { } + public Game(string name) + { + Name = name; + } + + public override string ToString() => Name; + private string DebuggerDisplay => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs new file mode 100644 index 000000000..385f37214 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public class GameAsset + { + internal GameAsset() { } + + internal ulong ApplicationId { get; set; } + + public string Text { get; internal set; } + public string ImageId { get; internal set; } + + public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs new file mode 100644 index 000000000..dbfe5b6ce --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public class GameParty + { + internal GameParty() { } + + public string Id { get; internal set; } + public int Members { get; internal set; } + public int Capacity { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs new file mode 100644 index 000000000..e9d988ba9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class GameSecrets + { + public string Match { get; } + public string Join { get; } + public string Spectate { get; } + + internal GameSecrets(string match, string join, string spectate) + { + Match = match; + Join = join; + Spectate = spectate; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs new file mode 100644 index 000000000..8c8c992fa --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class GameTimestamps + { + public DateTimeOffset? Start { get; } + public DateTimeOffset? End { get; } + + internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) + { + Start = start; + End = end; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs new file mode 100644 index 000000000..0dcf34273 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IActivity + { + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs new file mode 100644 index 000000000..e66eac1d2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RichGame : Game + { + internal RichGame() { } + + public string Details { get; internal set;} + public string State { get; internal set;} + public ulong ApplicationId { get; internal set; } + public GameAsset SmallAsset { get; internal set; } + public GameAsset LargeAsset { get; internal set; } + public GameParty Party { get; internal set; } + public GameSecrets Secrets { get; internal set; } + public GameTimestamps Timestamps { get; internal set; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Rich)"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs new file mode 100644 index 000000000..140024272 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class StreamingGame : Game + { + public string Url { get; internal set; } + public StreamType StreamType { get; internal set; } + + public StreamingGame(string name, string url, StreamType streamType) + { + Name = name; + Url = url; + StreamType = streamType; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Url})"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs deleted file mode 100644 index 3405b0dd4..000000000 --- a/src/Discord.Net.Core/Entities/Users/Game.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Game - { - public string Name { get; } - public string StreamUrl { get; } - public StreamType StreamType { get; } - - public Game(string name, string streamUrl, StreamType type) - { - Name = name; - StreamUrl = streamUrl; - StreamType = type; - } - private Game(string name) - : this(name, null, StreamType.NotStreaming) { } - - public override string ToString() => Name; - private string DebuggerDisplay => StreamUrl != null ? $"{Name} ({StreamUrl})" : Name; - } -} diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 7f182241b..25adcc9c4 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -2,8 +2,8 @@ { public interface IPresence { - /// Gets the game this user is currently playing, if any. - Game? Game { get; } + /// Gets the activity this user is currently doing. + IActivity Activity { get; } /// Gets the current status of this user. UserStatus Status { get; } } diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index a499d83b0..bfb861692 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -13,6 +13,22 @@ namespace Discord.API public Optional StreamUrl { get; set; } [JsonProperty("type")] public Optional StreamType { get; set; } + [JsonProperty("details")] + public Optional Details { get; set; } + [JsonProperty("state")] + public Optional State { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationId { get; set; } + [JsonProperty("assets")] + public Optional Assets { get; set; } + [JsonProperty("party")] + public Optional Party { get; set; } + [JsonProperty("secrets")] + public Optional Secrets { get; set; } + [JsonProperty("timestamps")] + public Optional Timestamps { get; set; } + [JsonProperty("instance")] + public Optional Instance { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs new file mode 100644 index 000000000..b5928a8ab --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameAssets.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameAssets + { + [JsonProperty("small_text")] + public Optional SmallText { get; set; } + [JsonProperty("small_image")] + public Optional SmallImage { get; set; } + [JsonProperty("large_image")] + public Optional LargeText { get; set; } + [JsonProperty("large_text")] + public Optional LargeImage { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameParty.cs b/src/Discord.Net.Rest/API/Common/GameParty.cs new file mode 100644 index 000000000..e0da4a098 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameParty.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameParty + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("size")] + public int[] Size { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameSecrets.cs b/src/Discord.Net.Rest/API/Common/GameSecrets.cs new file mode 100644 index 000000000..e70b48ff0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameSecrets.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameSecrets + { + [JsonProperty("match")] + public string Match { get; set; } + [JsonProperty("join")] + public string Join { get; set; } + [JsonProperty("spectate")] + public string Spectate { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameTimestamps.cs b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs new file mode 100644 index 000000000..5c6f10b86 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameTimestamps + { + [JsonProperty("start")] + [UnixTimestamp] + public Optional Start { get; set; } + [JsonProperty("end")] + [UnixTimestamp] + public Optional End { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs new file mode 100644 index 000000000..3890ffc46 --- /dev/null +++ b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + internal class UnixTimestampAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 439b7bbb1..29f79e410 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index d8ade3a6b..c6cf6103a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -16,7 +16,7 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; public virtual bool IsWebhook => false; diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index b465fbed2..9213c5d75 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -66,6 +66,12 @@ namespace Discord.Net.Converters if (type == typeof(ulong)) return UInt64Converter.Instance; } + bool hasUnixStamp = propInfo.GetCustomAttribute() != null; + if (hasUnixStamp) + { + if (type == typeof(DateTimeOffset)) + return UnixTimestampConverter.Instance; + } //Enums if (type == typeof(PermissionTarget)) diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs new file mode 100644 index 000000000..d4660dc44 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -0,0 +1,28 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + public class UnixTimestampConverter : JsonConverter + { + public static readonly UnixTimestampConverter Instance = new UnixTimestampConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Discord doesn't validate if timestamps contain decimals or not + if (reader.Value is double d) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); + long offset = (long)reader.Value; + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index c6b0b2fd8..f55c83b75 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -18,7 +18,7 @@ namespace Discord.Rpc public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual bool IsWebhook => false; - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; internal RpcUser(DiscordRpcClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index d248285cd..2ab244aeb 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -13,7 +13,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public abstract int Latency { get; protected set; } public abstract UserStatus Status { get; protected set; } - public abstract Game? Game { get; protected set; } + public abstract IActivity Activity { get; protected set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; @@ -45,6 +45,7 @@ namespace Discord.WebSocket public abstract Task StopAsync(); public abstract Task SetStatusAsync(UserStatus status); public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); + public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); /// diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 6c2a0f3b9..e827909d9 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -22,7 +22,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public override int Latency { get => GetLatency(); protected set { } } public override UserStatus Status { get => _shards[0].Status; protected set { } } - public override Game? Game { get => _shards[0].Game; protected set { } } + public override IActivity Activity { get => _shards[0].Activity; protected set { } } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); @@ -239,9 +239,18 @@ namespace Discord.WebSocket await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + { + IActivity activity = null; + if (streamUrl != null) + activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + activity = new Game(name); + await SetActivityAsync(activity).ConfigureAwait(false); + } + public override async Task SetActivityAsync(IActivity activity) { for (int i = 0; i < _shards.Length; i++) - await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); + await _shards[i].SetActivityAsync(activity).ConfigureAwait(false); } private void RegisterEvents(DiscordSocketClient client, bool isPrimary) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index d152bbc03..35a22edc4 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -48,7 +48,7 @@ namespace Discord.WebSocket /// public override int Latency { get; protected set; } public override UserStatus Status { get; protected set; } = UserStatus.Online; - public override Game? Game { get; protected set; } + public override IActivity Activity { get; protected set; } //From DiscordSocketConfig internal int TotalShards { get; private set; } @@ -328,33 +328,39 @@ namespace Discord.WebSocket } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { - if (name != null) - Game = new Game(name, streamUrl, streamType); + if (streamUrl != null) + Activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + Activity = new Game(name); else - Game = null; + Activity = null; await SendStatusAsync().ConfigureAwait(false); } + public override async Task SetActivityAsync(IActivity activity) + { + Activity = activity; + await SendStatusAsync().ConfigureAwait(false); + } + private async Task SendStatusAsync() { if (CurrentUser == null) return; - var game = Game; + var activity = Activity; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, game); + CurrentUser.Presence = new SocketPresence(status, activity); - GameModel gameModel; - if (game != null) + var gameModel = new GameModel(); + // Discord only accepts rich presence over RPC, don't even bother building a payload + if (activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); + if (activity is StreamingGame stream) { - gameModel = new API.Game - { - Name = game.Value.Name, - StreamType = game.Value.StreamType, - StreamUrl = game.Value.StreamUrl - }; + gameModel.StreamUrl = stream.Url; + gameModel.StreamType = stream.StreamType; } - else - gameModel = null; + else if (activity != null) + gameModel.Name = activity.Name; await ApiClient.SendStatusUpdateAsync( status, diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 00d4b4bbc..7d7ba16ce 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -8,20 +8,20 @@ namespace Discord.WebSocket public struct SocketPresence : IPresence { public UserStatus Status { get; } - public Game? Game { get; } + public IActivity Activity { get; } - internal SocketPresence(UserStatus status, Game? game) + internal SocketPresence(UserStatus status, IActivity activity) { Status = status; - Game = game; + Activity= activity; } internal static SocketPresence Create(Model model) { - return new SocketPresence(model.Status, model.Game != null ? model.Game.ToEntity() : (Game?)null); + return new SocketPresence(model.Status, model.Game?.ToEntity()); } public override string ToString() => Status.ToString(); - private string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; + private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; internal SocketPresence Clone() => this; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index a0c78b93f..58d5c62a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -18,7 +18,7 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public Game? Game => Presence.Game; + public IActivity Activity => Presence.Activity; public UserStatus Status => Presence.Status; internal SocketUser(DiscordSocketClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 636ef68f4..4aff13753 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -2,11 +2,83 @@ { internal static class EntityExtensions { - public static Game ToEntity(this API.Game model) + public static IActivity ToEntity(this API.Game model) { - return new Game(model.Name, - model.StreamUrl.GetValueOrDefault(null), - model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); + // Rich Game + if (model.ApplicationId.IsSpecified) + { + ulong appId = model.ApplicationId.Value; + var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); + return new RichGame + { + ApplicationId = appId, + Name = model.Name, + Details = model.Details.GetValueOrDefault(), + State = model.State.GetValueOrDefault(), + SmallAsset = assets?[0], + LargeAsset = assets?[1], + Party = model.Party.GetValueOrDefault()?.ToEntity(), + Secrets = model.Secrets.GetValueOrDefault()?.ToEntity(), + Timestamps = model.Timestamps.GetValueOrDefault()?.ToEntity() + }; + } + // Stream Game + if (model.StreamUrl.IsSpecified) + { + return new StreamingGame( + model.Name, + model.StreamUrl.Value, + model.StreamType.Value.GetValueOrDefault()); + } + // Normal Game + return new Game(model.Name); + } + + // (Small, Large) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) + { + return new GameAsset[] + { + model.SmallImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.SmallImage.GetValueOrDefault(), + Text = model.SmallText.GetValueOrDefault() + } : null, + model.LargeImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.LargeImage.GetValueOrDefault(), + Text = model.LargeText.GetValueOrDefault() + } : null, + }; + } + + public static GameParty ToEntity(this API.GameParty model) + { + // Discord will probably send bad data since they don't validate anything + int current = 0, cap = 0; + if (model.Size.Length == 2) + { + current = model.Size[0]; + cap = model.Size[1]; + } + return new GameParty + { + Id = model.Id, + Members = current, + Capacity = cap, + }; + } + + public static GameSecrets ToEntity(this API.GameSecrets model) + { + return new GameSecrets(model.Match, model.Join, model.Spectate); + } + + public static GameTimestamps ToEntity(this API.GameTimestamps model) + { + return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } } } From a19ff188e9f4ea7019966dd7411754230b739abc Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 23 Dec 2017 15:09:24 -0500 Subject: [PATCH 11/27] Added support for animated emoji (#913) * Added support for animated emoji This was such a useful feature Discord, I'm glad you added this instead of fixing bugs. * Fix bugs in emote parser * Added unit tests for emotes --- src/Discord.Net.Core/CDN.cs | 4 +- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 22 +++++++--- .../Entities/Emotes/GuildEmote.cs | 4 +- src/Discord.Net.Rest/API/Common/Emoji.cs | 2 + .../Entities/Messages/RestReaction.cs | 2 +- .../Extensions/EntityExtensions.cs | 2 +- .../Entities/Messages/SocketReaction.cs | 2 +- test/Discord.Net.Tests/Tests.Emotes.cs | 44 +++++++++++++++++++ 8 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 test/Discord.Net.Tests/Tests.Emotes.cs diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 415c0c30d..070b965ee 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -19,8 +19,8 @@ namespace Discord => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; - public static string GetEmojiUrl(ulong emojiId) - => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetEmojiUrl(ulong emojiId, bool animated) + => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) { diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index f498c818e..e3a228c83 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -16,13 +16,18 @@ namespace Discord /// The ID of this emote /// public ulong Id { get; } + /// + /// Is this emote animated? + /// + public bool Animated { get; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Url => CDN.GetEmojiUrl(Id); + public string Url => CDN.GetEmojiUrl(Id, Animated); - internal Emote(ulong id, string name) + internal Emote(ulong id, string name, bool animated) { Id = id; Name = name; + Animated = animated; } public override bool Equals(object other) @@ -59,17 +64,20 @@ namespace Discord public static bool TryParse(string text, out Emote result) { result = null; - if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') { - int splitIndex = text.IndexOf(':', 2); + bool animated = text[1] == 'a'; + int startIndex = animated ? 3 : 2; + + int splitIndex = text.IndexOf(':', startIndex); if (splitIndex == -1) return false; if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) return false; - string name = text.Substring(2, splitIndex - 2); - result = new Emote(id, name); + string name = text.Substring(startIndex, splitIndex - startIndex); + result = new Emote(id, name, animated); return true; } return false; @@ -77,6 +85,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 8d776a4cd..95b062bd2 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -13,7 +13,7 @@ namespace Discord public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name) + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name, animated) { IsManaged = isManaged; RequireColons = requireColons; @@ -21,6 +21,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index bd9c4d466..2bdfdcc36 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -9,6 +9,8 @@ namespace Discord.API public ulong? Id { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("animated")] + public bool? Animated { get; set; } [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("require_colons")] diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 05c817935..6d3f72419 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -18,7 +18,7 @@ namespace Discord.Rest { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new RestReaction(emote, model.Count, model.Me); diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index b88a5b515..74b05dacd 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -7,7 +7,7 @@ namespace Discord.Rest { public static GuildEmote ToEntity(this API.Emoji model) { - return new GuildEmote(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmote(model.Id.Value, model.Name, model.Animated.GetValueOrDefault(), model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public static Embed ToEntity(this API.Embed model) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 35bee9e68..e8fa17a35 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -24,7 +24,7 @@ namespace Discord.WebSocket { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); diff --git a/test/Discord.Net.Tests/Tests.Emotes.cs b/test/Discord.Net.Tests/Tests.Emotes.cs new file mode 100644 index 000000000..334975ce4 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Emotes.cs @@ -0,0 +1,44 @@ +using System; +using Xunit; + +namespace Discord +{ + public class EmoteTests + { + [Fact] + public void Test_Emote_Parse() + { + Assert.True(Emote.TryParse("<:typingstatus:394207658351263745>", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.False(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("png", emote.Url); + } + [Fact] + public void Test_Invalid_Emote_Parse() + { + Assert.False(Emote.TryParse("invalid", out _)); + Assert.False(Emote.TryParse("<:typingstatus:not_a_number>", out _)); + Assert.Throws(() => Emote.Parse("invalid")); + } + [Fact] + public void Test_Animated_Emote_Parse() + { + Assert.True(Emote.TryParse("", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.True(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("gif", emote.Url); + } + public void Test_Invalid_Amimated_Emote_Parse() + { + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + } + } +} From 7b2ddd027cd5f2fed37a06364f827ebe36fc5dbe Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sat, 23 Dec 2017 15:17:20 -0500 Subject: [PATCH 12/27] Add missing REST Webhook implemenation (#843) * Add Webhook API models, REST implementation, and Socket bridges. * Remove token overrides from REST. Leaving that as a Webhook package only feature. * Add Webhook API models, REST implementation, and Socket bridges. * Remove token overrides from REST. Leaving that as a Webhook package only feature. * Webhook core implementation. * Webhook REST implementation. * Webhook client implementation. * Add channel bucket id. --- .../Entities/Channels/ITextChannel.cs | 8 ++ .../Entities/Guilds/IGuild.cs | 5 + .../Entities/Users/IWebhookUser.cs | 1 - .../Entities/Webhooks/IWebhook.cs | 34 +++++++ .../Entities/Webhooks/WebhookProperties.cs | 41 +++++++++ src/Discord.Net.Core/IDiscordClient.cs | 2 + src/Discord.Net.Rest/API/Common/Webhook.cs | 25 +++++ .../API/Rest/CreateWebhookParams.cs | 14 +++ .../API/Rest/ModifyWebhookParams.cs | 16 ++++ .../API/Rest/UploadWebhookFileParams.cs | 6 +- src/Discord.Net.Rest/BaseDiscordClient.cs | 3 + src/Discord.Net.Rest/ClientHelper.cs | 8 ++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 74 ++++++++++++++- src/Discord.Net.Rest/DiscordRestClient.cs | 6 ++ .../Entities/Channels/ChannelHelper.cs | 25 +++++ .../Entities/Channels/RestTextChannel.cs | 15 +++ .../Entities/Guilds/GuildHelper.cs | 14 +++ .../Entities/Guilds/RestGuild.cs | 11 +++ .../Entities/Webhooks/RestWebhook.cs | 91 +++++++++++++++++++ .../Entities/Webhooks/WebhookHelper.cs | 38 ++++++++ .../Entities/Channels/RpcTextChannel.cs | 16 +++- .../API/Gateway/WebhookUpdateEvent.cs | 13 +++ .../Entities/Channels/SocketTextChannel.cs | 18 +++- .../Entities/Guilds/SocketGuild.cs | 11 +++ .../DiscordWebhookClient.cs | 77 +++++++++------- .../Entities/Webhooks/RestInternalWebhook.cs | 66 ++++++++++++++ .../WebhookClientHelper.cs | 81 +++++++++++++++++ 27 files changed, 679 insertions(+), 40 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs create mode 100644 src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs create mode 100644 src/Discord.Net.Rest/API/Common/Webhook.cs create mode 100644 src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs create mode 100644 src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs create mode 100644 src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs create mode 100644 src/Discord.Net.Webhook/WebhookClientHelper.cs diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index be4dd0260..7c6ec3908 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace Discord @@ -19,5 +20,12 @@ namespace Discord /// Modifies this text channel. Task ModifyAsync(Action func, RequestOptions options = null); + + /// Creates a webhook in this text channel. + Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); + /// Gets the webhook in this text channel with the provided id, or null if not found. + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// Gets the webhooks for this text channel. + Task> GetWebhooksAsync(RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 6b2d24cc6..0fdd92405 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -118,6 +118,11 @@ namespace Discord /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + /// Gets the webhook in this guild with the provided id, or null if not found. + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// Gets a collection of all webhooks for this guild. + Task> GetWebhooksAsync(RequestOptions options = null); + /// Gets a specific emote from this guild. Task GetEmoteAsync(ulong id, RequestOptions options = null); /// Creates a new emote in this guild. diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs index 8f4d42187..be769b944 100644 --- a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -1,6 +1,5 @@ namespace Discord { - //TODO: Add webhook endpoints public interface IWebhookUser : IGuildUser { ulong WebhookId { get; } diff --git a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs new file mode 100644 index 000000000..ef56f72b9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IWebhook : IDeletable, ISnowflakeEntity + { + /// Gets the token of this webhook. + string Token { get; } + + /// Gets the default name of this webhook. + string Name { get; } + /// Gets the id of this webhook's default avatar. + string AvatarId { get; } + /// Gets the url to this webhook's default avatar. + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + + /// Gets the channel for this webhook. + ITextChannel Channel { get; } + /// Gets the id of the channel for this webhook. + ulong ChannelId { get; } + + /// Gets the guild owning this webhook. + IGuild Guild { get; } + /// Gets the id of the guild owning this webhook. + ulong? GuildId { get; } + + /// Gets the user that created this webhook. + IUser Creator { get; } + + /// Modifies this webhook. + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs new file mode 100644 index 000000000..8759a1729 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -0,0 +1,41 @@ +namespace Discord +{ + /// + /// Modify an with the specified parameters. + /// + /// + /// + /// await webhook.ModifyAsync(x => + /// { + /// x.Name = "Bob"; + /// x.Avatar = new Image("avatar.jpg"); + /// }); + /// + /// + /// + public class WebhookProperties + { + /// + /// The default name of the webhook. + /// + public Optional Name { get; set; } + /// + /// The default avatar of the webhook. + /// + public Optional Image { get; set; } + /// + /// The channel for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional Channel { get; set; } + /// + /// The channel id for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 23e8e9c5b..9abb959b5 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -34,5 +34,7 @@ namespace Discord Task> GetVoiceRegionsAsync(RequestOptions options = null); Task GetVoiceRegionAsync(string id, RequestOptions options = null); + + Task GetWebhookAsync(ulong id, RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/API/Common/Webhook.cs b/src/Discord.Net.Rest/API/Common/Webhook.cs new file mode 100644 index 000000000..cbd5fdad5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Webhook.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Webhook + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + + [JsonProperty("user")] + public Optional Creator { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs new file mode 100644 index 000000000..0d1059fab --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs new file mode 100644 index 000000000..0f2d6e33b --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyWebhookParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index f2c34c015..6d6eb29b2 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 -using Discord.Net.Rest; using System.Collections.Generic; using System.IO; +using Discord.Net.Rest; namespace Discord.API.Rest { @@ -15,6 +15,7 @@ namespace Discord.API.Rest public Optional IsTTS { get; set; } public Optional Username { get; set; } public Optional AvatarUrl { get; set; } + public Optional Embeds { get; set; } public UploadWebhookFileParams(Stream file) { @@ -25,6 +26,7 @@ namespace Discord.API.Rest { var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) d["content"] = Content.Value; if (IsTTS.IsSpecified) @@ -35,6 +37,8 @@ namespace Discord.API.Rest d["username"] = Username.Value; if (AvatarUrl.IsSpecified) d["avatar_url"] = AvatarUrl.Value; + if (Embeds.IsSpecified) + d["embeds"] = Embeds.Value; return d; } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 47a946f20..269dedd71 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -164,6 +164,9 @@ namespace Discord.Rest Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); + Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => Task.FromResult(null); + Task IDiscordClient.StartAsync() => Task.Delay(0); Task IDiscordClient.StopAsync() diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 2f05d5d36..26d8c720e 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -144,6 +144,14 @@ namespace Discord.Rest return null; } + public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id); + if (model != null) + return RestWebhook.Create(client, (IGuild)null, model); + return null; + } + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 4e65b19d2..ab47b1e98 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -473,7 +473,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -486,8 +486,8 @@ namespace Discord.API if (args.Content.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); - - await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { @@ -503,7 +503,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -522,7 +522,7 @@ namespace Discord.API throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } - await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -1198,6 +1198,70 @@ namespace Discord.API return await SendAsync>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); } + //Webhooks + public async Task CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + var ids = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options); + } + public async Task GetWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + try + { + if (AuthTokenType == TokenType.Webhook) + return await SendAsync("GET", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendAsync("GET", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + public async Task ModifyWebhookAsync(ulong webhookId, ModifyWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task DeleteWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + await SendAsync("DELETE", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetGuildWebhooksAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/webhooks", ids, options: options).ConfigureAwait(false); + } + public async Task> GetChannelWebhooksAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync>("GET", () => $"channels/{channelId}/webhooks", ids, options: options).ConfigureAwait(false); + } + //Helpers protected void CheckState() { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index aa9937008..3d90b6c00 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -91,6 +91,9 @@ namespace Discord.Rest /// public Task GetVoiceRegionAsync(string id, RequestOptions options = null) => ClientHelper.GetVoiceRegionAsync(this, id, options); + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetWebhookAsync(this, id, options); //IDiscordClient async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) @@ -160,5 +163,8 @@ namespace Discord.Rest => await GetVoiceRegionsAsync(options).ConfigureAwait(false); async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + + async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 8dcb8c284..fa870be17 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; using UserModel = Discord.API.User; +using WebhookModel = Discord.API.Webhook; namespace Discord.Rest { @@ -280,6 +281,30 @@ namespace Discord.Rest RequestOptions options) => new TypingNotifier(client, channel, options); + //Webhooks + public static async Task CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options) + { + var args = new CreateWebhookParams { Name = name }; + if (avatar != null) + args.Avatar = new API.Image(avatar); + + var model = await client.ApiClient.CreateWebhookAsync(channel.Id, args, options).ConfigureAwait(false); + return RestWebhook.Create(client, channel, model); + } + public static async Task GetWebhookAsync(ITextChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, channel, model); + } + public static async Task> GetWebhooksAsync(ITextChannel channel, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetChannelWebhooksAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, channel, x)) + .ToImmutableArray(); + } + //Helpers private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) { diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 8a096302b..9c29624c1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -77,8 +77,23 @@ namespace Discord.Rest public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + //ITextChannel + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 58b7ed7f9..7e25b53f3 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -254,6 +254,20 @@ namespace Discord.Rest return model.Pruned; } + //Webhooks + public static async Task GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, guild, model); + } + public static async Task> GetWebhooksAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetGuildWebhooksAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, guild, x)).ToImmutableArray(); + } + //Emotes public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index de4b89e39..3b6a2bfa8 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -257,6 +257,12 @@ namespace Discord.Rest public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + //Webhooks + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; @@ -396,5 +402,10 @@ namespace Discord.Rest return ImmutableArray.Create(); } Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); } } diff --git a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs new file mode 100644 index 000000000..47cc50a9c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhook : RestEntity, IWebhook, IUpdateable + { + internal IGuild Guild { get; private set; } + internal ITextChannel Channel { get; private set; } + + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + public IUser Creator { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId) + : base(discord, id) + { + Guild = guild; + Token = token; + ChannelId = channelId; + } + internal RestWebhook(BaseDiscordClient discord, ITextChannel channel, ulong id, string token, ulong channelId) + : this(discord, channel.Guild, id, token, channelId) + { + Channel = channel; + } + + internal static RestWebhook Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestWebhook(discord, guild, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + internal static RestWebhook Create(BaseDiscordClient discord, ITextChannel channel, Model model) + { + var entity = new RestWebhook(discord, channel, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Creator.IsSpecified) + Creator = RestUser.Create(Discord, model.Creator.Value); + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false); + Update(model); + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + //IWebhook + IGuild IWebhook.Guild + => Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + ITextChannel IWebhook.Channel + => Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + Task IWebhook.ModifyAsync(Action func, RequestOptions options) + => ModifyAsync(func, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs new file mode 100644 index 000000000..50e9cab78 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Discord.API.Rest; +using ImageModel = Discord.API.Image; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + internal static class WebhookHelper + { + public static async Task ModifyAsync(IWebhook webhook, BaseDiscordClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(webhook.AvatarId); + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + return await client.ApiClient.ModifyWebhookAsync(webhook.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IWebhook webhook, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(webhook.Id, options).ConfigureAwait(false); + } + + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 9de2968db..8c49f0671 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -68,11 +68,25 @@ namespace Discord.Rpc => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); - + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; //ITextChannel string ITextChannel.Topic { get { throw new NotSupportedException(); } } + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs new file mode 100644 index 000000000..e5c7afe41 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class WebhookUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 07ec630d3..b6a304b50 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -112,10 +112,26 @@ namespace Discord.WebSocket } return null; } - + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + //ITextChannel + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index b639a9cf7..c4158c136 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -433,6 +433,12 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + //Webhooks + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + //Emotes public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); @@ -682,5 +688,10 @@ namespace Discord.WebSocket Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); } } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 3d8307da4..59cc8f3e7 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,32 +1,49 @@ -using Discord.API.Rest; -using Discord.Rest; using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using System.Linq; using Discord.Logging; +using Discord.Rest; namespace Discord.Webhook { - public partial class DiscordWebhookClient + public class DiscordWebhookClient : IDisposable { public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); private readonly ulong _webhookId; + internal IWebhook Webhook; internal readonly Logger _restLogger; internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(IWebhook webhook) + : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } /// Creates a new Webhook discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken) : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + : this(config) { _webhookId = webhookId; + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); + } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) + : this(config) + { + Webhook = webhook; + _webhookId = Webhook.Id; + } + private DiscordWebhookClient(DiscordRestConfig config) + { ApiClient = CreateApiClient(config); LogManager = new LogManager(config.LogLevel); LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); @@ -41,42 +58,40 @@ namespace Discord.Webhook await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); - ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - - public async Task SendMessageAsync(string text, bool isTTS = false, Embed[] embeds = null, + + /// Sends a message using to the channel for this webhook. Returns the ID of the created message. + public Task SendMessageAsync(string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) - { - var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; - if (embeds != null) - args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = avatarUrl; - await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); - } + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); #if FILESYSTEM - public async Task SendFileAsync(string filePath, string text, bool isTTS = false, - string username = null, string avatarUrl = null, RequestOptions options = null) + /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + public Task SendFileAsync(string filePath, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options); +#endif + /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options); + + /// Modifies the properties of this webhook. + public Task ModifyWebhookAsync(Action func, RequestOptions options = null) + => Webhook.ModifyAsync(func, options); + + /// Deletes this webhook from Discord and disposes the client. + public async Task DeleteWebhookAsync(RequestOptions options = null) { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - await SendFileAsync(file, filename, text, isTTS, username, avatarUrl, options).ConfigureAwait(false); + await Webhook.DeleteAsync(options).ConfigureAwait(false); + Dispose(); } -#endif - public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, - string username = null, string avatarUrl = null, RequestOptions options = null) + + public void Dispose() { - var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = username; - await ApiClient.UploadWebhookFileAsync(_webhookId, args, options).ConfigureAwait(false); + ApiClient?.Dispose(); } } } diff --git a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs new file mode 100644 index 000000000..cd35d731c --- /dev/null +++ b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Webhook +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class RestInternalWebhook : IWebhook + { + private DiscordWebhookClient _client; + + public ulong Id { get; } + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestInternalWebhook(DiscordWebhookClient apiClient, Model model) + { + _client = apiClient; + Id = model.Id; + ChannelId = model.Id; + Token = model.Token; + } + internal static RestInternalWebhook Create(DiscordWebhookClient client, Model model) + { + var entity = new RestInternalWebhook(client, model); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookClientHelper.ModifyAsync(_client, func, options); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookClientHelper.DeleteAsync(_client, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + IUser IWebhook.Creator => null; + ITextChannel IWebhook.Channel => null; + IGuild IWebhook.Guild => null; + } +} diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs new file mode 100644 index 000000000..f3a3984cf --- /dev/null +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Discord.API.Rest; +using Discord.Rest; +using ImageModel = Discord.API.Image; +using WebhookModel = Discord.API.Webhook; + +namespace Discord.Webhook +{ + internal static class WebhookClientHelper + { + public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong webhookId) + { + var model = await client.ApiClient.GetWebhookAsync(webhookId); + if (model == null) + throw new InvalidOperationException("Could not find a webhook for the supplied credentials."); + return RestInternalWebhook.Create(client, model); + } + public static async Task SendMessageAsync(DiscordWebhookClient client, + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = avatarUrl; + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + return model.Id; + } +#if FILESYSTEM + public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options).ConfigureAwait(false); + } +#endif + public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = username; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + return msg.Id; + } + + public static async Task ModifyAsync(DiscordWebhookClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && client.Webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(client.Webhook.AvatarId); + + return await client.ApiClient.ModifyWebhookAsync(client.Webhook.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task DeleteAsync(DiscordWebhookClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(client.Webhook.Id, options).ConfigureAwait(false); + } + } +} From 5ce85deb9db694de28333bd3b5655be1638bcddd Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 27 Dec 2017 14:47:55 -0500 Subject: [PATCH 13/27] Attempt to patch the GameParty nullref Not sure if this works, needs a more proper solution in the future anyways --- src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 4aff13753..b59702be2 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -56,6 +56,8 @@ public static GameParty ToEntity(this API.GameParty model) { + // todo: proper fix for this + if (model == null) return null; // Discord will probably send bad data since they don't validate anything int current = 0, cap = 0; if (model.Size.Length == 2) From 5f46aef3a761b24dcd8ef5ecb4b8811d0f374059 Mon Sep 17 00:00:00 2001 From: vim2meta Date: Fri, 5 Jan 2018 15:02:27 -0500 Subject: [PATCH 14/27] Ability to ignore unused parameters instead of failing the command. (#915) * Addition of FailOnTooManyArgs * Correct name & only pass in bool --- src/Discord.Net.Commands/CommandParser.cs | 9 +++++++-- src/Discord.Net.Commands/CommandService.cs | 3 ++- src/Discord.Net.Commands/CommandServiceConfig.cs | 3 +++ src/Discord.Net.Commands/Info/CommandInfo.cs | 4 +++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 28e36d54d..d65d99349 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -14,7 +14,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) + public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -109,7 +109,12 @@ namespace Discord.Commands if (argString != null) { if (curParam == null) - return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + { + if (ignoreExtraArgs) + break; + else + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + } var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index cf2b93277..8e7dab898 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -27,7 +27,7 @@ namespace Discord.Commands private readonly HashSet _moduleDefs; private readonly CommandMap _map; - internal readonly bool _caseSensitive, _throwOnError; + internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; internal readonly Logger _cmdLogger; @@ -42,6 +42,7 @@ namespace Discord.Commands { _caseSensitive = config.CaseSensitiveCommands; _throwOnError = config.ThrowOnError; + _ignoreExtraArgs = config.IgnoreExtraArgs; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; if (_defaultRunMode == RunMode.Default) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index b53b0248c..7fdbe368b 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -15,5 +15,8 @@ /// Determines whether RunMode.Sync commands should push exceptions up to the caller. public bool ThrowOnError { get; set; } = true; + + /// Determines whether extra parameters should be ignored. + public bool IgnoreExtraArgs { get; set; } = false; } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 6bb621f94..f0d406e8d 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -18,6 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); + private readonly CommandService _commandService; private readonly Func _action; public ModuleInfo Module { get; } @@ -64,6 +65,7 @@ namespace Discord.Commands HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; _action = builder.Callback; + _commandService = service; } public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) @@ -117,7 +119,7 @@ namespace Discord.Commands return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0).ConfigureAwait(false); } public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) From b30af57b7facbac43f1079891b249471d67e9983 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 5 Jan 2018 20:23:19 -0500 Subject: [PATCH 15/27] Remove RPC from main distribution (#925) --- Discord.Net.sln | 17 +---------------- appveyor.yml | 1 - .../API/Rpc/AuthenticateParams.cs | 0 .../API/Rpc/AuthenticateResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs | 0 .../API/Rpc/AuthorizeResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/Channel.cs | 0 .../API/Rpc/ChannelSubscriptionParams.cs | 0 .../Discord.Net.Rpc/API/Rpc/ChannelSummary.cs | 0 .../Discord.Net.Rpc/API/Rpc/ErrorEvent.cs | 0 .../API/Rpc/ExtendedVoiceState.cs | 0 .../Discord.Net.Rpc/API/Rpc/GetChannelParams.cs | 0 .../API/Rpc/GetChannelsParams.cs | 0 .../API/Rpc/GetChannelsResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/GetGuildParams.cs | 0 .../Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs | 0 .../API/Rpc/GetGuildsResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/Guild.cs | 0 .../Discord.Net.Rpc/API/Rpc/GuildMember.cs | 0 .../Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs | 0 .../API/Rpc/GuildSubscriptionParams.cs | 0 .../Discord.Net.Rpc/API/Rpc/GuildSummary.cs | 0 .../Discord.Net.Rpc/API/Rpc/Message.cs | 0 .../Discord.Net.Rpc/API/Rpc/MessageEvent.cs | 0 .../Discord.Net.Rpc/API/Rpc/Pan.cs | 0 .../Discord.Net.Rpc/API/Rpc/ReadyEvent.cs | 0 .../Discord.Net.Rpc/API/Rpc/RpcConfig.cs | 0 .../API/Rpc/SelectChannelParams.cs | 0 .../API/Rpc/SetLocalVolumeParams.cs | 0 .../API/Rpc/SetLocalVolumeResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs | 0 .../API/Rpc/SubscriptionResponse.cs | 0 .../API/Rpc/UserVoiceSettings.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceDevice.cs | 0 .../API/Rpc/VoiceDeviceSettings.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceMode.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceSettings.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs | 0 .../Discord.Net.Rpc/API/RpcFrame.cs | 0 .../Discord.Net.Rpc/AssemblyInfo.cs | 0 .../Commands/RpcCommandContext.cs | 0 .../Discord.Net.Rpc/Discord.Net.Rpc.csproj | 0 .../Discord.Net.Rpc/DiscordRpcApiClient.cs | 0 .../Discord.Net.Rpc/DiscordRpcClient.Events.cs | 0 .../Discord.Net.Rpc/DiscordRpcClient.cs | 0 .../Discord.Net.Rpc/DiscordRpcConfig.cs | 0 .../Entities/Channels/IRpcAudioChannel.cs | 0 .../Entities/Channels/IRpcMessageChannel.cs | 0 .../Entities/Channels/IRpcPrivateChannel.cs | 0 .../Entities/Channels/RpcChannel.cs | 0 .../Entities/Channels/RpcChannelSummary.cs | 0 .../Entities/Channels/RpcDMChannel.cs | 0 .../Entities/Channels/RpcGroupChannel.cs | 0 .../Entities/Channels/RpcGuildChannel.cs | 0 .../Entities/Channels/RpcTextChannel.cs | 0 .../Entities/Channels/RpcVoiceChannel.cs | 0 .../Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs | 0 .../Entities/Guilds/RpcGuildStatus.cs | 0 .../Entities/Guilds/RpcGuildSummary.cs | 0 .../Entities/Messages/RpcMessage.cs | 0 .../Entities/Messages/RpcSystemMessage.cs | 0 .../Entities/Messages/RpcUserMessage.cs | 0 .../Discord.Net.Rpc/Entities/RpcEntity.cs | 0 .../Entities/UserVoiceProperties.cs | 0 .../Discord.Net.Rpc/Entities/Users/Pan.cs | 0 .../Entities/Users/RpcGuildUser.cs | 0 .../Discord.Net.Rpc/Entities/Users/RpcUser.cs | 0 .../Entities/Users/RpcVoiceState.cs | 0 .../Entities/Users/RpcWebhookUser.cs | 0 .../Discord.Net.Rpc/Entities/VoiceDevice.cs | 0 .../Entities/VoiceDeviceProperties.cs | 0 .../Entities/VoiceModeProperties.cs | 0 .../Discord.Net.Rpc/Entities/VoiceProperties.cs | 0 .../Discord.Net.Rpc/Entities/VoiceSettings.cs | 0 .../Discord.Net.Rpc/Entities/VoiceShortcut.cs | 0 .../Entities/VoiceShortcutType.cs | 0 .../Extensions/EntityExtensions.cs | 0 .../Discord.Net.Rpc/RpcChannelEvent.cs | 0 .../Discord.Net.Rpc/RpcGlobalEvent.cs | 0 .../Discord.Net.Rpc/RpcGuildEvent.cs | 0 src/Discord.Net/Discord.Net.nuspec | 3 --- 81 files changed, 1 insertion(+), 20 deletions(-) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Channel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Guild.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildMember.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Message.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/MessageEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Pan.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/RpcConfig.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceMode.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/RpcFrame.cs (100%) rename {src => experiment}/Discord.Net.Rpc/AssemblyInfo.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Commands/RpcCommandContext.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Discord.Net.Rpc.csproj (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcApiClient.cs (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcClient.Events.cs (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcClient.cs (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcConfig.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/RpcEntity.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/UserVoiceProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/Pan.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcUser.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceDevice.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceModeProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceShortcut.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceShortcutType.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Extensions/EntityExtensions.cs (100%) rename {src => experiment}/Discord.Net.Rpc/RpcChannelEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/RpcGlobalEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/RpcGuildEvent.cs (100%) diff --git a/Discord.Net.sln b/Discord.Net.sln index 58bfcad86..cac6c9064 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -8,8 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.csproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.csproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}" @@ -58,18 +56,6 @@ Global {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Release|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -136,7 +122,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E} - {5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} diff --git a/appveyor.yml b/appveyor.yml index 3bf70c09c..393485fee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,6 @@ after_build: - ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Channel.cs b/experiment/Discord.Net.Rpc/API/Rpc/Channel.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Channel.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Channel.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs b/experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Guild.cs b/experiment/Discord.Net.Rpc/API/Rpc/Guild.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Guild.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Guild.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildMember.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Message.cs b/experiment/Discord.Net.Rpc/API/Rpc/Message.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Message.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Message.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Pan.cs b/experiment/Discord.Net.Rpc/API/Rpc/Pan.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Pan.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Pan.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs b/experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs rename to experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs diff --git a/src/Discord.Net.Rpc/API/RpcFrame.cs b/experiment/Discord.Net.Rpc/API/RpcFrame.cs similarity index 100% rename from src/Discord.Net.Rpc/API/RpcFrame.cs rename to experiment/Discord.Net.Rpc/API/RpcFrame.cs diff --git a/src/Discord.Net.Rpc/AssemblyInfo.cs b/experiment/Discord.Net.Rpc/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Rpc/AssemblyInfo.cs rename to experiment/Discord.Net.Rpc/AssemblyInfo.cs diff --git a/src/Discord.Net.Rpc/Commands/RpcCommandContext.cs b/experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs similarity index 100% rename from src/Discord.Net.Rpc/Commands/RpcCommandContext.cs rename to experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj similarity index 100% rename from src/Discord.Net.Rpc/Discord.Net.Rpc.csproj rename to experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj diff --git a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcApiClient.cs rename to experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.Events.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcClient.Events.cs rename to experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcClient.cs rename to experiment/Discord.Net.Rpc/DiscordRpcClient.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcConfig.cs b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcConfig.cs rename to experiment/Discord.Net.Rpc/DiscordRpcConfig.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/experiment/Discord.Net.Rpc/Entities/RpcEntity.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/RpcEntity.cs rename to experiment/Discord.Net.Rpc/Entities/RpcEntity.cs diff --git a/src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/Pan.cs b/experiment/Discord.Net.Rpc/Entities/Users/Pan.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/Pan.cs rename to experiment/Discord.Net.Rpc/Entities/Users/Pan.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceDevice.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceDevice.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceSettings.cs b/experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceSettings.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceShortcut.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs diff --git a/src/Discord.Net.Rpc/Extensions/EntityExtensions.cs b/experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs similarity index 100% rename from src/Discord.Net.Rpc/Extensions/EntityExtensions.cs rename to experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs diff --git a/src/Discord.Net.Rpc/RpcChannelEvent.cs b/experiment/Discord.Net.Rpc/RpcChannelEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcChannelEvent.cs rename to experiment/Discord.Net.Rpc/RpcChannelEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGlobalEvent.cs b/experiment/Discord.Net.Rpc/RpcGlobalEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcGlobalEvent.cs rename to experiment/Discord.Net.Rpc/RpcGlobalEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGuildEvent.cs b/experiment/Discord.Net.Rpc/RpcGuildEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcGuildEvent.cs rename to experiment/Discord.Net.Rpc/RpcGuildEvent.cs diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index f904f4126..cd57d2fcf 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -16,7 +16,6 @@ - @@ -24,7 +23,6 @@ - @@ -32,7 +30,6 @@ - From 227f61aa4e05affb0eb76aa3ab2ebd3bc2c0e30f Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 5 Jan 2018 17:24:21 -0800 Subject: [PATCH 16/27] Allow null value to reset IGuildUser nickname (#923) * Added workaround for UserHelper#ModifyAsync that accepts null values as a way to reset user nicknames * Update comments * Update comment to use see tag --- .../Entities/Users/GuildUserProperties.cs | 2 +- src/Discord.Net.Rest/Entities/Users/UserHelper.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 33b311604..1c5e5482c 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -34,7 +34,7 @@ namespace Discord /// Should the user have a nickname set? /// /// - /// To clear the user's nickname, this value can be set to null. + /// To clear the user's nickname, this value can be set to or . /// public Optional Nickname { get; set; } /// diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 562cfaae8..dfb81ff2c 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -48,6 +48,14 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) apiArgs.RoleIds = args.RoleIds.Value.ToArray(); + /* + * Ensure that the nick passed in the params of the request is not null. + * string.Empty ("") is the only way to reset the user nick in the API, + * a value of null does not. This is a workaround. + */ + if (apiArgs.Nickname.IsSpecified && apiArgs.Nickname.Value == null) + apiArgs.Nickname = new Optional(string.Empty); + await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, apiArgs, options).ConfigureAwait(false); return args; } From c08f37bb0344bc48159c6cebcd1599b30d12bf7e Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 5 Jan 2018 20:49:59 -0500 Subject: [PATCH 17/27] Fix nullref exceptions from Rich Presences --- .../Extensions/EntityExtensions.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index b59702be2..c66163610 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -17,9 +17,9 @@ State = model.State.GetValueOrDefault(), SmallAsset = assets?[0], LargeAsset = assets?[1], - Party = model.Party.GetValueOrDefault()?.ToEntity(), - Secrets = model.Secrets.GetValueOrDefault()?.ToEntity(), - Timestamps = model.Timestamps.GetValueOrDefault()?.ToEntity() + Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, + Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, + Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null }; } // Stream Game @@ -56,11 +56,9 @@ public static GameParty ToEntity(this API.GameParty model) { - // todo: proper fix for this - if (model == null) return null; // Discord will probably send bad data since they don't validate anything int current = 0, cap = 0; - if (model.Size.Length == 2) + if (model.Size?.Length == 2) { current = model.Size[0]; cap = model.Size[1]; From 39bddca5d13965ea59a7dbd1317b76b29fe2fff3 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 6 Jan 2018 10:04:07 +0800 Subject: [PATCH 18/27] Fix broken markdown in command docs (#920) I screwed up in ##826 and broke the `CommandAttribute` section. --- docs/guides/commands/commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 6781764c9..2b012af0e 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -93,9 +93,9 @@ If you would like a parameter to parse until the end of a Command, flag the parameter with the [RemainderAttribute]. This will allow a user to invoke a Command without wrapping a parameter in quotes. -Finally, flag your Command with the [CommandAttribute] (you must +Finally, flag your Command with the [CommandAttribute]. (you must specify a name for this Command, except for when it is part of a -Module Group - see below). +Module Group - see below) [RemainderAttribute]: xref:Discord.Commands.RemainderAttribute [CommandAttribute]: xref:Discord.Commands.CommandAttribute @@ -340,4 +340,4 @@ and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader]. -[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ \ No newline at end of file +[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ From 030422fa1ddada549cea65cd87dae537cb1aaf76 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:27:51 -0500 Subject: [PATCH 19/27] Add support for channel categories (#907) commit a85c5814a74e473e95fe172f0379cbc7f9f951d8 Author: Christopher F Date: Sat Jan 6 22:25:48 2018 -0500 Code cleanup commit 4b243fd3dd99152b4ebc7ee01d704bd8e57eeee1 Author: Christopher F Date: Sat Jan 6 22:08:28 2018 -0500 Add support for channel categories (#907) commit 41ed9106f2b05530acbf06b245c9aa618011d815 Author: mrspits4ever Date: Thu Dec 14 20:02:57 2017 +0100 removed mentioning support for RestCategoryChannel, added channels property to SocketCategoryChannel commit 71142c310847886dff80c49e9357dd0786d67a1b Merge: 4589d731 678a7238 Author: mrspits4ever Date: Wed Dec 13 21:17:53 2017 +0100 Merge branch 'dev' of https://github.com/RogueException/Discord.Net into feature/channel-categories commit 4589d73187871c98485ed25c6d223706927af7ec Author: mrspits4ever Date: Wed Dec 13 21:17:46 2017 +0100 adressed requested changes commit d59b038efa048b2279602e2015ddd2c185e58d63 Author: pegasy Date: Mon Sep 25 18:53:23 2017 +0200 Renamed classes / properties / methods to use CategoryChannel instead of ChannelCategory to be consistant with how text / voice channels are named. commit 5c4777dc8cc443108f2e7e4afae98824c9a32b1f Author: pegasy Date: Sun Sep 24 19:08:25 2017 +0200 removed Guild from class name for ChannelCategory Renamed all properties to use Category instead of Parent Throw exception on GetUsers / GetInvites etc for categories commit e18bd8c799d2327270021c05866cb2e97ad4671b Author: pegasy Date: Sun Sep 24 15:49:51 2017 +0200 Add support for channel categories (as its own channel type) --- .../Entities/Channels/RpcGuildChannel.cs | 7 +++ .../Channels/GuildChannelProperties.cs | 4 ++ .../Entities/Channels/ICategoryChannel.cs | 12 +++++ .../Entities/Channels/IGuildChannel.cs | 6 ++- .../Entities/Guilds/IGuild.cs | 3 ++ src/Discord.Net.Rest/API/Common/Channel.cs | 2 + .../API/Rest/ModifyGuildChannelParams.cs | 2 + .../Entities/Channels/ChannelHelper.cs | 31 ++++++----- .../Entities/Channels/ChannelType.cs | 3 +- .../Entities/Channels/RestCategoryChannel.cs | 43 +++++++++++++++ .../Entities/Channels/RestGuildChannel.cs | 27 ++++++---- .../Entities/Guilds/GuildHelper.cs | 9 ++++ .../Entities/Guilds/RestGuild.cs | 28 +++++++--- .../Entities/Channels/RpcCategoryChannel.cs | 36 +++++++++++++ .../Channels/SocketCategoryChannel.cs | 53 +++++++++++++++++++ .../Entities/Channels/SocketGuildChannel.cs | 9 ++++ .../Entities/Guilds/SocketGuild.cs | 15 ++++-- 17 files changed, 256 insertions(+), 34 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs create mode 100644 src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs index 401263555..576a0489c 100644 --- a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -10,6 +10,7 @@ namespace Discord.Rpc { public ulong GuildId { get; } public int Position { get; private set; } + public ulong? CategoryId { get; private set; } internal RpcGuildChannel(DiscordRpcClient discord, ulong id, ulong guildId) : base(discord, id) @@ -57,6 +58,12 @@ namespace Discord.Rpc public override string ToString() => Name; //IGuildChannel + public Task GetCategoryAsync() + { + //Always fails + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + IGuild IGuildChannel.Guild { get diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 0ea196a4a..2ac6c8d52 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -26,5 +26,9 @@ /// Move the channel to the following position. This is 0-based! /// public Optional Position { get; set; } + /// + /// Sets the category for this channel + /// + public Optional CategoryId { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs new file mode 100644 index 000000000..0f7f5aa62 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface ICategoryChannel : IGuildChannel + { + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index c7cf0b3c2..c9841cb15 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -9,6 +9,10 @@ namespace Discord /// Gets the position of this channel in the guild's channel list, relative to others of the same type. int Position { get; } + /// Gets the parentid (category) of this channel in the guild's channel list. + ulong? CategoryId { get; } + /// Gets the parent channel (category) of this channel. + Task GetCategoryAsync(); /// Gets the guild this channel is a member of. IGuild Guild { get; } /// Gets the id of the guild this channel is a member of. @@ -23,7 +27,7 @@ namespace Discord Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); /// Returns a collection of all invites to this channel. Task> GetInvitesAsync(RequestOptions options = null); - + /// Modifies this guild channel. Task ModifyAsync(Action func, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 0fdd92405..2f0599d76 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -84,6 +84,7 @@ namespace Discord Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); @@ -93,6 +94,8 @@ namespace Discord Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. Task CreateVoiceChannelAsync(string name, RequestOptions options = null); + /// Creates a new channel category. + Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 608ddcf66..97c35a57b 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -23,6 +23,8 @@ namespace Discord.API public Optional Position { get; set; } [JsonProperty("permission_overwrites")] public Optional PermissionOverwrites { get; set; } + [JsonProperty("parent_id")] + public ulong? CategoryId { get; set; } //TextChannel [JsonProperty("topic")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index b4add2ac9..120eeb3a8 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -10,5 +10,7 @@ namespace Discord.API.Rest public Optional Name { get; set; } [JsonProperty("position")] public Optional Position { get; set; } + [JsonProperty("parent_id")] + public Optional CategoryId { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index fa870be17..ad5029785 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -14,13 +14,13 @@ namespace Discord.Rest internal static class ChannelHelper { //General - public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, + public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, RequestOptions options) - { + { await client.ApiClient.DeleteChannelAsync(channel.Id, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new GuildChannelProperties(); @@ -28,12 +28,13 @@ namespace Discord.Rest var apiArgs = new API.Rest.ModifyGuildChannelParams { Name = args.Name, - Position = args.Position + Position = args.Position, + CategoryId = args.CategoryId }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new TextChannelProperties(); @@ -42,13 +43,14 @@ namespace Discord.Rest { Name = args.Name, Position = args.Position, + CategoryId = args.CategoryId, Topic = args.Topic, IsNsfw = args.IsNsfw }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new VoiceChannelProperties(); @@ -58,6 +60,7 @@ namespace Discord.Rest Bitrate = args.Bitrate, Name = args.Name, Position = args.Position, + CategoryId = args.CategoryId, UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); @@ -87,7 +90,7 @@ namespace Discord.Rest } //Messages - public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, + public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) { var guildId = (channel as IGuildChannel)?.GuildId; @@ -98,7 +101,7 @@ namespace Discord.Rest var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } - public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, ulong? fromMessageId, Direction dir, int limit, RequestOptions options) { if (dir == Direction.Around) @@ -124,7 +127,7 @@ namespace Discord.Rest foreach (var model in models) { var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); - builder.Add(RestMessage.Create(client, channel, author, model)); + builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); }, @@ -180,7 +183,7 @@ namespace Discord.Rest var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); - } + } public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, IEnumerable messageIds, RequestOptions options) @@ -277,7 +280,7 @@ namespace Discord.Rest { await client.ApiClient.TriggerTypingIndicatorAsync(channel.Id, options).ConfigureAwait(false); } - public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, + public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, RequestOptions options) => new TypingNotifier(client, channel, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs index f05f1598e..e9f069a50 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs @@ -5,6 +5,7 @@ Text = 0, DM = 1, Voice = 2, - Group = 3 + Group = 3, + Category = 4 } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs new file mode 100644 index 000000000..397e14e76 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestCategoryChannel : RestGuildChannel, ICategoryChannel + { + internal RestCategoryChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + } + internal new static RestCategoryChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestCategoryChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + + // IGuildChannel + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException(); + Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException(); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 1ce1c8368..026d03cc8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -16,7 +16,7 @@ namespace Discord.Rest internal IGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } - + public ulong? CategoryId { get; private set; } public ulong GuildId => Guild.Id; internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -32,6 +32,8 @@ namespace Discord.Rest return RestTextChannel.Create(discord, guild, model); case ChannelType.Voice: return RestVoiceChannel.Create(discord, guild, model); + case ChannelType.Category: + return RestCategoryChannel.Create(discord, guild, model); default: // TODO: Channel categories return new RestGuildChannel(discord, guild, model.Id); @@ -61,7 +63,14 @@ namespace Discord.Rest } public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); - + + public async Task GetCategoryAsync() + { + if (CategoryId.HasValue) + return (await Guild.GetChannelAsync(CategoryId.Value).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + public OverwritePermissions? GetPermissionOverwrite(IUser user) { for (int i = 0; i < _overwrites.Length; i++) @@ -139,20 +148,20 @@ namespace Discord.Rest => await GetInvitesAsync(options).ConfigureAwait(false); async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - - OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); - async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); - async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) + async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); - async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); - async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) + async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); - + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7e25b53f3..12fdb075d 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -157,6 +157,15 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } + public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var args = new CreateGuildChannelParams(name, ChannelType.Category); + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestCategoryChannel.Create(client, guild, model); + } //Integrations public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 3b6a2bfa8..401a121e0 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -23,7 +23,7 @@ namespace Discord.Rest public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } - + public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } public ulong? SystemChannelId { get; private set; } @@ -114,7 +114,7 @@ namespace Discord.Rest Update(model); } public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) - { + { var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } @@ -155,7 +155,7 @@ namespace Discord.Rest public Task> GetChannelsAsync(RequestOptions options = null) => GuildHelper.GetChannelsAsync(this, Discord, options); public Task GetChannelAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetChannelAsync(this, Discord, id, options); + => GuildHelper.GetChannelAsync(this, Discord, id, options); public async Task GetTextChannelAsync(ulong id, RequestOptions options = null) { var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); @@ -176,6 +176,11 @@ namespace Discord.Rest var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); return channels.Select(x => x as RestVoiceChannel).Where(x => x != null).ToImmutableArray(); } + public async Task> GetCategoryChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.Select(x => x as RestCategoryChannel).Where(x => x != null).ToImmutableArray(); + } public async Task GetAFKChannelAsync(RequestOptions options = null) { @@ -199,7 +204,7 @@ namespace Discord.Rest public async Task GetEmbedChannelAsync(RequestOptions options = null) { var embedId = EmbedChannelId; - if (embedId.HasValue) + if (embedId.HasValue) return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); return null; } @@ -217,6 +222,8 @@ namespace Discord.Rest => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); //Integrations public Task> GetIntegrationsAsync(RequestOptions options = null) @@ -236,7 +243,7 @@ namespace Discord.Rest return null; } - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) { var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); @@ -320,6 +327,13 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + async Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetCategoryChannelsAsync(options).ConfigureAwait(false); + else + return null; + } async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -359,6 +373,8 @@ namespace Discord.Rest => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) + => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); @@ -368,7 +384,7 @@ namespace Discord.Rest async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); - IRole IGuild.GetRole(ulong id) + IRole IGuild.GetRole(ulong id) => GetRole(id); async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs new file mode 100644 index 000000000..cac766f92 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs @@ -0,0 +1,36 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcCategoryChannel : RpcGuildChannel, ICategoryChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + + internal RpcCategoryChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcCategoryChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcCategoryChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs new file mode 100644 index 000000000..d5a183b1e --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord.Audio; +using Discord.Rest; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel + { + public override IReadOnlyCollection Users + => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + + public IReadOnlyCollection Channels + => Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); + + internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + } + internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; + + // IGuildChannel + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException(); + Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException(); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 8e24a5196..2163daf55 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -17,6 +17,9 @@ namespace Discord.WebSocket public SocketGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } + public ulong? CategoryId { get; private set; } + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; public IReadOnlyCollection PermissionOverwrites => _overwrites; public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); @@ -34,6 +37,8 @@ namespace Discord.WebSocket return SocketTextChannel.Create(guild, state, model); case ChannelType.Voice: return SocketVoiceChannel.Create(guild, state, model); + case ChannelType.Category: + return SocketCategoryChannel.Create(guild, state, model); default: // TODO: Proper implementation for channel categories return new SocketGuildChannel(guild.Discord, model.Id, guild); @@ -43,6 +48,7 @@ namespace Discord.WebSocket { Name = model.Name.Value; Position = model.Position.Value; + CategoryId = model.CategoryId; var overwrites = model.PermissionOverwrites.Value; var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); @@ -129,6 +135,9 @@ namespace Discord.WebSocket IGuild IGuildChannel.Guild => Guild; ulong IGuildChannel.GuildId => Guild.Id; + Task IGuildChannel.GetCategoryAsync() + => Task.FromResult(Category); + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index c4158c136..ea68a8f54 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -75,7 +75,7 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } - public SocketGuildChannel EmbedChannel + public SocketGuildChannel EmbedChannel { get { @@ -95,6 +95,8 @@ namespace Discord.WebSocket => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection VoiceChannels => Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray(); + public IReadOnlyCollection CategoryChannels + => Channels.Select(x => x as SocketCategoryChannel).Where(x => x != null).ToImmutableArray(); public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; public SocketRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Channels @@ -317,6 +319,9 @@ namespace Discord.WebSocket => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); @@ -348,7 +353,7 @@ namespace Discord.WebSocket return value; return null; } - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); internal SocketRole AddRole(RoleModel model) @@ -594,7 +599,7 @@ namespace Discord.WebSocket try { await RepopulateAudioStreamsAsync().ConfigureAwait(false); - await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -651,6 +656,8 @@ namespace Discord.WebSocket => Task.FromResult(GetTextChannel(id)); Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(VoiceChannels); + Task> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options) + => Task.FromResult>(CategoryChannels); Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetVoiceChannel(id)); Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) @@ -665,6 +672,8 @@ namespace Discord.WebSocket => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) + => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); From 5c8c78498b0f26358cbad5271f55bd2ab58dd7f0 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:30:04 -0500 Subject: [PATCH 20/27] Add EditorConfig (#876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit c692306fcc1e86ab92dd10683d3719f16c02a249 Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 10:49:00 2017 +0900 Add target of the internal fields commit 2d08f9a655b4949c1177f778d0f499047484a537 Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 10:17:42 2017 +0900 Add some more extension commit 4f19b835ffe8c64a93a9b4659e60b03ac797760f Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 01:19:11 2017 +0900 Add naming rules commit af756cd9feb630baadbf6025cbb079cd9e1f45cb Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 00:35:30 2017 +0900 Add basic .NET style rules commit 503ece558b4f07bd8008157d3aeb6a4e7100d349 Author: Acid Chicken (硫酸鶏) Date: Fri Nov 10 22:36:52 2017 +0900 Add EditorConfig --- .editorconfig | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..696bb866b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,113 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true + +[*.{csproj,json,md,nuspec,yml}] +indent_size = 2 +indent_style = space + +[*.{sln,xml}] +indent_style = tab + +[*.cs] +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_operators = true:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +csharp_prefer_simple_default_expression = false:none + +csharp_style_throw_expression = true:none +csharp_style_conditional_delegate_call = true:none +csharp_prefer_braces = false:none + +dotnet_sort_system_directives_first = false + +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_within_query_expression_clauses = true + +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false + +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +dotnet_naming_rule.all_of_const_fields_are_pascal_case.symbols = const_fields +dotnet_naming_rule.all_of_const_fields_are_pascal_case.style = pascal_case +dotnet_naming_rule.all_of_const_fields_are_pascal_case.severity = suggestion + +dotnet_naming_rule.all_of_local_fields_without_const_are_camel_case.symbols = local_fields +dotnet_naming_rule.all_of_local_fields_without_const_are_camel_case.style = starts_with_low_line_camel_case +dotnet_naming_rule.all_of_local_fields_without_const_are_camel_case.severity = suggestion + +dotnet_naming_rule.all_of_interfaces_starts_with_low_line_camel_case.symbols = interfaces +dotnet_naming_rule.all_of_interfaces_starts_with_low_line_camel_case.style = starts_with_i_pascal_case +dotnet_naming_rule.all_of_interfaces_starts_with_low_line_camel_case.severity = suggestion + +dotnet_naming_rule.default_is_pascal_case.symbols = without_interfaces_and_fields +dotnet_naming_rule.default_is_pascal_case.style = pascal_case +dotnet_naming_rule.default_is_pascal_case.severity = suggestion + +dotnet_naming_symbols.const_fields.applicable_kinds = field +dotnet_naming_symbols.const_fields.applicable_accessibilities = * +dotnet_naming_symbols.const_fields.required_modifiers = const + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = * + +dotnet_naming_symbols.local_fields.applicable_kinds = field +dotnet_naming_symbols.local_fields.applicable_accessibilities = internal, private, protected, protected_internal +dotnet_naming_symbols.local_fields.required_modifiers = abstract, must_inherit, readonly, static, shared + +dotnet_naming_symbols.without_interfaces_and_fields.applicable_kinds = class, struct, enum, property, method, event, namespace, delegate, type_parameter +dotnet_naming_symbols.without_interfaces_and_fields.applicable_accessibilities = * + +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.starts_with_i_pascal_case.required_prefix = I +dotnet_naming_style.starts_with_i_pascal_case.capitalization = pascal_case + +dotnet_naming_style.starts_with_low_line_camel_case.required_prefix = _ +dotnet_naming_style.starts_with_low_line_camel_case.capitalization = camel_case From cd82a0f70cb7c350c36e35f9a70c8c1f4aa6bb27 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:30:52 -0500 Subject: [PATCH 21/27] Added a contributing guide (#828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 779ec4677e8b3979f83bcc6a673062ff6caa8be6 Author: Christopher F Date: Wed Sep 27 21:14:54 2017 -0400 Added a contributing guide Happy Hacktoberfest 🎃 --- CONTRIBUTING.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8248291e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing + +Discord.Net is an open-source project, and we appreciate any and all +contributions made by our community. However, please conform to the +following guidelines when possible: + +## Development Cycle + +We prefer all changes to the library to be discussed beforehand, +either in a GitHub issue, or in a discussion in our Discord channel +with library regulars or other contributors. + +Issues that are tagged as "up for grabs" are free to be picked up by +any member of the community. + +### Pull Requests + +We prefer pull-requests that are descriptive of the changes being made +and highlight any potential benefits/drawbacks of the change, but these +types of write-ups are not required. See this [merge request](https://github.com/RogueException/Discord.Net/pull/793) +for an example of a well-written description. + +## Semantic Versioning + +This project follows [Semantic Versioning](http://semver.org/). When +writing changes to this project, it is recommended to write changes +that are SemVer compliant with the latest version of the library in +development. + +The working release should be the latest build off of the `dev` branch, +but can also be found on the [development board](https://github.com/RogueException/Discord.Net/projects/1). + +We follow the .NET Foundation's [Breaking Change Rules](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md) +when determining the SemVer compliance of a change. + +Obsoleting a method is considered a **minor** increment. + +## Coding Style + +We attempt to conform to the .NET Foundation's [Coding Style](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md) +where possible. + +As a general rule, follow the coding style already set in the file you +are editing, or look at a similar file if you are adding a new one. \ No newline at end of file From 804d9188e79df57fc2d298107f9f1260c83b1ea1 Mon Sep 17 00:00:00 2001 From: BinkanSalaryman Date: Sun, 7 Jan 2018 04:33:00 +0100 Subject: [PATCH 22/27] Fix flawed bulk message deletion (#872) * Fix flawed bulk message deletion https://github.com/RogueException/Discord.Net/issues/871, consider changing DeleteMessagesParams.MessageIds type to I(Readonly)List or IEnumerable to avoid unnecessary copying (batch.ToArray()) * Update code formatting --- .../Entities/Channels/ChannelHelper.cs | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index ad5029785..585e0a354 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -188,21 +188,27 @@ namespace Discord.Rest public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, IEnumerable messageIds, RequestOptions options) { + const int BATCH_SIZE = 100; + var msgs = messageIds.ToArray(); - if (msgs.Length < 100) - { - var args = new DeleteMessagesParams(msgs); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); - } - else + int batches = msgs.Length / BATCH_SIZE; + for (int i = 0; i <= batches; i++) { - var batch = new ulong[100]; - for (int i = 0; i < (msgs.Length + 99) / 100; i++) + ArraySegment batch; + if (i < batches) { - Array.Copy(msgs, i * 100, batch, 0, Math.Min(msgs.Length - (100 * i), 100)); - var args = new DeleteMessagesParams(batch); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + batch = new ArraySegment(msgs, i * BATCH_SIZE, BATCH_SIZE); } + else + { + batch = new ArraySegment(msgs, i * BATCH_SIZE, msgs.Length - batches * BATCH_SIZE); + if (batch.Count == 0) + { + break; + } + } + var args = new DeleteMessagesParams(batch.ToArray()); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); } } From edfbd055bb3ee62c565370af3abb84ab7214d2d6 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sat, 6 Jan 2018 19:35:17 -0800 Subject: [PATCH 23/27] Fix #854 Added ViewChannel enum and property to channel permissions (#874) * Fix #854 Added ViewChannel enum and property to channel permissions * replaced usages of ChannelPermission#ReadMessages with ViewChannel * rename parameter of ChannelPermissions constructor * made OverwritePermissions#ReadMessages obsolete, use ViewChannel instead * Fix #854 Added ViewChannel enum and property to channel permissions replaced usages of ChannelPermission#ReadMessages with ViewChannel rename parameter of ChannelPermissions constructor made OverwritePermissions#ReadMessages obsolete, use ViewChannel instead * renamed readMessages parameter in ChannelPermissions constructor and Modify * fixed channel permission tests to use ChannelPermission enum instead of GuildPermission enum * replaced usages of readmessages in channel permission tests * resolve build warnings for permission tests --- .../Entities/Permissions/ChannelPermission.cs | 4 +- .../Permissions/ChannelPermissions.cs | 18 +++-- .../Permissions/OverwritePermissions.cs | 12 ++-- src/Discord.Net.Core/Utils/Permissions.cs | 2 +- .../Entities/Channels/ChannelHelper.cs | 4 +- .../Entities/Guilds/RestGuild.cs | 2 +- .../Entities/Channels/SocketTextChannel.cs | 4 +- .../Tests.ChannelPermissions.cs | 65 ++++++++++--------- .../Tests.GuildPermissions.cs | 7 +- 9 files changed, 67 insertions(+), 51 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 3e438f43f..740b6c30b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -11,7 +11,9 @@ namespace Discord // Text AddReactions = 0x00_00_00_40, - ReadMessages = 0x00_00_04_00, + [Obsolete("Use ViewChannel instead.")] + ReadMessages = ViewChannel, + ViewChannel = 0x00_00_04_00, SendMessages = 0x00_00_08_00, SendTTSMessages = 0x00_00_10_00, ManageMessages = 0x00_00_20_00, diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 4c11d0db0..1a8aad53c 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -41,7 +41,11 @@ namespace Discord /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); /// If True, a user may join channels. - public bool ReadMessages => Permissions.GetValue(RawValue, ChannelPermission.ReadMessages); + [Obsolete("Use ViewChannel instead.")] + public bool ReadMessages => ViewChannel; + /// If True, a user may view channels. + public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); + /// If True, a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); /// If True, a user may send text-to-speech messages. @@ -82,7 +86,7 @@ namespace Discord private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? manageRoles = null, bool? manageWebhooks = null) @@ -92,7 +96,7 @@ namespace Discord Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref value, addReactions, ChannelPermission.AddReactions); - Permissions.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref value, viewChannel, ChannelPermission.ViewChannel); Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); Permissions.SetValue(ref value, manageMessages, ChannelPermission.ManageMessages); @@ -116,11 +120,11 @@ namespace Discord /// Creates a new ChannelPermissions with the provided permissions. public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, - bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool viewChannel = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool manageRoles = false, bool manageWebhooks = false) - : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + : this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } @@ -128,11 +132,11 @@ namespace Discord /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool useExternalEmojis = false, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? manageRoles = null, bool? manageWebhooks = null) - => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks); diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index c3e296e2c..108b67273 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -27,7 +28,10 @@ namespace Discord /// If Allowed, a user may add reactions. public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); /// If Allowed, a user may join channels. - public PermValue ReadMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessages); + [Obsolete("Use ViewChannel instead.")] + public PermValue ReadMessages => ViewChannel; + /// If Allowed, a user may join channels. + public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel); /// If Allowed, a user may send messages. public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); /// If Allowed, a user may send text-to-speech messages. @@ -72,7 +76,7 @@ namespace Discord private OverwritePermissions(ulong allowValue, ulong denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, PermValue? addReactions = null, - PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, + PermValue? viewChannel = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? useExternalEmojis = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, @@ -81,7 +85,7 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref allowValue, ref denyValue, addReactions, ChannelPermission.AddReactions); - Permissions.SetValue(ref allowValue, ref denyValue, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref allowValue, ref denyValue, viewChannel, ChannelPermission.ViewChannel); Permissions.SetValue(ref allowValue, ref denyValue, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref allowValue, ref denyValue, sendTTSMessages, ChannelPermission.SendTTSMessages); Permissions.SetValue(ref allowValue, ref denyValue, manageMessages, ChannelPermission.ManageMessages); diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a7de90623..367926dd1 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -152,7 +152,7 @@ namespace Discord if (channel is ITextChannel textChannel) { - if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + if (!GetValue(resolvedPermissions, ChannelPermission.ViewChannel)) { //No read permission on a text channel removes all other permissions resolvedPermissions = 0; diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 585e0a354..f4b6c7f23 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -244,7 +244,7 @@ namespace Discord.Rest if (model == null) return null; var user = RestGuildUser.Create(client, guild, model); - if (!user.GetPermissions(channel).ReadMessages) + if (!user.GetPermissions(channel).ViewChannel) return null; return user; @@ -265,7 +265,7 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); return models .Select(x => RestGuildUser.Create(client, guild, x)) - .Where(x => x.GetPermissions(channel).ReadMessages) + .Where(x => x.GetPermissions(channel).ViewChannel) .ToImmutableArray(); }, nextPage: (info, lastPage) => diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 401a121e0..76ddc07ca 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -197,7 +197,7 @@ namespace Discord.Rest var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); var user = await GetCurrentUserAsync(options).ConfigureAwait(false); return channels - .Where(c => user.GetPermissions(c).ReadMessages) + .Where(c => user.GetPermissions(c).ViewChannel) .OrderBy(c => c.Position) .FirstOrDefault(); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index b6a304b50..7b8f572d2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -25,7 +25,7 @@ namespace Discord.WebSocket public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), - ChannelPermission.ReadMessages)).ToImmutableArray(); + ChannelPermission.ViewChannel)).ToImmutableArray(); internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) @@ -107,7 +107,7 @@ namespace Discord.WebSocket { var guildPerms = Permissions.ResolveGuild(Guild, user); var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); - if (Permissions.GetValue(channelPerms, ChannelPermission.ReadMessages)) + if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) return user; } return null; diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs index 92234e88b..ac8ede4e4 100644 --- a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public void TestChannelPermission() + public Task TestChannelPermission() { var perm = new ChannelPermissions(); @@ -29,7 +29,7 @@ namespace Discord ulong textChannel = (ulong)( ChannelPermission.CreateInstantInvite | ChannelPermission.ManageChannels | ChannelPermission.AddReactions - | ChannelPermission.ReadMessages + | ChannelPermission.ViewChannel | ChannelPermission.SendMessages | ChannelPermission.SendTTSMessages | ChannelPermission.ManageMessages @@ -59,7 +59,7 @@ namespace Discord // DM Channels ulong dmChannel = (ulong)( - ChannelPermission.ReadMessages + ChannelPermission.ViewChannel | ChannelPermission.SendMessages | ChannelPermission.EmbedLinks | ChannelPermission.AttachFiles @@ -82,9 +82,10 @@ namespace Discord | ChannelPermission.UseVAD ); Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); + return Task.CompletedTask; } - - public void TestChannelPermissionModify() + + public Task TestChannelPermissionModify() { // test channel permission modify @@ -96,7 +97,7 @@ namespace Discord // ensure that when modified it works perm = perm.Modify(createInstantInvite: true); Assert.True(perm.CreateInstantInvite); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.CreateInstantInvite); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.CreateInstantInvite); // set false again, move on to next permission perm = perm.Modify(createInstantInvite: false); @@ -108,7 +109,7 @@ namespace Discord perm = perm.Modify(manageChannel: true); Assert.True(perm.ManageChannel); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageChannels); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageChannels); perm = perm.Modify(manageChannel: false); Assert.False(perm.ManageChannel); @@ -119,21 +120,21 @@ namespace Discord perm = perm.Modify(addReactions: true); Assert.True(perm.AddReactions); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.AddReactions); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.AddReactions); perm = perm.Modify(addReactions: false); Assert.False(perm.AddReactions); Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); // individual permission test - Assert.False(perm.ReadMessages); + Assert.False(perm.ViewChannel); - perm = perm.Modify(readMessages: true); - Assert.True(perm.ReadMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessages); + perm = perm.Modify(viewChannel: true); + Assert.True(perm.ViewChannel); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ViewChannel); - perm = perm.Modify(readMessages: false); - Assert.False(perm.ReadMessages); + perm = perm.Modify(viewChannel: false); + Assert.False(perm.ViewChannel); Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); // individual permission test @@ -141,7 +142,7 @@ namespace Discord perm = perm.Modify(sendMessages: true); Assert.True(perm.SendMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.SendMessages); perm = perm.Modify(sendMessages: false); Assert.False(perm.SendMessages); @@ -152,7 +153,7 @@ namespace Discord perm = perm.Modify(sendTTSMessages: true); Assert.True(perm.SendTTSMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendTTSMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.SendTTSMessages); perm = perm.Modify(sendTTSMessages: false); Assert.False(perm.SendTTSMessages); @@ -163,7 +164,7 @@ namespace Discord perm = perm.Modify(manageMessages: true); Assert.True(perm.ManageMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageMessages); perm = perm.Modify(manageMessages: false); Assert.False(perm.ManageMessages); @@ -174,7 +175,7 @@ namespace Discord perm = perm.Modify(embedLinks: true); Assert.True(perm.EmbedLinks); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.EmbedLinks); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.EmbedLinks); perm = perm.Modify(embedLinks: false); Assert.False(perm.EmbedLinks); @@ -185,7 +186,7 @@ namespace Discord perm = perm.Modify(attachFiles: true); Assert.True(perm.AttachFiles); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.AttachFiles); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.AttachFiles); perm = perm.Modify(attachFiles: false); Assert.False(perm.AttachFiles); @@ -196,7 +197,7 @@ namespace Discord perm = perm.Modify(readMessageHistory: true); Assert.True(perm.ReadMessageHistory); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessageHistory); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ReadMessageHistory); perm = perm.Modify(readMessageHistory: false); Assert.False(perm.ReadMessageHistory); @@ -207,7 +208,7 @@ namespace Discord perm = perm.Modify(mentionEveryone: true); Assert.True(perm.MentionEveryone); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.MentionEveryone); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MentionEveryone); perm = perm.Modify(mentionEveryone: false); Assert.False(perm.MentionEveryone); @@ -218,7 +219,7 @@ namespace Discord perm = perm.Modify(useExternalEmojis: true); Assert.True(perm.UseExternalEmojis); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseExternalEmojis); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.UseExternalEmojis); perm = perm.Modify(useExternalEmojis: false); Assert.False(perm.UseExternalEmojis); @@ -229,7 +230,7 @@ namespace Discord perm = perm.Modify(connect: true); Assert.True(perm.Connect); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.Connect); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.Connect); perm = perm.Modify(connect: false); Assert.False(perm.Connect); @@ -240,7 +241,7 @@ namespace Discord perm = perm.Modify(speak: true); Assert.True(perm.Speak); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.Speak); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.Speak); perm = perm.Modify(speak: false); Assert.False(perm.Speak); @@ -251,7 +252,7 @@ namespace Discord perm = perm.Modify(muteMembers: true); Assert.True(perm.MuteMembers); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.MuteMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MuteMembers); perm = perm.Modify(muteMembers: false); Assert.False(perm.MuteMembers); @@ -262,7 +263,7 @@ namespace Discord perm = perm.Modify(deafenMembers: true); Assert.True(perm.DeafenMembers); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.DeafenMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.DeafenMembers); perm = perm.Modify(deafenMembers: false); Assert.False(perm.DeafenMembers); @@ -273,7 +274,7 @@ namespace Discord perm = perm.Modify(moveMembers: true); Assert.True(perm.MoveMembers); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.MoveMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MoveMembers); perm = perm.Modify(moveMembers: false); Assert.False(perm.MoveMembers); @@ -284,7 +285,7 @@ namespace Discord perm = perm.Modify(useVoiceActivation: true); Assert.True(perm.UseVAD); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseVAD); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.UseVAD); perm = perm.Modify(useVoiceActivation: false); Assert.False(perm.UseVAD); @@ -295,7 +296,7 @@ namespace Discord perm = perm.Modify(manageRoles: true); Assert.True(perm.ManageRoles); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageRoles); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageRoles); perm = perm.Modify(manageRoles: false); Assert.False(perm.ManageRoles); @@ -306,19 +307,21 @@ namespace Discord perm = perm.Modify(manageWebhooks: true); Assert.True(perm.ManageWebhooks); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageWebhooks); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageWebhooks); perm = perm.Modify(manageWebhooks: false); Assert.False(perm.ManageWebhooks); Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + return Task.CompletedTask; } [Fact] - public void TestChannelTypeResolution() + public Task TestChannelTypeResolution() { ITextChannel someChannel = null; // null channels will throw exception Assert.Throws(() => ChannelPermissions.All(someChannel)); + return Task.CompletedTask; } } } diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs index dc51600cf..bb113d221 100644 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public void TestGuildPermission() + public Task TestGuildPermission() { // Test Guild Permission Constructors var perm = new GuildPermissions(); @@ -46,10 +46,12 @@ namespace Discord GuildPermission.SendMessages | GuildPermission.SendTTSMessages | GuildPermission.EmbedLinks | GuildPermission.AttachFiles); Assert.Equal(webHookPermissions, GuildPermissions.Webhook.RawValue); + + return Task.CompletedTask; } [Fact] - public void TestGuildPermissionModify() + public Task TestGuildPermissionModify() { var perm = new GuildPermissions(); @@ -298,6 +300,7 @@ namespace Discord Assert.False(perm.ManageEmojis); Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + return Task.CompletedTask; } } From 5bbd9bba8254e3b4ba21cf359955fd091a94d8a6 Mon Sep 17 00:00:00 2001 From: ObsidianMinor Date: Sat, 6 Jan 2018 21:43:11 -0600 Subject: [PATCH 24/27] Renamed existing Flatten method to FlattenAsync and added new Flatten method. Also fixed ClientHelper using incorrect guild batch count. (#744) --- .../Readers/UserTypeReader.cs | 14 ++--- .../Extensions/AsyncEnumerableExtensions.cs | 54 ++++++++++++++++++- src/Discord.Net.Rest/ClientHelper.cs | 4 +- .../Entities/Guilds/RestGuild.cs | 2 +- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index ca337aaf6..8fc330d4c 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -13,7 +13,7 @@ namespace Discord.Commands public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); - IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? + IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better IReadOnlyCollection guildUsers = ImmutableArray.Create(); ulong id; @@ -45,7 +45,7 @@ namespace Discord.Commands string username = input.Substring(0, index); if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) { - var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + var channelUser = await channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); @@ -57,8 +57,9 @@ namespace Discord.Commands //By Username (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) - AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); + await channelUsers + .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); @@ -66,8 +67,9 @@ namespace Discord.Commands //By Nickname (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))) - AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); + await channelUsers + .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, (guildUser as IGuildUser).Nickname == input ? 0.60f : 0.50f); diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index f52edd719..345154f1d 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -1,14 +1,64 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Discord { public static class AsyncEnumerableExtensions { - public static async Task> Flatten(this IAsyncEnumerable> source) + /// + /// Flattens the specified pages into one asynchronously + /// + /// + /// + /// + public static async Task> FlattenAsync(this IAsyncEnumerable> source) { - return (await source.ToArray().ConfigureAwait(false)).SelectMany(x => x); + return await source.Flatten().ToArray().ConfigureAwait(false); + } + + public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) + { + return new PagedCollectionEnumerator(source); + } + + internal class PagedCollectionEnumerator : IAsyncEnumerator, IAsyncEnumerable + { + readonly IAsyncEnumerator> _source; + IEnumerator _enumerator; + + public IAsyncEnumerator GetEnumerator() => this; + + internal PagedCollectionEnumerator(IAsyncEnumerable> source) + { + _source = source.GetEnumerator(); + } + + public T Current => _enumerator.Current; + + public void Dispose() + { + _enumerator?.Dispose(); + _source.Dispose(); + } + + public async Task MoveNext(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if(!_enumerator?.MoveNext() ?? true) + { + if (!await _source.MoveNext(cancellationToken).ConfigureAwait(false)) + return false; + + _enumerator?.Dispose(); + _enumerator = _source.Current.GetEnumerator(); + return _enumerator.MoveNext(); + } + + return true; + } } } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 26d8c720e..5c9e26433 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -79,7 +79,7 @@ namespace Discord.Rest ulong? fromGuildId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( - DiscordConfig.MaxUsersPerBatch, + DiscordConfig.MaxGuildsPerBatch, async (info, ct) => { var args = new GetGuildSummariesParams @@ -106,7 +106,7 @@ namespace Discord.Rest } public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) { - var summaryModels = await GetGuildSummariesAsync(client, null, null, options).Flatten(); + var summaryModels = await GetGuildSummariesAsync(client, null, null, options).FlattenAsync().ConfigureAwait(false); var guilds = ImmutableArray.CreateBuilder(); foreach (var summaryModel in summaryModels) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 76ddc07ca..5d12731a6 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -413,7 +413,7 @@ namespace Discord.Rest async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return (await GetUsersAsync(options).Flatten().ConfigureAwait(false)).ToImmutableArray(); + return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return ImmutableArray.Create(); } From d5e9d6f9c1e70c7b6c569615e491de1149324f56 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:54:50 -0500 Subject: [PATCH 25/27] Fix ShardedClient#GetShardFor null-case (#742) This resolves #742. Common cases for IGuild being null on access are DMs (since they do not belong to a guild) - this change resolves null guilds to shard zero, where DMs are also received. --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index e827909d9..4e99ae28d 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -133,7 +133,7 @@ namespace Discord.WebSocket private DiscordSocketClient GetShardFor(ulong guildId) => GetShard(GetShardIdFor(guildId)); public DiscordSocketClient GetShardFor(IGuild guild) - => GetShardFor(guild.Id); + => GetShardFor(guild?.Id ?? 0); /// public override async Task GetApplicationInfoAsync(RequestOptions options = null) From fdd2c80d2b47c4dcb507ed54352f592f890b9f93 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 23:20:21 -0500 Subject: [PATCH 26/27] Fix outgoing activity sending (#916) This change resolves #916 Discord requires the {"type": 0} payload for all non-streaming activities. This change fixes a bug where name-only games would fail to include this payload, causing the presence change to be discarded by Discord. --- .../DiscordSocketClient.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 35a22edc4..cb3f23c57 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -328,9 +328,9 @@ namespace Discord.WebSocket } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { - if (streamUrl != null) + if (!string.IsNullOrEmpty(streamUrl)) Activity = new StreamingGame(name, streamUrl, streamType); - else if (name != null) + else if (!string.IsNullOrEmpty(name)) Activity = new Game(name); else Activity = null; @@ -346,21 +346,24 @@ namespace Discord.WebSocket { if (CurrentUser == null) return; - var activity = Activity; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, activity); + CurrentUser.Presence = new SocketPresence(status, Activity); var gameModel = new GameModel(); // Discord only accepts rich presence over RPC, don't even bother building a payload - if (activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); - if (activity is StreamingGame stream) + if (Activity is RichGame game) + throw new NotSupportedException("Outgoing Rich Presences are not supported"); + else if (Activity is StreamingGame stream) { gameModel.StreamUrl = stream.Url; gameModel.StreamType = stream.StreamType; } - else if (activity != null) - gameModel.Name = activity.Name; + else if (Activity != null) + { + gameModel.Name = Activity.Name; + gameModel.StreamType = StreamType.NotStreaming; + } await ApiClient.SendStatusUpdateAsync( status, From 9d77a3cd3757a48a3ca77aa50071016209e40f02 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 23:36:08 -0500 Subject: [PATCH 27/27] Fix parsing timestamps with a timezone attached Resolves #918. For some reason (that i'm sure will bite me in the ass later), we configured Json.Net with a time format that forced UTC - even when the API appends a timezone to the timestamp. Removing the custom time format seems to resolve this issue. --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ab47b1e98..689cba9c3 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 #pragma warning disable CS0618 using Discord.API.Rest; using Discord.Net; @@ -52,7 +52,7 @@ namespace Discord.API _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; - _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1);