From b6f22652a40fba7c6ba81b0f6b21f511898f8ad9 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Wed, 11 Oct 2017 18:03:58 -0400 Subject: [PATCH] Webhook client implementation. --- .../DiscordWebhookClient.cs | 76 +++++++++-------- .../Entities/Webhooks/RestInternalWebhook.cs | 66 +++++++++++++++ .../WebhookClientHelper.cs | 81 +++++++++++++++++++ 3 files changed, 191 insertions(+), 32 deletions(-) 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.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 7cc4c77f5..59cc8f3e7 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,19 +1,19 @@ -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; } @@ -21,15 +21,29 @@ namespace Discord.Webhook /// Creates a new Webhook discord client. public DiscordWebhookClient(IWebhook webhook) - : this(webhook.Id, webhook.Token) { } + : 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); @@ -44,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: 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); + } + } +}