diff --git a/Discord.Net.sln b/Discord.Net.sln index 6308b4444..1ba7549c3 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E30 EndProject Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" EndProject +Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,6 +161,18 @@ Global {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64 {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.Build.0 = Debug|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.ActiveCfg = Debug|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.Build.0 = Debug|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.Build.0 = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.ActiveCfg = Release|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -171,5 +185,6 @@ Global {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs index c75729acf..116bc3850 100644 --- a/src/Discord.Net.Core/AssemblyInfo.cs +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -4,5 +4,6 @@ [assembly: InternalsVisibleTo("Discord.Net.Rest")] [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 519f4bf0b..e19197cd6 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -5,5 +5,6 @@ User, Bearer, Bot, + Webhook } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs new file mode 100644 index 000000000..970a30201 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookMessageParams + { + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("tts")] + public Optional IsTTS { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("avatar_url")] + public Optional AvatarUrl { get; set; } + + public CreateWebhookMessageParams(string content) + { + Content = content; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs new file mode 100644 index 000000000..f2c34c015 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -0,0 +1,41 @@ +#pragma warning disable CS1591 +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; + +namespace Discord.API.Rest +{ + internal class UploadWebhookFileParams + { + public Stream File { get; } + + public Optional Filename { get; set; } + public Optional Content { get; set; } + public Optional Nonce { get; set; } + public Optional IsTTS { get; set; } + public Optional Username { get; set; } + public Optional AvatarUrl { get; set; } + + public UploadWebhookFileParams(Stream file) + { + File = file; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) + d["content"] = Content.Value; + if (IsTTS.IsSpecified) + d["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + d["nonce"] = Nonce.Value; + if (Username.IsSpecified) + d["username"] = Username.Value; + if (AvatarUrl.IsSpecified) + d["avatar_url"] = AvatarUrl.Value; + return d; + } + } +} diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index aff0626bf..a4f045ab5 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -2,5 +2,6 @@ [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 9e9ac4611..b9c0e9fda 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -120,7 +120,8 @@ namespace Discord.API AuthTokenType = tokenType; AuthToken = token; - RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); + if (tokenType != TokenType.Webhook) + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); LoginState = LoginState.LoggedIn; } @@ -438,8 +439,8 @@ namespace Discord.API } public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); if (!args.Embed.IsSpecified || args.Embed.Value == null) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); @@ -450,6 +451,22 @@ 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) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + 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); + } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -469,6 +486,27 @@ 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) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (args.Content.GetValueOrDefault(null) == null) + args.Content = ""; + else if (args.Content.IsSpecified) + { + if (args.Content.Value == null) + args.Content = ""; + if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + 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); + } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); diff --git a/src/Discord.Net.Webhook/AssemblyInfo.cs b/src/Discord.Net.Webhook/AssemblyInfo.cs new file mode 100644 index 000000000..c6b5997b4 --- /dev/null +++ b/src/Discord.Net.Webhook/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj new file mode 100644 index 000000000..747586aea --- /dev/null +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -0,0 +1,27 @@ + + + 1.0.0 + rc-dev + rc-$(BuildNumber) + netstandard1.1;netstandard1.3 + Discord.Net.Webhook + RogueException + A core Discord.Net library containing the Webhook client and models. + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + Discord.Webhook + true + + + + + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs new file mode 100644 index 000000000..201ed8f09 --- /dev/null +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -0,0 +1,82 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.IO; +using System.Threading.Tasks; +using System.Linq; +using Discord.Logging; + +namespace Discord.Webhook +{ + public partial class DiscordWebhookClient + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly ulong _webhookId; + internal readonly Logger _restLogger; + + internal API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } + + /// 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) + { + _webhookId = webhookId; + + ApiClient = CreateApiClient(config); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + + _restLogger = LogManager.CreateLogger("Rest"); + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + { + if (info == null) + await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + else + 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, + 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 = username; + await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); + } + +#if NETSTANDARD1_3 + public async Task SendFileAsync(string filePath, string text, bool isTTS = false, + string username = null, string avatarUrl = null, 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); + } +#endif + public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + 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); + } + } +}