Skip to content

Commit

Permalink
Add missing REST Webhook implemenation (#843)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
AntiTcb authored and foxbot committed Dec 23, 2017
1 parent a19ff18 commit 7b2ddd0
Show file tree
Hide file tree
Showing 27 changed files with 679 additions and 40 deletions.
8 changes: 8 additions & 0 deletions src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Discord
Expand All @@ -19,5 +20,12 @@ public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel

/// <summary> Modifies this text channel. </summary>
Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null);

/// <summary> Creates a webhook in this text channel. </summary>
Task<IWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null);
/// <summary> Gets the webhook in this text channel with the provided id, or null if not found. </summary>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary> Gets the webhooks for this text channel. </summary>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);
}
}
5 changes: 5 additions & 0 deletions src/Discord.Net.Core/Entities/Guilds/IGuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ public interface IGuild : IDeletable, ISnowflakeEntity
/// <summary> 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. </summary>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);

/// <summary> Gets the webhook in this guild with the provided id, or null if not found. </summary>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary> Gets a collection of all webhooks for this guild. </summary>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);

/// <summary> Gets a specific emote from this guild. </summary>
Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null);
/// <summary> Creates a new emote in this guild. </summary>
Expand Down
1 change: 0 additions & 1 deletion src/Discord.Net.Core/Entities/Users/IWebhookUser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Discord
{
//TODO: Add webhook endpoints
public interface IWebhookUser : IGuildUser
{
ulong WebhookId { get; }
Expand Down
34 changes: 34 additions & 0 deletions src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;

namespace Discord
{
public interface IWebhook : IDeletable, ISnowflakeEntity
{
/// <summary> Gets the token of this webhook. </summary>
string Token { get; }

/// <summary> Gets the default name of this webhook. </summary>
string Name { get; }
/// <summary> Gets the id of this webhook's default avatar. </summary>
string AvatarId { get; }
/// <summary> Gets the url to this webhook's default avatar. </summary>
string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128);

/// <summary> Gets the channel for this webhook. </summary>
ITextChannel Channel { get; }
/// <summary> Gets the id of the channel for this webhook. </summary>
ulong ChannelId { get; }

/// <summary> Gets the guild owning this webhook. </summary>
IGuild Guild { get; }
/// <summary> Gets the id of the guild owning this webhook. </summary>
ulong? GuildId { get; }

/// <summary> Gets the user that created this webhook. </summary>
IUser Creator { get; }

/// <summary> Modifies this webhook. </summary>
Task ModifyAsync(Action<WebhookProperties> func, RequestOptions options = null);
}
}
41 changes: 41 additions & 0 deletions src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Discord
{
/// <summary>
/// Modify an <see cref="IWebhook"/> with the specified parameters.
/// </summary>
/// <example>
/// <code language="c#">
/// await webhook.ModifyAsync(x =>
/// {
/// x.Name = "Bob";
/// x.Avatar = new Image("avatar.jpg");
/// });
/// </code>
/// </example>
/// <seealso cref="IWebhook"/>
public class WebhookProperties
{
/// <summary>
/// The default name of the webhook.
/// </summary>
public Optional<string> Name { get; set; }
/// <summary>
/// The default avatar of the webhook.
/// </summary>
public Optional<Image?> Image { get; set; }
/// <summary>
/// The channel for this webhook.
/// </summary>
/// <remarks>
/// This field is not used when authenticated with <see cref="TokenType.Webhook"/>.
/// </remarks>
public Optional<ITextChannel> Channel { get; set; }
/// <summary>
/// The channel id for this webhook.
/// </summary>
/// <remarks>
/// This field is not used when authenticated with <see cref="TokenType.Webhook"/>.
/// </remarks>
public Optional<ulong> ChannelId { get; set; }
}
}
2 changes: 2 additions & 0 deletions src/Discord.Net.Core/IDiscordClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ public interface IDiscordClient : IDisposable

Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null);
Task<IVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null);

Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
}
}
25 changes: 25 additions & 0 deletions src/Discord.Net.Rest/API/Common/Webhook.cs
Original file line number Diff line number Diff line change
@@ -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<string> Name { get; set; }
[JsonProperty("avatar")]
public Optional<string> Avatar { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }

[JsonProperty("user")]
public Optional<User> Creator { get; set; }
}
}
14 changes: 14 additions & 0 deletions src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs
Original file line number Diff line number Diff line change
@@ -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<Image?> Avatar { get; set; }
}
}
16 changes: 16 additions & 0 deletions src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs
Original file line number Diff line number Diff line change
@@ -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<string> Name { get; set; }
[JsonProperty("avatar")]
public Optional<Image?> Avatar { get; set; }
[JsonProperty("channel_id")]
public Optional<ulong> ChannelId { get; set; }
}
}
6 changes: 5 additions & 1 deletion src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -15,6 +15,7 @@ internal class UploadWebhookFileParams
public Optional<bool> IsTTS { get; set; }
public Optional<string> Username { get; set; }
public Optional<string> AvatarUrl { get; set; }
public Optional<Embed[]> Embeds { get; set; }

public UploadWebhookFileParams(Stream file)
{
Expand All @@ -25,6 +26,7 @@ public IReadOnlyDictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>();
d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat"));

if (Content.IsSpecified)
d["content"] = Content.Value;
if (IsTTS.IsSpecified)
Expand All @@ -35,6 +37,8 @@ public IReadOnlyDictionary<string, object> ToDictionary()
d["username"] = Username.Value;
if (AvatarUrl.IsSpecified)
d["avatar_url"] = AvatarUrl.Value;
if (Embeds.IsSpecified)
d["embeds"] = Embeds.Value;
return d;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Discord.Net.Rest/BaseDiscordClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync(Requ
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options)
=> Task.FromResult<IVoiceRegion>(null);

Task<IWebhook> IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options)
=> Task.FromResult<IWebhook>(null);

Task IDiscordClient.StartAsync()
=> Task.Delay(0);
Task IDiscordClient.StopAsync()
Expand Down
8 changes: 8 additions & 0 deletions src/Discord.Net.Rest/ClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ public static async Task<RestGuildUser> GetGuildUserAsync(BaseDiscordClient clie
return null;
}

public static async Task<RestWebhook> 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<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options)
{
var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false);
Expand Down
74 changes: 69 additions & 5 deletions src/Discord.Net.Rest/DiscordRestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ public async Task<Message> CreateMessageAsync(ulong channelId, CreateMessagePara
var ids = new BucketIds(channelId: channelId);
return await SendJsonAsync<Message>("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<Message> 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.");
Expand All @@ -486,8 +486,8 @@ public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessag
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<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
}
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
{
Expand All @@ -503,7 +503,7 @@ public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams arg
var ids = new BucketIds(channelId: channelId);
return await SendMultipartAsync<Message>("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<Message> 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.");
Expand All @@ -522,7 +522,7 @@ public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParam
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<Message>("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)
{
Expand Down Expand Up @@ -1198,6 +1198,70 @@ public async Task<IReadOnlyCollection<VoiceRegion>> GetGuildVoiceRegionsAsync(ul
return await SendAsync<IReadOnlyCollection<VoiceRegion>>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false);
}

//Webhooks
public async Task<Webhook> 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<Webhook>("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options);
}
public async Task<Webhook> GetWebhookAsync(ulong webhookId, RequestOptions options = null)
{
Preconditions.NotEqual(webhookId, 0, nameof(webhookId));
options = RequestOptions.CreateOrClone(options);

try
{
if (AuthTokenType == TokenType.Webhook)
return await SendAsync<Webhook>("GET", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false);
else
return await SendAsync<Webhook>("GET", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
}
public async Task<Webhook> 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<Webhook>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), options: options).ConfigureAwait(false);
else
return await SendJsonAsync<Webhook>("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<IReadOnlyCollection<Webhook>> 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<IReadOnlyCollection<Webhook>>("GET", () => $"guilds/{guildId}/webhooks", ids, options: options).ConfigureAwait(false);
}
public async Task<IReadOnlyCollection<Webhook>> 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<IReadOnlyCollection<Webhook>>("GET", () => $"channels/{channelId}/webhooks", ids, options: options).ConfigureAwait(false);
}

//Helpers
protected void CheckState()
{
Expand Down
6 changes: 6 additions & 0 deletions src/Discord.Net.Rest/DiscordRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOp
/// <inheritdoc />
public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null)
=> ClientHelper.GetVoiceRegionAsync(this, id, options);
/// <inheritdoc />
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetWebhookAsync(this, id, options);

//IDiscordClient
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
Expand Down Expand Up @@ -160,5 +163,8 @@ async Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsyn
=> await GetVoiceRegionsAsync(options).ConfigureAwait(false);
async Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options)
=> await GetVoiceRegionAsync(id, options).ConfigureAwait(false);

async Task<IWebhook> IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options)
=> await GetWebhookAsync(id, options);
}
}
Loading

0 comments on commit 7b2ddd0

Please sign in to comment.