diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 16841e936..1253de0a3 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Discord.Logging; using Discord.Rest; @@ -26,6 +27,12 @@ namespace Discord.Webhook /// 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(string webhookUrl) + : this(webhookUrl, new DiscordRestConfig()) { } + + // regex pattern to match webhook urls + private static Regex WebhookUrlRegex = new Regex(@"^.*discordapp\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) @@ -43,6 +50,21 @@ namespace Discord.Webhook _webhookId = Webhook.Id; } + /// + /// Creates a new Webhook Discord client. + /// + /// The url of the webhook. + /// The configuration options to use for this client. + /// Thrown if the is an invalid format. + /// Thrown if the is null or whitespace. + public DiscordWebhookClient(string webhookUrl, DiscordRestConfig config) : this(config) + { + string token; + ParseWebhookUrl(webhookUrl, out _webhookId, out token); + ApiClient.LoginAsync(TokenType.Webhook, token).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, _webhookId).GetAwaiter().GetResult(); + } + private DiscordWebhookClient(DiscordRestConfig config) { ApiClient = CreateApiClient(config); @@ -94,5 +116,31 @@ namespace Discord.Webhook { ApiClient?.Dispose(); } + + internal static void ParseWebhookUrl(string webhookUrl, out ulong webhookId, out string webhookToken) + { + if (string.IsNullOrWhiteSpace(webhookUrl)) + throw new ArgumentNullException(paramName: nameof(webhookUrl), message: + "The given webhook Url cannot be null or whitespace."); + + // thrown when groups are not populated/valid, or when there is no match + ArgumentException ex(string reason = null) + => new ArgumentException(paramName: nameof(webhookUrl), message: + $"The given webhook Url was not in a valid format. {reason}"); + var match = WebhookUrlRegex.Match(webhookUrl); + if (match != null) + { + // ensure that the first group is a ulong, set the _webhookId + // 0th group is always the entire match, so start at index 1 + if (!(match.Groups[1].Success && ulong.TryParse(match.Groups[1].Value, out webhookId))) + throw ex("The webhook Id could not be parsed."); + + if (!match.Groups[2].Success) + throw ex("The webhook token could not be parsed."); + webhookToken = match.Groups[2].Value; + } + else + throw ex(); + } } } diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 46e37655e..d29a728b0 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs b/test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs new file mode 100644 index 000000000..039525afc --- /dev/null +++ b/test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs @@ -0,0 +1,60 @@ +using Discord.Webhook; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + /// + /// Tests the function. + /// + public class DiscordWebhookClientTests + { + [Theory] + [InlineData("https://discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // ptb, canary, etc will have slightly different urls + [InlineData("https://ptb.discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + [InlineData("https://canary.discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // don't care about https + [InlineData("http://canary.discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // this is the minimum that the regex cares about + [InlineData("discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + public void TestWebhook_Valid(string webhookurl, ulong expectedId, string expectedToken) + { + DiscordWebhookClient.ParseWebhookUrl(webhookurl, out ulong id, out string token); + + Assert.Equal(expectedId, id); + Assert.Equal(expectedToken, token); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void TestWebhook_Null(string webhookurl) + { + Assert.Throws(() => + { + DiscordWebhookClient.ParseWebhookUrl(webhookurl, out ulong id, out string token); + }); + } + + [Theory] + [InlineData("123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // trailing slash + [InlineData("https://discordapp.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK/")] + public void TestWebhook_Invalid(string webhookurl) + { + Assert.Throws(() => + { + DiscordWebhookClient.ParseWebhookUrl(webhookurl, out ulong id, out string token); + }); + } + } +}