From d57c6ae64ffc1a9d5decbb75e3c891149e4dfae9 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Thu, 31 Aug 2017 17:13:17 -0400 Subject: [PATCH] Add Webhook API models, REST implementation, and Socket bridges. --- .../Entities/Channels/ITextChannel.cs | 9 ++ .../Entities/Guilds/IGuild.cs | 5 ++ .../Entities/Users/IWebhookUser.cs | 1 - .../Entities/Webhooks/IWebhook.cs | 34 +++++++ .../Entities/Webhooks/WebhookProperties.cs | 27 ++++++ src/Discord.Net.Core/IDiscordClient.cs | 2 + src/Discord.Net.Rest/API/Common/Webhook.cs | 25 ++++++ .../API/Rest/CreateWebhookMessageParams.cs | 2 + .../API/Rest/CreateWebhookParams.cs | 14 +++ .../API/Rest/ModifyWebhookParams.cs | 14 +++ src/Discord.Net.Rest/BaseDiscordClient.cs | 3 + src/Discord.Net.Rest/ClientHelper.cs | 8 ++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 85 +++++++++++++++++- 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 | 89 +++++++++++++++++++ .../Entities/Webhooks/WebhookHelper.cs | 34 +++++++ .../Entities/Channels/RpcTextChannel.cs | 16 +++- .../API/Gateway/WebhookUpdateEvent.cs | 13 +++ .../Entities/Channels/SocketTextChannel.cs | 18 +++- .../Entities/Guilds/SocketGuild.cs | 11 +++ .../DiscordWebhookClient.cs | 2 +- 25 files changed, 476 insertions(+), 7 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 diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 038faf6bc..fbdde88af 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace Discord @@ -10,5 +12,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 7874f5fd1..7d12d1cb0 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -114,5 +114,10 @@ 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 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); } } \ No newline at end of file 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..4a8bb309b --- /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, string webhookToken = null, 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..e00ba86be --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -0,0 +1,27 @@ +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; } + } +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 23e8e9c5b..b694ccf4e 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, string webhookToken = null, 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/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs index 970a30201..279f538bd 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -8,6 +8,8 @@ namespace Discord.API.Rest { [JsonProperty("content")] public string Content { get; } + [JsonProperty("wait")] + public bool ReturnCreatedMessage { get; set; } [JsonProperty("nonce")] public Optional Nonce { 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..1d385c328 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs @@ -0,0 +1,14 @@ +#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; } + } +} diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index ed12ff383..9b88140ae 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -160,6 +160,9 @@ namespace Discord.Rest Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); + Task IDiscordClient.GetWebhookAsync(ulong id, string webhookToken, 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..12f2f95e6 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, string webhookToken, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, webhookToken); + 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 a6c42782a..e34ce2603 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -472,11 +472,13 @@ 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, string webhookToken = null, RequestOptions options = null) { - if (AuthTokenType != TokenType.Webhook) + if (AuthTokenType != TokenType.Webhook && string.IsNullOrWhiteSpace(webhookToken)) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + webhookToken = webhookToken ?? AuthToken; + Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) @@ -486,7 +488,11 @@ namespace Discord.API 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); + if (args.ReturnCreatedMessage) + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{webhookToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + + await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{webhookToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return null; } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { @@ -1153,6 +1159,79 @@ 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); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, new BucketIds(), options: options); + } + public async Task GetWebhookAsync(ulong webhookId, string webhookToken = null, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (!string.IsNullOrWhiteSpace(webhookToken)) + { + webhookToken = "/" + webhookToken; + options.IgnoreState = true; + } + + try + { + return await SendAsync("GET", () => $"webhooks/{webhookId}{webhookToken}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + public async Task ModifyWebhookAsync(ulong webhookId, ModifyWebhookParams args, string webhookToken = null, 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 (!string.IsNullOrWhiteSpace(webhookToken)) + { + webhookToken = "/" + webhookToken; + options.IgnoreState = true; + } + + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}{webhookToken}", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task DeleteWebhookAsync(ulong webhookId, string webhookToken = null, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + options.IgnoreState = true; + + if (!string.IsNullOrWhiteSpace(webhookToken)) + { + webhookToken = "/" + webhookToken; + options.IgnoreState = true; + } + + await SendAsync("DELETE", () => $"webhooks/{webhookId}{webhookToken}", 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..1169bca9e 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, string webhookToken = null, RequestOptions options = null) + => ClientHelper.GetWebhookAsync(this, id, webhookToken, 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, string webhookToken, RequestOptions options) + => await GetWebhookAsync(id, webhookToken, options); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 6b7dca3a9..543b755ca 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 { @@ -279,6 +280,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 d7405fb4a..a13412065 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -73,8 +73,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 5cfb1e566..9fe31f69e 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -247,5 +247,19 @@ namespace Discord.Rest model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); 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(); + } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 11971a5c1..715b7832f 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -239,6 +239,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})"; @@ -361,5 +367,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..c93479c89 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -0,0 +1,89 @@ +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; } + internal ITextChannel Channel { get; } + public string Token { get; private set; } + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong ChannelId { 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) + : base(discord, id) + { + Guild = guild; + } + internal RestWebhook(BaseDiscordClient discord, ITextChannel channel, ulong id) + : this(discord, channel.Guild, id) + { + Channel = channel; + } + internal static RestWebhook Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestWebhook(discord, guild, model.Id); + entity.Update(model); + return entity; + } + internal static RestWebhook Create(BaseDiscordClient discord, ITextChannel channel, Model model) + { + var entity = new RestWebhook(discord, channel, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Token = model.Token; + ChannelId = model.ChannelId; + 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, Token, 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, string webhookToken = null, RequestOptions options = null) + { + var model = await WebhookHelper.ModifyAsync(this, Discord, func, webhookToken, options).ConfigureAwait(false); + Update(model); + } + + public Task DeleteAsync(string webhookToken = null, RequestOptions options = null) + => WebhookHelper.DeleteAsync(this, Discord, webhookToken, options); + + public override string ToString() => Name; + private string DebuggerDisplay => $"{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, string webhookToken, RequestOptions options) + => ModifyAsync(func, webhookToken, options); + Task IDeletable.DeleteAsync(RequestOptions options) + => DeleteAsync(Token, 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..0bd518dab --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs @@ -0,0 +1,34 @@ +using Discord.API.Rest; +using System; +using System.Threading.Tasks; +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, string webhookToken, 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); + + return await client.ApiClient.ModifyWebhookAsync(webhook.Id, apiArgs, webhookToken, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IWebhook webhook, BaseDiscordClient client, string webhookToken, + RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(webhook.Id, webhookToken, options).ConfigureAwait(false); + } + + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 72b45e466..fee63946c 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -66,11 +66,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 c22523e00..48577d51e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -108,10 +108,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 aae18be36..579f04394 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -423,6 +423,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); + //Voice States internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { @@ -659,5 +665,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 9695099ee..beae73461 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -56,7 +56,7 @@ namespace Discord.Webhook args.Username = username; if (avatarUrl != null) args.AvatarUrl = avatarUrl; - await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); + await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options: options).ConfigureAwait(false); } #if FILESYSTEM