| @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E30 | |||||
| EndProject | EndProject | ||||
| Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" | Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" | ||||
| EndProject | 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 | Global | ||||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| Debug|Any CPU = Debug|Any CPU | 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|x64.Build.0 = Release|x64 | ||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 | {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 | ||||
| {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = 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 | EndGlobalSection | ||||
| GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
| HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
| @@ -171,5 +185,6 @@ Global | |||||
| {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | ||||
| {ABC9F4B9-2452-4725-B522-754E0A02E282} = {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} | {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} | ||||
| {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} | |||||
| EndGlobalSection | EndGlobalSection | ||||
| EndGlobal | EndGlobal | ||||
| @@ -4,5 +4,6 @@ | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Rest")] | [assembly: InternalsVisibleTo("Discord.Net.Rest")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Commands")] | [assembly: InternalsVisibleTo("Discord.Net.Commands")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | [assembly: InternalsVisibleTo("Discord.Net.Tests")] | ||||
| @@ -5,5 +5,6 @@ | |||||
| User, | User, | ||||
| Bearer, | Bearer, | ||||
| Bot, | Bot, | ||||
| Webhook | |||||
| } | } | ||||
| } | } | ||||
| @@ -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<string> Nonce { get; set; } | |||||
| [JsonProperty("tts")] | |||||
| public Optional<bool> IsTTS { get; set; } | |||||
| [JsonProperty("embeds")] | |||||
| public Optional<Embed[]> Embeds { get; set; } | |||||
| [JsonProperty("username")] | |||||
| public Optional<string> Username { get; set; } | |||||
| [JsonProperty("avatar_url")] | |||||
| public Optional<string> AvatarUrl { get; set; } | |||||
| public CreateWebhookMessageParams(string content) | |||||
| { | |||||
| Content = content; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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<string> Filename { get; set; } | |||||
| public Optional<string> Content { get; set; } | |||||
| public Optional<string> Nonce { get; set; } | |||||
| public Optional<bool> IsTTS { get; set; } | |||||
| public Optional<string> Username { get; set; } | |||||
| public Optional<string> AvatarUrl { get; set; } | |||||
| public UploadWebhookFileParams(Stream file) | |||||
| { | |||||
| File = file; | |||||
| } | |||||
| 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) | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -2,5 +2,6 @@ | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Commands")] | [assembly: InternalsVisibleTo("Discord.Net.Commands")] | ||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | [assembly: InternalsVisibleTo("Discord.Net.Tests")] | ||||
| @@ -120,7 +120,8 @@ namespace Discord.API | |||||
| AuthTokenType = tokenType; | AuthTokenType = tokenType; | ||||
| AuthToken = token; | AuthToken = token; | ||||
| RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | |||||
| if (tokenType != TokenType.Webhook) | |||||
| RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); | |||||
| LoginState = LoginState.LoggedIn; | LoginState = LoginState.LoggedIn; | ||||
| } | } | ||||
| @@ -438,8 +439,8 @@ namespace Discord.API | |||||
| } | } | ||||
| public async Task<Message> CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) | public async Task<Message> CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) | ||||
| { | { | ||||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||||
| Preconditions.NotNull(args, nameof(args)); | Preconditions.NotNull(args, nameof(args)); | ||||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||||
| if (!args.Embed.IsSpecified || args.Embed.Value == null) | if (!args.Embed.IsSpecified || args.Embed.Value == null) | ||||
| Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | ||||
| @@ -450,6 +451,22 @@ namespace Discord.API | |||||
| var ids = new BucketIds(channelId: channelId); | var ids = new BucketIds(channelId: channelId); | ||||
| return await SendJsonAsync<Message>("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | 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) | |||||
| { | |||||
| 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<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) | public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) | ||||
| { | { | ||||
| Preconditions.NotNull(args, nameof(args)); | Preconditions.NotNull(args, nameof(args)); | ||||
| @@ -469,6 +486,27 @@ namespace Discord.API | |||||
| var ids = new BucketIds(channelId: channelId); | var ids = new BucketIds(channelId: channelId); | ||||
| return await SendMultipartAsync<Message>("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | 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) | |||||
| { | |||||
| 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) | public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) | ||||
| { | { | ||||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | Preconditions.NotEqual(channelId, 0, nameof(channelId)); | ||||
| @@ -0,0 +1,3 @@ | |||||
| using System.Runtime.CompilerServices; | |||||
| [assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
| @@ -0,0 +1,27 @@ | |||||
| <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <PropertyGroup> | |||||
| <VersionPrefix>1.0.0</VersionPrefix> | |||||
| <VersionSuffix Condition="'$(BuildNumber)' == ''">rc-dev</VersionSuffix> | |||||
| <VersionSuffix Condition="'$(BuildNumber)' != ''">rc-$(BuildNumber)</VersionSuffix> | |||||
| <TargetFrameworks>netstandard1.1;netstandard1.3</TargetFrameworks> | |||||
| <AssemblyName>Discord.Net.Webhook</AssemblyName> | |||||
| <Authors>RogueException</Authors> | |||||
| <Description>A core Discord.Net library containing the Webhook client and models.</Description> | |||||
| <PackageTags>discord;discordapp</PackageTags> | |||||
| <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> | |||||
| <PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl> | |||||
| <RepositoryType>git</RepositoryType> | |||||
| <RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl> | |||||
| <RootNamespace>Discord.Webhook</RootNamespace> | |||||
| <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||||
| <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||||
| </ItemGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> | |||||
| <NoWarn>$(NoWarn);CS1573;CS1591</NoWarn> | |||||
| <WarningsAsErrors>true</WarningsAsErrors> | |||||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | |||||
| </PropertyGroup> | |||||
| </Project> | |||||
| @@ -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<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||||
| internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||||
| private readonly ulong _webhookId; | |||||
| internal readonly Logger _restLogger; | |||||
| internal API.DiscordRestApiClient ApiClient { get; } | |||||
| internal LogManager LogManager { get; } | |||||
| /// <summary> Creates a new Webhook discord client. </summary> | |||||
| public DiscordWebhookClient(ulong webhookId, string webhookToken) | |||||
| : this(webhookId, webhookToken, new DiscordRestConfig()) { } | |||||
| /// <summary> Creates a new Webhook discord client. </summary> | |||||
| 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); | |||||
| } | |||||
| } | |||||
| } | |||||