diff --git a/Discord.Net.sln b/Discord.Net.sln index daf902b96..9bb940d8c 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.0 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -22,7 +22,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -130,6 +136,30 @@ Global {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.Build.0 = Debug|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x86.Build.0 = Debug|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|Any CPU.Build.0 = Release|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x64.ActiveCfg = Release|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x64.Build.0 = Release|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x86.ActiveCfg = Release|Any CPU + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x86.Build.0 = Release|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x64.Build.0 = Debug|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x86.Build.0 = Debug|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|Any CPU.Build.0 = Release|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.ActiveCfg = Release|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -141,6 +171,8 @@ Global {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} + {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..a672330d4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# Instructions for Building Documentation + +The documentation for the Discord.NET library uses [DocFX][docfx-main]. [Instructions for installing this tool can be found here.][docfx-installing] + +1. Navigate to the root of the repository. +2. (Optional) If you intend to target a specific version, ensure that you +have the correct version checked out. +3. Build the library. Run `dotnet build` in the root of this repository. + Ensure that the build passes without errors. +4. Build the docs using `docfx .\docs\docfx.json`. Add the `--serve` parameter +to preview the site locally. Some elements of the page may appear incorrect +when not hosted by a server. + - Remarks: According to the docfx website, this tool does work on Linux under mono. + +[docfx-main]: https://dotnet.github.io/docfx/ +[docfx-installing]: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html diff --git a/docs/docfx.json b/docs/docfx.json index 3c0b0611e..50ae39092 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -67,8 +67,8 @@ "default" ], "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2017" + "_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta" }, "noLangKeyword": false } -} \ No newline at end of file +} diff --git a/samples/01_basic_ping_bot/01_basic_ping_bot.csproj b/samples/01_basic_ping_bot/01_basic_ping_bot.csproj new file mode 100644 index 000000000..5484e3d55 --- /dev/null +++ b/samples/01_basic_ping_bot/01_basic_ping_bot.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp2.0 + + + + + + + diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs new file mode 100644 index 000000000..0fcc52b85 --- /dev/null +++ b/samples/01_basic_ping_bot/Program.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; + +namespace _01_basic_ping_bot +{ + // This is a minimal, barebones example of using Discord.Net + // + // If writing a bot with commands, we recommend using the Discord.Net.Commands + // framework, rather than handling commands yourself, like we do in this sample. + // + // You can find samples of using the command framework: + // - Here, under the 02_commands_framework sample + // - https://github.com/foxbot/DiscordBotBase - a barebones bot template + // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library + class Program + { + private DiscordSocketClient _client; + + // Discord.Net heavily utilizes TAP for async, so we create + // an asynchronous context from the beginning. + static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(); + + _client.Log += LogAsync; + _client.Ready += ReadyAsync; + _client.MessageReceived += MessageReceivedAsync; + + // Tokens should be considered secret data, and never hard-coded. + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await _client.StartAsync(); + + // Block the program until it is closed. + await Task.Delay(-1); + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + return Task.CompletedTask; + } + + // The Ready event indicates that the client has opened a + // connection and it is now safe to access the cache. + private Task ReadyAsync() + { + Console.WriteLine($"{_client.CurrentUser} is connected!"); + + return Task.CompletedTask; + } + + // This is not the recommmended way to write a bot - consider + // reading over the Commands Framework sample. + private async Task MessageReceivedAsync(SocketMessage message) + { + // The bot should never respond to itself. + if (message.Author.Id == _client.CurrentUser.Id) + return; + + if (message.Content == "!ping") + await message.Channel.SendMessageAsync("pong!"); + } + } +} diff --git a/samples/02_commands_framework/02_commands_framework.csproj b/samples/02_commands_framework/02_commands_framework.csproj new file mode 100644 index 000000000..77fdc65e1 --- /dev/null +++ b/samples/02_commands_framework/02_commands_framework.csproj @@ -0,0 +1,17 @@ + + + + Exe + netcoreapp2.0 + + + + + + + + + + + + diff --git a/samples/02_commands_framework/Modules/PublicModule.cs b/samples/02_commands_framework/Modules/PublicModule.cs new file mode 100644 index 000000000..f30dfd73f --- /dev/null +++ b/samples/02_commands_framework/Modules/PublicModule.cs @@ -0,0 +1,63 @@ +using System.IO; +using System.Threading.Tasks; +using Discord; +using Discord.Commands; +using _02_commands_framework.Services; + +namespace _02_commands_framework.Modules +{ + // Modules must be public and inherit from an IModuleBase + public class PublicModule : ModuleBase + { + // Dependency Injection will fill this value in for us + public PictureService PictureService { get; set; } + + [Command("ping")] + [Alias("pong", "hello")] + public Task PingAsync() + => ReplyAsync("pong!"); + + [Command("cat")] + public async Task CatAsync() + { + // Get a stream containing an image of a cat + var stream = await PictureService.GetCatPictureAsync(); + // Streams must be seeked to their beginning before being uploaded! + stream.Seek(0, SeekOrigin.Begin); + await Context.Channel.SendFileAsync(stream, "cat.png"); + } + + // Get info on a user, or the user who invoked the command if one is not specified + [Command("userinfo")] + public async Task UserInfoAsync(IUser user = null) + { + user = user ?? Context.User; + + await ReplyAsync(user.ToString()); + } + + // Ban a user + [Command("ban")] + [RequireContext(ContextType.Guild)] + // make sure the user invoking the command can ban + [RequireUserPermission(GuildPermission.BanMembers)] + // make sure the bot itself can ban + [RequireBotPermission(GuildPermission.BanMembers)] + public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null) + { + await user.Guild.AddBanAsync(user, reason: reason); + await ReplyAsync("ok!"); + } + + // [Remainder] takes the rest of the command's arguments as one argument, rather than splitting every space + [Command("echo")] + public Task EchoAsync([Remainder] string text) + // Insert a ZWSP before the text to prevent triggering other bots! + => ReplyAsync('\u200B' + text); + + // 'params' will parse space-separated elements into a list + [Command("list")] + public Task ListAsync(params string[] objects) + => ReplyAsync("You listed: " + string.Join("; ", objects)); + } +} diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs new file mode 100644 index 000000000..3fed652d3 --- /dev/null +++ b/samples/02_commands_framework/Program.cs @@ -0,0 +1,60 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Discord; +using Discord.WebSocket; +using Discord.Commands; +using _02_commands_framework.Services; + +namespace _02_commands_framework +{ + // This is a minimal example of using Discord.Net's command + // framework - by no means does it show everything the framework + // is capable of. + // + // You can find samples of using the command framework: + // - Here, under the 02_commands_framework sample + // - https://github.com/foxbot/DiscordBotBase - a barebones bot template + // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library + class Program + { + static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + var services = ConfigureServices(); + + var client = services.GetRequiredService(); + + client.Log += LogAsync; + services.GetRequiredService().Log += LogAsync; + + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await client.StartAsync(); + + await services.GetRequiredService().InitializeAsync(); + + await Task.Delay(-1); + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + + return Task.CompletedTask; + } + + private IServiceProvider ConfigureServices() + { + return new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + } + } +} diff --git a/samples/02_commands_framework/Services/CommandHandlingService.cs b/samples/02_commands_framework/Services/CommandHandlingService.cs new file mode 100644 index 000000000..fc253eed3 --- /dev/null +++ b/samples/02_commands_framework/Services/CommandHandlingService.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace _02_commands_framework.Services +{ + public class CommandHandlingService + { + private readonly CommandService _commands; + private readonly DiscordSocketClient _discord; + private readonly IServiceProvider _services; + + public CommandHandlingService(IServiceProvider services) + { + _commands = services.GetRequiredService(); + _discord = services.GetRequiredService(); + _services = services; + + _discord.MessageReceived += MessageReceivedAsync; + } + + public async Task InitializeAsync() + { + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public async Task MessageReceivedAsync(SocketMessage rawMessage) + { + // Ignore system messages, or messages from other bots + if (!(rawMessage is SocketUserMessage message)) return; + if (message.Source != MessageSource.User) return; + + // This value holds the offset where the prefix ends + var argPos = 0; + if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return; + + var context = new SocketCommandContext(_discord, message); + var result = await _commands.ExecuteAsync(context, argPos, _services); + + if (result.Error.HasValue && + result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors + await context.Channel.SendMessageAsync(result.ToString()); + } + } +} diff --git a/samples/02_commands_framework/Services/PictureService.cs b/samples/02_commands_framework/Services/PictureService.cs new file mode 100644 index 000000000..dda818cc3 --- /dev/null +++ b/samples/02_commands_framework/Services/PictureService.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace _02_commands_framework.Services +{ + public class PictureService + { + private readonly HttpClient _http; + + public PictureService(HttpClient http) + => _http = http; + + public async Task GetCatPictureAsync() + { + var resp = await _http.GetAsync("https://cataas.com/cat"); + return await resp.Content.ReadAsStreamAsync(); + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index 5f8e9ceaf..a0fcf3e4a 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -7,6 +7,7 @@ namespace Discord.Commands { public string Text { get; } public RunMode RunMode { get; set; } = RunMode.Default; + public bool? IgnoreExtraArgs { get; set; } public CommandAttribute() { diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index b6d002c70..17a170775 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { @@ -22,6 +21,7 @@ namespace Discord.Commands.Builders public string PrimaryAlias { get; set; } public RunMode RunMode { get; set; } public int Priority { get; set; } + public bool IgnoreExtraArgs { get; set; } public IReadOnlyList Preconditions => _preconditions; public IReadOnlyList Parameters => _parameters; @@ -140,4 +140,4 @@ namespace Discord.Commands.Builders return new CommandInfo(this, info, service); } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 1809c2c63..0ada5a9c2 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index e9ce9eb86..cbe02aafb 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -158,6 +158,7 @@ namespace Discord.Commands builder.AddAliases(command.Text); builder.RunMode = command.RunMode; builder.Name = builder.Name ?? command.Text; + builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs; break; case NameAttribute name: builder.Name = name.Text; @@ -291,7 +292,7 @@ namespace Discord.Commands //We dont have a cached type reader, create one reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); - service.AddTypeReader(paramType, reader); + service.AddTypeReader(paramType, reader, false); return reader; } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index d65d99349..64daf841e 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Immutable; using System.Text; using System.Threading.Tasks; @@ -14,7 +14,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos) + public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -110,7 +110,7 @@ namespace Discord.Commands { if (curParam == null) { - if (ignoreExtraArgs) + if (command.IgnoreExtraArgs) break; else return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f4fbcf8b2..c996f7334 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Discord.Commands.Builders; using Discord.Logging; @@ -215,10 +214,11 @@ namespace Discord.Commands return true; } - //Type Readers + //Type Readers /// /// Adds a custom to this for the supplied object type. /// If is a , a will also be added. + /// If a default exists for , a warning will be logged and the default will be replaced. /// /// The object type to be read by the . /// An instance of the to be added. @@ -226,17 +226,54 @@ namespace Discord.Commands => AddTypeReader(typeof(T), reader); /// /// Adds a custom to this for the supplied object type. - /// If is a , a for the value type will also be added. + /// If is a , a for the value type will also be added. + /// If a default exists for , a warning will be logged and the default will be replaced. /// /// A instance for the type to be read. /// An instance of the to be added. public void AddTypeReader(Type type, TypeReader reader) { - var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); - readers[reader.GetType()] = reader; + if (_defaultTypeReaders.ContainsKey(type)) + _ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." + + $"To suppress this message, use AddTypeReader(reader, true)."); + AddTypeReader(type, reader, true); + } + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a will also be added. + /// + /// The object type to be read by the . + /// An instance of the to be added. + /// If should replace the default for if one exists. + public void AddTypeReader(TypeReader reader, bool replaceDefault) + => AddTypeReader(typeof(T), reader, replaceDefault); + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a for the value type will also be added. + /// + /// A instance for the type to be read. + /// An instance of the to be added. + /// If should replace the default for if one exists. + public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) + { + if (replaceDefault && _defaultTypeReaders.ContainsKey(type)) + { + _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); + if (type.GetTypeInfo().IsValueType) + { + var nullableType = typeof(Nullable<>).MakeGenericType(type); + var nullableReader = NullableTypeReader.Create(type, reader); + _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader); + } + } + else + { + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); + readers[reader.GetType()] = reader; - if (type.GetTypeInfo().IsValueType) - AddNullableTypeReader(type, reader); + if (type.GetTypeInfo().IsValueType) + AddNullableTypeReader(type, reader); + } } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 77c5b2262..f5cd14fef 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -20,11 +20,5 @@ namespace Discord.Commands /// Determines whether extra parameters should be ignored. public bool IgnoreExtraArgs { get; set; } = false; - - ///// Gets or sets the to use. - //public IServiceProvider ServiceProvider { get; set; } = null; - - ///// Gets or sets a factory function for the to use. - //public Func ServiceProviderFactory { get; set; } = null; } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index f0d406e8d..1a9cf69c5 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -27,6 +27,7 @@ namespace Discord.Commands public string Remarks { get; } public int Priority { get; } public bool HasVarArgs { get; } + public bool IgnoreExtraArgs { get; } public RunMode RunMode { get; } public IReadOnlyList Aliases { get; } @@ -63,6 +64,7 @@ namespace Discord.Commands Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; + IgnoreExtraArgs = builder.IgnoreExtraArgs; _action = builder.Callback; _commandService = service; @@ -119,7 +121,7 @@ namespace Discord.Commands return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); } public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) @@ -165,11 +167,11 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - return await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); + return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); case RunMode.Async: //Always async var t2 = Task.Run(async () => { - await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); + await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); }); break; } @@ -181,7 +183,7 @@ namespace Discord.Commands } } - private async Task ExecuteAsyncInternalAsync(ICommandContext context, object[] args, IServiceProvider services) + private async Task ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 5a7f9208e..7c144599b 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; -using System.Reflection; using Discord.Commands.Builders; namespace Discord.Commands @@ -23,8 +22,6 @@ namespace Discord.Commands public ModuleInfo Parent { get; } public bool IsSubmodule => Parent != null; - //public TypeInfo TypeInfo { get; } - internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) { Service = service; @@ -35,8 +32,6 @@ namespace Discord.Commands Group = builder.Group; Parent = parent; - //TypeInfo = builder.TypeInfo; - Aliases = BuildAliases(builder, service).ToImmutableArray(); Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray(); diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index 3136eb2cb..cd7a9d744 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Discord.Commands { - internal class ChannelTypeReader : TypeReader + public class ChannelTypeReader : TypeReader where T : class, IChannel { public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index fe576c3c6..a87cfbe43 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Globalization; using System.Threading.Tasks; namespace Discord.Commands { - internal class MessageTypeReader : TypeReader + public class MessageTypeReader : TypeReader where T : class, IMessage { public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index 703374c05..508624103 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Discord.Commands { - internal class RoleTypeReader : TypeReader + public class RoleTypeReader : TypeReader where T : class, IRole { public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 8fc330d4c..425c2ccb7 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Discord.Commands { - internal class UserTypeReader : TypeReader + public class UserTypeReader : TypeReader where T : class, IUser { public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs index 68bc359c6..639ca3ac1 100644 --- a/src/Discord.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; namespace Discord.Commands { @@ -30,6 +31,9 @@ namespace Discord.Commands public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; + public object BestMatch => IsSuccess + ? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value) + : throw new InvalidOperationException("TypeReaderResult was not successful."); private TypeReaderResult(IReadOnlyCollection values, CommandError? error, string errorReason) { diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index f23f55238..6bac241aa 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -13,6 +13,10 @@ namespace Discord string extension = FormatToExtension(format, avatarId); return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } + public static string GetDefaultUserAvatarUrl(ushort discriminator) + { + return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; + } public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; public static string GetGuildSplashUrl(ulong guildId, string splashId) @@ -30,6 +34,8 @@ namespace Discord public static string GetSpotifyAlbumArtUrl(string albumArtId) => $"https://i.scdn.co/image/{albumArtId}"; + public static string GetSpotifyDirectUrl(string trackId) + => $"https://open.spotify.com/track/{trackId}"; private static string FormatToExtension(ImageFormat format, string imageId) { @@ -37,11 +43,16 @@ namespace Discord format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; switch (format) { - case ImageFormat.Gif: return "gif"; - case ImageFormat.Jpeg: return "jpeg"; - case ImageFormat.Png: return "png"; - case ImageFormat.WebP: return "webp"; - default: throw new ArgumentException(nameof(format)); + case ImageFormat.Gif: + return "gif"; + case ImageFormat.Jpeg: + return "jpeg"; + case ImageFormat.Png: + return "png"; + case ImageFormat.WebP: + return "webp"; + default: + throw new ArgumentException(nameof(format)); } } } diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index bce577ddd..321803114 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -4,11 +4,12 @@ Discord.Net.Core Discord The core components for the Discord.Net library. - net45;netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3;netstandard2.0 + netstandard1.1;netstandard1.3;netstandard2.0 - \ No newline at end of file + diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index fd2fe92e8..5cdf6a77e 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -1,13 +1,13 @@ -using System.Reflection; +using System.Reflection; namespace Discord { public class DiscordConfig { - public const int APIVersion = 6; + public const int APIVersion = 6; public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? - typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? + typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; @@ -20,10 +20,12 @@ namespace Discord public const int MaxMessagesPerBatch = 100; public const int MaxUsersPerBatch = 1000; public const int MaxGuildsPerBatch = 100; + public const int MaxUserReactionsPerBatch = 100; + public const int MaxAuditLogEntriesPerBatch = 100; /// Gets or sets how a request should act in the case of an error, by default. public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; - + /// Gets or sets the minimum log level severity that will be sent to the Log event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index a20384242..8b966d68b 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -7,17 +7,20 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SpotifyGame : Game { - public IEnumerable Artists { get; internal set; } - public string AlbumArt { get; internal set; } + public IReadOnlyCollection Artists { get; internal set; } public string AlbumTitle { get; internal set; } public string TrackTitle { get; internal set; } - public string SyncId { get; internal set; } - public string SessionId { get; internal set; } public TimeSpan? Duration { get; internal set; } + public string TrackId { get; internal set; } + public string SessionId { get; internal set; } + + public string AlbumArtUrl { get; internal set; } + public string TrackUrl { get; internal set; } + internal SpotifyGame() { } - public override string ToString() => Name; + public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})"; private string DebuggerDisplay => $"{Name} (Spotify)"; } } diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs new file mode 100644 index 000000000..e5a4ff30a --- /dev/null +++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// The action type within a + /// + public enum ActionType + { + GuildUpdated = 1, + + ChannelCreated = 10, + ChannelUpdated = 11, + ChannelDeleted = 12, + + OverwriteCreated = 13, + OverwriteUpdated = 14, + OverwriteDeleted = 15, + + Kick = 20, + Prune = 21, + Ban = 22, + Unban = 23, + + MemberUpdated = 24, + MemberRoleUpdated = 25, + + RoleCreated = 30, + RoleUpdated = 31, + RoleDeleted = 32, + + InviteCreated = 40, + InviteUpdated = 41, + InviteDeleted = 42, + + WebhookCreated = 50, + WebhookUpdated = 51, + WebhookDeleted = 52, + + EmojiCreated = 60, + EmojiUpdated = 61, + EmojiDeleted = 62, + + MessageDeleted = 72 + } +} diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs new file mode 100644 index 000000000..47aaffb26 --- /dev/null +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents data applied to an + /// + public interface IAuditLogData + { } +} diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs new file mode 100644 index 000000000..b85730a1d --- /dev/null +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents an entry in an audit log + /// + public interface IAuditLogEntry : IEntity + { + /// + /// The action which occured to create this entry + /// + ActionType Action { get; } + + /// + /// The data for this entry. May be if no data was available. + /// + IAuditLogData Data { get; } + + /// + /// The user responsible for causing the changes + /// + IUser User { get; } + + /// + /// The reason behind the change. May be if no reason was provided. + /// + string Reason { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 75e4e0b35..262b865ea 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -8,7 +8,7 @@ namespace Discord public interface IMessageChannel : IChannel { /// Sends a message to this message channel. - Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// Sends a file to this text channel, with an optional caption. Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 2f0599d76..4c1c274d0 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -66,6 +66,24 @@ namespace Discord /// Gets a collection of all users banned on this guild. Task> GetBansAsync(RequestOptions options = null); + /// + /// Gets a ban object for a banned user. + /// + /// The banned user. + /// + /// An awaitable containing the ban object, which contains the user information and the + /// reason for the ban; if the ban entry cannot be found. + /// + Task GetBanAsync(IUser user, RequestOptions options = null); + /// + /// Gets a ban object for a banned user. + /// + /// The snowflake identifier for the banned user. + /// + /// An awaitable containing the ban object, which contains the user information and the + /// reason for the ban; if the ban entry cannot be found. + /// + Task GetBanAsync(ulong userId, RequestOptions options = null); /// Bans the provided user from this guild and optionally prunes their recent messages. /// The number of days to remove messages from this user for - must be between [0, 7] Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); @@ -91,9 +109,9 @@ namespace Discord Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Creates a new text channel. - Task CreateTextChannelAsync(string name, RequestOptions options = null); + Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null); /// Creates a new voice channel. - Task CreateVoiceChannelAsync(string name, RequestOptions options = null); + Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null); /// Creates a new channel category. Task CreateCategoryAsync(string name, RequestOptions options = null); @@ -121,6 +139,10 @@ namespace Discord /// 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. Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + /// Gets the specified number of audit log entries for this guild. + Task> GetAuditLogAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Gets the webhook in this guild with the provided id, or null if not found. Task GetWebhookAsync(ulong id, RequestOptions options = null); /// Gets a collection of all webhooks for this guild. @@ -135,4 +157,4 @@ namespace Discord /// Deletes an existing emote from this guild. Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs index 1a76287d8..67bedbc3d 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs @@ -1,4 +1,4 @@ -namespace Discord +namespace Discord { public interface IVoiceRegion { @@ -10,9 +10,9 @@ bool IsVip { get; } /// Returns true if this voice region is the closest to your machine. bool IsOptimal { get; } - /// Gets an example hostname for this voice region. - string SampleHostname { get; } - /// Gets an example port for this voice region. - int SamplePort { get; } + /// Returns true if this is a deprecated voice region (avoid switching to these). + bool IsDeprecated { get; } + /// Returns true if this is a custom voice region (used for events/etc) + bool IsCustom { get; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index 73555e453..0ebb65679 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { @@ -22,8 +22,9 @@ namespace Discord ulong GuildId { get; } /// Gets the name of the guild this invite is linked to. string GuildName { get; } - - /// Accepts this invite and joins the target guild. This will fail on bot accounts. - Task AcceptAsync(RequestOptions options = null); + /// Gets the approximated count of online members in the guild. + int? PresenceCount { get; } + /// Gets the approximated count of total members in the guild. + int? MemberCount { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index 5fae7acde..dc62066eb 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; @@ -57,7 +57,18 @@ namespace Discord Fields = fields; } - public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0; + public int Length + { + get + { + int titleLength = Title?.Length ?? 0; + int authorLength = Author?.Name?.Length ?? 0; + int descriptionLength = Description?.Length ?? 0; + int footerLength = Footer?.Text?.Length ?? 0; + int fieldSum = Fields.Sum(f => f.Name?.Length + f.Value?.ToString().Length) ?? 0; + return titleLength + authorLength + descriptionLength + footerLength + fieldSum; + } + } public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index f5663cea3..04f4f6884 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -1,89 +1,105 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Discord { public class EmbedBuilder { - private readonly Embed _embed; + private string _title; + private string _description; + private string _url; + private EmbedImage? _image; + private EmbedThumbnail? _thumbnail; + private List _fields; public const int MaxFieldCount = 25; public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 2048; - public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. + public const int MaxEmbedLength = 6000; public EmbedBuilder() { - _embed = new Embed(EmbedType.Rich); Fields = new List(); } public string Title { - get => _embed.Title; + get => _title; set { if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); - _embed.Title = value; + _title = value; } } - public string Description { - get => _embed.Description; + get => _description; set { if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); - _embed.Description = value; + _description = value; } } public string Url { - get => _embed.Url; + get => _url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); - _embed.Url = value; + _url = value; } } public string ThumbnailUrl { - get => _embed.Thumbnail?.Url; + get => _thumbnail?.Url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); - _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); + _thumbnail = new EmbedThumbnail(value, null, null, null); } } public string ImageUrl { - get => _embed.Image?.Url; + get => _image?.Url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); - _embed.Image = new EmbedImage(value, null, null, null); + _image = new EmbedImage(value, null, null, null); } } - public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } - public Color? Color { get => _embed.Color; set { _embed.Color = value; } } - - public EmbedAuthorBuilder Author { get; set; } - public EmbedFooterBuilder Footer { get; set; } - private List _fields; public List Fields { get => _fields; set { - if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); _fields = value; } } + public DateTimeOffset? Timestamp { get; set; } + public Color? Color { get; set; } + public EmbedAuthorBuilder Author { get; set; } + public EmbedFooterBuilder Footer { get; set; } + + public int Length + { + get + { + int titleLength = Title?.Length ?? 0; + int authorLength = Author?.Name?.Length ?? 0; + int descriptionLength = Description?.Length ?? 0; + int footerLength = Footer?.Text?.Length ?? 0; + int fieldSum = Fields.Sum(f => f.Name.Length + f.Value.ToString().Length); + + return titleLength + authorLength + descriptionLength + footerLength + fieldSum; + } + } + public EmbedBuilder WithTitle(string title) { Title = title; @@ -180,7 +196,6 @@ namespace Discord AddField(field); return this; } - public EmbedBuilder AddField(EmbedFieldBuilder field) { if (Fields.Count >= MaxFieldCount) @@ -195,63 +210,53 @@ namespace Discord { var field = new EmbedFieldBuilder(); action(field); - this.AddField(field); + AddField(field); return this; } public Embed Build() { - _embed.Footer = Footer?.Build(); - _embed.Author = Author?.Build(); + if (Length > MaxEmbedLength) + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); + var fields = ImmutableArray.CreateBuilder(Fields.Count); for (int i = 0; i < Fields.Count; i++) fields.Add(Fields[i].Build()); - _embed.Fields = fields.ToImmutable(); - if (_embed.Length > MaxEmbedLength) - { - throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); - } - - return _embed; + return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); } } public class EmbedFieldBuilder { - private EmbedField _field; - + private string _name; + private string _value; public const int MaxFieldNameLength = 256; public const int MaxFieldValueLength = 1024; public string Name { - get => _field.Name; + get => _name; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); - _field.Name = value; + _name = value; } } public object Value { - get => _field.Value; + get => _value; set { var stringValue = value?.ToString(); if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); - _field.Value = stringValue; + _value = stringValue; } } - public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } - - public EmbedFieldBuilder() - { - _field = new EmbedField(); - } + public bool IsInline { get; set; } public EmbedFieldBuilder WithName(string name) { @@ -270,48 +275,44 @@ namespace Discord } public EmbedField Build() - => _field; + => new EmbedField(Name, Value.ToString(), IsInline); } public class EmbedAuthorBuilder { - private EmbedAuthor _author; - + private string _name; + private string _url; + private string _iconUrl; public const int MaxAuthorNameLength = 256; public string Name { - get => _author.Name; + get => _name; set { if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); - _author.Name = value; + _name = value; } } public string Url { - get => _author.Url; + get => _url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); - _author.Url = value; + _url = value; } } public string IconUrl { - get => _author.IconUrl; + get => _iconUrl; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); - _author.IconUrl = value; + _iconUrl = value; } } - public EmbedAuthorBuilder() - { - _author = new EmbedAuthor(); - } - public EmbedAuthorBuilder WithName(string name) { Name = name; @@ -329,39 +330,35 @@ namespace Discord } public EmbedAuthor Build() - => _author; + => new EmbedAuthor(Name, Url, IconUrl, null); } public class EmbedFooterBuilder { - private EmbedFooter _footer; + private string _text; + private string _iconUrl; public const int MaxFooterTextLength = 2048; public string Text { - get => _footer.Text; + get => _text; set { if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); - _footer.Text = value; + _text = value; } } public string IconUrl { - get => _footer.IconUrl; + get => _iconUrl; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); - _footer.IconUrl = value; + _iconUrl = value; } } - public EmbedFooterBuilder() - { - _footer = new EmbedFooter(); - } - public EmbedFooterBuilder WithText(string text) { Text = text; @@ -374,6 +371,6 @@ namespace Discord } public EmbedFooter Build() - => _footer; + => new EmbedFooter(Text, IconUrl, null); } } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 52df187f8..36ee725ff 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -23,7 +23,7 @@ namespace Discord /// Removes all reactions from this message. Task RemoveAllReactionsAsync(RequestOptions options = null); /// Gets all users that reacted to a message with a given emote - Task> GetReactionUsersAsync(IEmote emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); + IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); /// Transforms this message's text into a human readable form by resolving its tags. string Resolve( diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 1a8aad53c..038b18e2e 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; @@ -12,7 +12,9 @@ namespace Discord /// Gets a ChannelPermissions that grants all permissions for text channels. public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); /// Gets a ChannelPermissions that grants all permissions for voice channels. - public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); + public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000010000_010001); + /// Gets a ChannelPermissions that grants all permissions for category channels. + public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); /// Gets a ChannelPermissions that grants all permissions for direct message channels. public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); /// Gets a ChannelPermissions that grants all permissions for group channels. @@ -24,6 +26,7 @@ namespace Discord { case ITextChannel _: return Text; case IVoiceChannel _: return Voice; + case ICategoryChannel _: return Category; case IDMChannel _: return DM; case IGroupChannel _: return Group; default: throw new ArgumentException("Unknown channel type", nameof(channel)); @@ -157,4 +160,4 @@ namespace Discord public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 89e76df6d..0bb04d339 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -1,5 +1,8 @@ using System; using System.Diagnostics; +#if NETSTANDARD2_0 || NET45 +using StandardColor = System.Drawing.Color; +#endif namespace Discord { @@ -96,7 +99,14 @@ namespace Discord ((uint)(g * 255.0f) << 8) | (uint)(b * 255.0f); } - + +#if NETSTANDARD2_0 || NET45 + public static implicit operator StandardColor(Color color) => + StandardColor.FromArgb((int)color.RawValue); + public static explicit operator Color(StandardColor color) => + new Color((uint)color.ToArgb() << 8 >> 8); +#endif + public override string ToString() => $"#{Convert.ToString(RawValue, 16)}"; private string DebuggerDisplay => diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index e3f270f6f..c5cce7a25 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -8,6 +8,8 @@ namespace Discord string AvatarId { get; } /// Gets the url to this user's avatar. string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + /// Gets the url to this user's default avatar. + string GetDefaultAvatarUrl(); /// Gets the per-username unique id for this user. string Discriminator { get; } /// Gets the per-username unique id for this user. diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookType.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookType.cs new file mode 100644 index 000000000..0ddfa393d --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookType.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + /// + /// Represents the type of a webhook. + /// + /// + /// This type is currently unused, and is only returned in audit log responses. + /// + public enum WebhookType + { + /// An incoming webhook + Incoming = 1 + } +} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index d3e968e39..3296d00fd 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -9,7 +9,7 @@ namespace Discord /// Sends a message to the user via DM. /// public static async Task SendMessageAsync(this IUser user, - string text, + string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index a383c37da..083c0b512 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -27,7 +27,7 @@ namespace Discord Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null); - Task GetInviteAsync(string inviteId, RequestOptions options = null); + Task GetInviteAsync(string inviteId, bool withCount = false, RequestOptions options = null); Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetUserAsync(string username, string discriminator, RequestOptions options = null); diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj index 43d627c99..8c56a5fb4 100644 --- a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -4,9 +4,10 @@ Discord.Net.DebugTools Discord A Discord.Net extension adding some helper classes for diagnosing issues. - net45;netstandard1.3 + net45;netstandard1.3 + netstandard1.3 - \ No newline at end of file + diff --git a/src/Discord.Net.Rest/API/Common/AuditLog.cs b/src/Discord.Net.Rest/API/Common/AuditLog.cs new file mode 100644 index 000000000..cd8ad147d --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AuditLog.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AuditLog + { + [JsonProperty("webhooks")] + public Webhook[] Webhooks { get; set; } + + [JsonProperty("users")] + public User[] Users { get; set; } + + [JsonProperty("audit_log_entries")] + public AuditLogEntry[] Entries { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/AuditLogChange.cs b/src/Discord.Net.Rest/API/Common/AuditLogChange.cs new file mode 100644 index 000000000..44e585021 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AuditLogChange.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Discord.API +{ + internal class AuditLogChange + { + [JsonProperty("key")] + public string ChangedProperty { get; set; } + + [JsonProperty("new_value")] + public JToken NewValue { get; set; } + + [JsonProperty("old_value")] + public JToken OldValue { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs new file mode 100644 index 000000000..80d9a9e97 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AuditLogEntry + { + [JsonProperty("target_id")] + public ulong? TargetId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + + [JsonProperty("changes")] + public AuditLogChange[] Changes { get; set; } + [JsonProperty("options")] + public AuditLogOptions Options { get; set; } + + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("action_type")] + public ActionType Action { get; set; } + + [JsonProperty("reason")] + public string Reason { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs new file mode 100644 index 000000000..65b401cce --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AuditLogOptions + { + //Message delete + [JsonProperty("count")] + public int? MessageDeleteCount { get; set; } + [JsonProperty("channel_id")] + public ulong? MessageDeleteChannelId { get; set; } + + //Prune + [JsonProperty("delete_member_days")] + public int? PruneDeleteMemberDays { get; set; } + [JsonProperty("members_removed")] + public int? PruneMembersRemoved { get; set; } + + //Overwrite Update + [JsonProperty("role_name")] + public string OverwriteRoleName { get; set; } + [JsonProperty("type")] + public string OverwriteType { get; set; } + [JsonProperty("id")] + public ulong? OverwriteTargetId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Invite.cs b/src/Discord.Net.Rest/API/Common/Invite.cs index 67a318c5a..1b35da870 100644 --- a/src/Discord.Net.Rest/API/Common/Invite.cs +++ b/src/Discord.Net.Rest/API/Common/Invite.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -11,5 +11,9 @@ namespace Discord.API public InviteGuild Guild { get; set; } [JsonProperty("channel")] public InviteChannel Channel { get; set; } + [JsonProperty("approximate_presence_count")] + public Optional PresenceCount { get; set; } + [JsonProperty("approximate_member_count")] + public Optional MemberCount { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/VoiceRegion.cs b/src/Discord.Net.Rest/API/Common/VoiceRegion.cs index 5f31e8f64..606af07bd 100644 --- a/src/Discord.Net.Rest/API/Common/VoiceRegion.cs +++ b/src/Discord.Net.Rest/API/Common/VoiceRegion.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -13,9 +13,9 @@ namespace Discord.API public bool IsVip { get; set; } [JsonProperty("optimal")] public bool IsOptimal { get; set; } - [JsonProperty("sample_hostname")] - public string SampleHostname { get; set; } - [JsonProperty("sample_port")] - public int SamplePort { get; set; } + [JsonProperty("deprecated")] + public bool IsDeprecated { get; set; } + [JsonProperty("custom")] + public bool IsCustom { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index bae677148..05cdf4b8a 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -10,9 +10,20 @@ namespace Discord.API.Rest public string Name { get; } [JsonProperty("type")] public ChannelType Type { get; } + [JsonProperty("parent_id")] + public Optional CategoryId { get; set; } + //Text channels + [JsonProperty("topic")] + public Optional Topic { get; set; } + [JsonProperty("nsfw")] + public Optional IsNsfw { get; set; } + + //Voice channels [JsonProperty("bitrate")] public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } public CreateGuildChannelParams(string name, ChannelType type) { diff --git a/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs b/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs new file mode 100644 index 000000000..ceabccbc8 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs @@ -0,0 +1,8 @@ +namespace Discord.API.Rest +{ + class GetAuditLogsParams + { + public Optional Limit { get; set; } + public Optional BeforeEntryId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/GetInviteParams.cs b/src/Discord.Net.Rest/API/Rest/GetInviteParams.cs new file mode 100644 index 000000000..cb8d8f7fe --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetInviteParams.cs @@ -0,0 +1,7 @@ +namespace Discord.API.Rest +{ + internal class GetInviteParams + { + public Optional WithCounts { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs b/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs index d70da5632..a0967bb75 100644 --- a/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Rest +namespace Discord.API.Rest { internal class GetReactionUsersParams { diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index 126365e47..9c90919af 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -6,5 +6,17 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: TypeForwardedTo(typeof(Discord.Embed))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthor))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthorBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedField))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedFieldBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedFooter))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedFooterBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedImage))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedProvider))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedThumbnail))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedType))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedVideo))] diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index f8642b96c..db8e2e691 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -148,7 +148,7 @@ namespace Discord.Rest Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + Task IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options) => Task.FromResult(null); Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 08305f857..a1f8ece69 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -50,12 +50,16 @@ namespace Discord.Rest return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); } - public static async Task GetInviteAsync(BaseDiscordClient client, - string inviteId, RequestOptions options) + public static async Task GetInviteAsync(BaseDiscordClient client, + string inviteId, bool withCount, RequestOptions options) { - var model = await client.ApiClient.GetInviteAsync(inviteId, options).ConfigureAwait(false); + var args = new GetInviteParams + { + WithCounts = withCount + }; + var model = await client.ApiClient.GetInviteAsync(inviteId, args, options).ConfigureAwait(false); if (model != null) - return RestInvite.Create(client, null, null, model); + return RestInviteMetadata.Create(client, null, null, model); return null; } diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 29f79e410..0eb07a4b2 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -4,7 +4,8 @@ Discord.Net.Rest Discord.Rest A core Discord.Net library containing the REST client and models. - net45;netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3 + netstandard1.1;netstandard1.3 @@ -16,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index f0c4358ad..85e04f962 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -618,15 +618,15 @@ namespace Discord.API Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); Preconditions.NotNull(args, nameof(args)); Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); - Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUserReactionsPerBatch, nameof(args.Limit)); Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); options = RequestOptions.CreateOrClone(options); - int limit = args.Limit.GetValueOrDefault(int.MaxValue); + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUserReactionsPerBatch); ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); var ids = new BucketIds(channelId: channelId); - Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}"; + Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) @@ -800,6 +800,15 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false); } + public async Task GetGuildBanAsync(ulong guildId, ulong userId, RequestOptions options) + { + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + } public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -897,7 +906,7 @@ namespace Discord.API } //Guild Invites - public async Task GetInviteAsync(string inviteId, RequestOptions options = null) + public async Task GetInviteAsync(string inviteId, GetInviteParams args, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); options = RequestOptions.CreateOrClone(options); @@ -910,9 +919,11 @@ namespace Discord.API if (index >= 0) inviteId = inviteId.Substring(index + 1); + var withCounts = args.WithCounts.GetValueOrDefault(false); + try { - return await SendAsync("GET", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"invites/{inviteId}?with_counts={withCounts}", new BucketIds(), options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } @@ -950,13 +961,6 @@ namespace Discord.API return await SendAsync("DELETE", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task AcceptInviteAsync(string inviteId, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); - options = RequestOptions.CreateOrClone(options); - - await SendAsync("POST", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); - } //Guild Members public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) @@ -1202,6 +1206,26 @@ namespace Discord.API return await SendAsync>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); } + //Audit logs + public async Task GetAuditLogsAsync(ulong guildId, GetAuditLogsParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + + var ids = new BucketIds(guildId: guildId); + Expression> endpoint; + + if (args.BeforeEntryId.IsSpecified) + endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}&before={args.BeforeEntryId.Value}"; + else + endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}"; + + return await SendAsync("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + //Webhooks public async Task CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 8850da3a5..3a596624d 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -56,8 +56,8 @@ namespace Discord.Rest => ClientHelper.GetConnectionsAsync(this, options); /// - public Task GetInviteAsync(string inviteId, RequestOptions options = null) - => ClientHelper.GetInviteAsync(this, inviteId, options); + public Task GetInviteAsync(string inviteId, bool withCount = false, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, withCount, options); /// public Task GetGuildAsync(ulong id, RequestOptions options = null) @@ -131,8 +131,8 @@ namespace Discord.Rest async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync(options).ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) - => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options) + => await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false); async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs new file mode 100644 index 000000000..7936343f3 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + internal static class AuditLogHelper + { + private static readonly Dictionary> CreateMapping + = new Dictionary>() + { + [ActionType.GuildUpdated] = GuildUpdateAuditLogData.Create, + + [ActionType.ChannelCreated] = ChannelCreateAuditLogData.Create, + [ActionType.ChannelUpdated] = ChannelUpdateAuditLogData.Create, + [ActionType.ChannelDeleted] = ChannelDeleteAuditLogData.Create, + + [ActionType.OverwriteCreated] = OverwriteCreateAuditLogData.Create, + [ActionType.OverwriteUpdated] = OverwriteUpdateAuditLogData.Create, + [ActionType.OverwriteDeleted] = OverwriteDeleteAuditLogData.Create, + + [ActionType.Kick] = KickAuditLogData.Create, + [ActionType.Prune] = PruneAuditLogData.Create, + [ActionType.Ban] = BanAuditLogData.Create, + [ActionType.Unban] = UnbanAuditLogData.Create, + [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, + [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, + + [ActionType.RoleCreated] = RoleCreateAuditLogData.Create, + [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, + [ActionType.RoleDeleted] = RoleDeleteAuditLogData.Create, + + [ActionType.InviteCreated] = InviteCreateAuditLogData.Create, + [ActionType.InviteUpdated] = InviteUpdateAuditLogData.Create, + [ActionType.InviteDeleted] = InviteDeleteAuditLogData.Create, + + [ActionType.WebhookCreated] = WebhookCreateAuditLogData.Create, + [ActionType.WebhookUpdated] = WebhookUpdateAuditLogData.Create, + [ActionType.WebhookDeleted] = WebhookDeleteAuditLogData.Create, + + [ActionType.EmojiCreated] = EmoteCreateAuditLogData.Create, + [ActionType.EmojiUpdated] = EmoteUpdateAuditLogData.Create, + [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, + + [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, + }; + + public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) + { + if (CreateMapping.TryGetValue(entry.Action, out var func)) + return func(discord, log, entry); + + return null; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs new file mode 100644 index 000000000..4b9d5875f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -0,0 +1,23 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class BanAuditLogData : IAuditLogData + { + private BanAuditLogData(IUser user) + { + Target = user; + } + + internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new BanAuditLogData(RestUser.Create(discord, userInfo)); + } + + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs new file mode 100644 index 000000000..ef4787295 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class ChannelCreateAuditLogData : IAuditLogData + { + private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites) + { + ChannelId = id; + ChannelName = name; + ChannelType = type; + Overwrites = overwrites; + } + + internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + var overwrites = new List(); + + var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var type = typeModel.NewValue.ToObject(); + var name = nameModel.NewValue.ToObject(); + + foreach (var overwrite in overwritesModel.NewValue) + { + var deny = overwrite.Value("deny"); + var _type = overwrite.Value("type"); + var id = overwrite.Value("id"); + var allow = overwrite.Value("allow"); + + PermissionTarget permType = _type == "member" ? PermissionTarget.User : PermissionTarget.Role; + + overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); + } + + return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, overwrites.ToReadOnlyCollection()); + } + + public ulong ChannelId { get; } + public string ChannelName { get; } + public ChannelType ChannelType { get; } + public IReadOnlyCollection Overwrites { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs new file mode 100644 index 000000000..4816ce770 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class ChannelDeleteAuditLogData : IAuditLogData + { + private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites) + { + ChannelId = id; + ChannelName = name; + ChannelType = type; + Overwrites = overwrites; + } + + internal static ChannelDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var overwrites = overwritesModel.OldValue.ToObject() + .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) + .ToList(); + var type = typeModel.OldValue.ToObject(); + var name = nameModel.OldValue.ToObject(); + var id = entry.TargetId.Value; + + return new ChannelDeleteAuditLogData(id, name, type, overwrites.ToReadOnlyCollection()); + } + + public ulong ChannelId { get; } + public string ChannelName { get; } + public ChannelType ChannelType { get; } + public IReadOnlyCollection Overwrites { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs new file mode 100644 index 000000000..e2d6064a9 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -0,0 +1,18 @@ +namespace Discord.Rest +{ + public struct ChannelInfo + { + internal ChannelInfo(string name, string topic, int? bitrate, int? limit) + { + Name = name; + Topic = topic; + Bitrate = bitrate; + UserLimit = limit; + } + + public string Name { get; } + public string Topic { get; } + public int? Bitrate { get; } + public int? UserLimit { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs new file mode 100644 index 000000000..f3403138d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs @@ -0,0 +1,45 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class ChannelUpdateAuditLogData : IAuditLogData + { + private ChannelUpdateAuditLogData(ulong id, ChannelInfo before, ChannelInfo after) + { + ChannelId = id; + Before = before; + After = after; + } + + internal static ChannelUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var topicModel = changes.FirstOrDefault(x => x.ChangedProperty == "topic"); + var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + var userLimitModel = changes.FirstOrDefault(x => x.ChangedProperty == "user_limit"); + + string oldName = nameModel?.OldValue?.ToObject(), + newName = nameModel?.NewValue?.ToObject(); + string oldTopic = topicModel?.OldValue?.ToObject(), + newTopic = topicModel?.NewValue?.ToObject(); + int? oldBitrate = bitrateModel?.OldValue?.ToObject(), + newBitrate = bitrateModel?.NewValue?.ToObject(); + int? oldLimit = userLimitModel?.OldValue?.ToObject(), + newLimit = userLimitModel?.NewValue?.ToObject(); + + var before = new ChannelInfo(oldName, oldTopic, oldBitrate, oldLimit); + var after = new ChannelInfo(newName, newTopic, newBitrate, newLimit); + + return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after); + } + + public ulong ChannelId { get; } + public ChannelInfo Before { get; set; } + public ChannelInfo After { get; set; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs new file mode 100644 index 000000000..5d1ef8463 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class EmoteCreateAuditLogData : IAuditLogData + { + private EmoteCreateAuditLogData(ulong id, string name) + { + EmoteId = id; + Name = name; + } + + internal static EmoteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var emoteName = change.NewValue?.ToObject(); + return new EmoteCreateAuditLogData(entry.TargetId.Value, emoteName); + } + + public ulong EmoteId { get; } + public string Name { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs new file mode 100644 index 000000000..d0a11191f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs @@ -0,0 +1,28 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class EmoteDeleteAuditLogData : IAuditLogData + { + private EmoteDeleteAuditLogData(ulong id, string name) + { + EmoteId = id; + Name = name; + } + + internal static EmoteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var emoteName = change.OldValue?.ToObject(); + + return new EmoteDeleteAuditLogData(entry.TargetId.Value, emoteName); + } + + public ulong EmoteId { get; } + public string Name { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs new file mode 100644 index 000000000..60020bcaa --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs @@ -0,0 +1,31 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class EmoteUpdateAuditLogData : IAuditLogData + { + private EmoteUpdateAuditLogData(ulong id, string oldName, string newName) + { + EmoteId = id; + OldName = oldName; + NewName = newName; + } + + internal static EmoteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var newName = change.NewValue?.ToObject(); + var oldName = change.OldValue?.ToObject(); + + return new EmoteUpdateAuditLogData(entry.TargetId.Value, oldName, newName); + } + + public ulong EmoteId { get; } + public string NewName { get; } + public string OldName { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs new file mode 100644 index 000000000..90865ef72 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs @@ -0,0 +1,32 @@ +namespace Discord.Rest +{ + public struct GuildInfo + { + internal GuildInfo(int? afkTimeout, DefaultMessageNotifications? defaultNotifs, + ulong? afkChannel, string name, string region, string icon, + VerificationLevel? verification, IUser owner, MfaLevel? mfa, int? filter) + { + AfkTimeout = afkTimeout; + DefaultMessageNotifications = defaultNotifs; + AfkChannelId = afkChannel; + Name = name; + RegionId = region; + IconHash = icon; + VerificationLevel = verification; + Owner = owner; + MfaLevel = mfa; + ContentFilterLevel = filter; + } + + public int? AfkTimeout { get; } + public DefaultMessageNotifications? DefaultMessageNotifications { get; } + public ulong? AfkChannelId { get; } + public string Name { get; } + public string RegionId { get; } + public string IconHash { get; } + public VerificationLevel? VerificationLevel { get; } + public IUser Owner { get; } + public MfaLevel? MfaLevel { get; } + public int? ContentFilterLevel { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs new file mode 100644 index 000000000..08550ed7a --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs @@ -0,0 +1,79 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class GuildUpdateAuditLogData : IAuditLogData + { + private GuildUpdateAuditLogData(GuildInfo before, GuildInfo after) + { + Before = before; + After = after; + } + + internal static GuildUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var afkTimeoutModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var defaultMessageNotificationsModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var afkChannelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var regionIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var iconHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var verificationLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var ownerIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var mfaLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var contentFilterModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + + int? oldAfkTimeout = afkTimeoutModel?.OldValue?.ToObject(), + newAfkTimeout = afkTimeoutModel?.NewValue?.ToObject(); + DefaultMessageNotifications? oldDefaultMessageNotifications = defaultMessageNotificationsModel?.OldValue?.ToObject(), + newDefaultMessageNotifications = defaultMessageNotificationsModel?.NewValue?.ToObject(); + ulong? oldAfkChannelId = afkChannelModel?.OldValue?.ToObject(), + newAfkChannelId = afkChannelModel?.NewValue?.ToObject(); + string oldName = nameModel?.OldValue?.ToObject(), + newName = nameModel?.NewValue?.ToObject(); + string oldRegionId = regionIdModel?.OldValue?.ToObject(), + newRegionId = regionIdModel?.NewValue?.ToObject(); + string oldIconHash = iconHashModel?.OldValue?.ToObject(), + newIconHash = iconHashModel?.NewValue?.ToObject(); + VerificationLevel? oldVerificationLevel = verificationLevelModel?.OldValue?.ToObject(), + newVerificationLevel = verificationLevelModel?.NewValue?.ToObject(); + ulong? oldOwnerId = ownerIdModel?.OldValue?.ToObject(), + newOwnerId = ownerIdModel?.NewValue?.ToObject(); + MfaLevel? oldMfaLevel = mfaLevelModel?.OldValue?.ToObject(), + newMfaLevel = mfaLevelModel?.NewValue?.ToObject(); + int? oldContentFilter = contentFilterModel?.OldValue?.ToObject(), + newContentFilter = contentFilterModel?.NewValue?.ToObject(); + + IUser oldOwner = null; + if (oldOwnerId != null) + { + var oldOwnerInfo = log.Users.FirstOrDefault(x => x.Id == oldOwnerId.Value); + oldOwner = RestUser.Create(discord, oldOwnerInfo); + } + + IUser newOwner = null; + if (newOwnerId != null) + { + var newOwnerInfo = log.Users.FirstOrDefault(x => x.Id == newOwnerId.Value); + newOwner = RestUser.Create(discord, newOwnerInfo); + } + + var before = new GuildInfo(oldAfkTimeout, oldDefaultMessageNotifications, + oldAfkChannelId, oldName, oldRegionId, oldIconHash, oldVerificationLevel, oldOwner, + oldMfaLevel, oldContentFilter); + var after = new GuildInfo(newAfkTimeout, newDefaultMessageNotifications, + newAfkChannelId, newName, newRegionId, newIconHash, newVerificationLevel, newOwner, + newMfaLevel, newContentFilter); + + return new GuildUpdateAuditLogData(before, after); + } + + public GuildInfo Before { get; } + public GuildInfo After { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs new file mode 100644 index 000000000..292715420 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -0,0 +1,55 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class InviteCreateAuditLogData : IAuditLogData + { + private InviteCreateAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) + { + MaxAge = maxAge; + Code = code; + Temporary = temporary; + Creator = inviter; + ChannelId = channelId; + Uses = uses; + MaxUses = maxUses; + } + + internal static InviteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age"); + var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code"); + var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary"); + var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses"); + var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses"); + + var maxAge = maxAgeModel.NewValue.ToObject(); + var code = codeModel.NewValue.ToObject(); + var temporary = temporaryModel.NewValue.ToObject(); + var inviterId = inviterIdModel.NewValue.ToObject(); + var channelId = channelIdModel.NewValue.ToObject(); + var uses = usesModel.NewValue.ToObject(); + var maxUses = maxUsesModel.NewValue.ToObject(); + + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + var inviter = RestUser.Create(discord, inviterInfo); + + return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); + } + + public int MaxAge { get; } + public string Code { get; } + public bool Temporary { get; } + public IUser Creator { get; } + public ulong ChannelId { get; } + public int Uses { get; } + public int MaxUses { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs new file mode 100644 index 000000000..1dc6d518b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -0,0 +1,55 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class InviteDeleteAuditLogData : IAuditLogData + { + private InviteDeleteAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) + { + MaxAge = maxAge; + Code = code; + Temporary = temporary; + Creator = inviter; + ChannelId = channelId; + Uses = uses; + MaxUses = maxUses; + } + + internal static InviteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age"); + var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code"); + var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary"); + var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses"); + var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses"); + + var maxAge = maxAgeModel.OldValue.ToObject(); + var code = codeModel.OldValue.ToObject(); + var temporary = temporaryModel.OldValue.ToObject(); + var inviterId = inviterIdModel.OldValue.ToObject(); + var channelId = channelIdModel.OldValue.ToObject(); + var uses = usesModel.OldValue.ToObject(); + var maxUses = maxUsesModel.OldValue.ToObject(); + + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + var inviter = RestUser.Create(discord, inviterInfo); + + return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); + } + + public int MaxAge { get; } + public string Code { get; } + public bool Temporary { get; } + public IUser Creator { get; } + public ulong ChannelId { get; } + public int Uses { get; } + public int MaxUses { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs new file mode 100644 index 000000000..c9840f6cc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs @@ -0,0 +1,20 @@ +namespace Discord.Rest +{ + public struct InviteInfo + { + internal InviteInfo(int? maxAge, string code, bool? temporary, ulong? channelId, int? maxUses) + { + MaxAge = maxAge; + Code = code; + Temporary = temporary; + ChannelId = channelId; + MaxUses = maxUses; + } + + public int? MaxAge { get; } + public string Code { get; } + public bool? Temporary { get; } + public ulong? ChannelId { get; } + public int? MaxUses { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs new file mode 100644 index 000000000..b932cfbfc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs @@ -0,0 +1,46 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class InviteUpdateAuditLogData : IAuditLogData + { + private InviteUpdateAuditLogData(InviteInfo before, InviteInfo after) + { + Before = before; + After = after; + } + + internal static InviteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age"); + var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code"); + var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses"); + + int? oldMaxAge = maxAgeModel?.OldValue?.ToObject(), + newMaxAge = maxAgeModel?.NewValue?.ToObject(); + string oldCode = codeModel?.OldValue?.ToObject(), + newCode = codeModel?.NewValue?.ToObject(); + bool? oldTemporary = temporaryModel?.OldValue?.ToObject(), + newTemporary = temporaryModel?.NewValue?.ToObject(); + ulong? oldChannelId = channelIdModel?.OldValue?.ToObject(), + newChannelId = channelIdModel?.NewValue?.ToObject(); + int? oldMaxUses = maxUsesModel?.OldValue?.ToObject(), + newMaxUses = maxUsesModel?.NewValue?.ToObject(); + + var before = new InviteInfo(oldMaxAge, oldCode, oldTemporary, oldChannelId, oldMaxUses); + var after = new InviteInfo(newMaxAge, newCode, newTemporary, newChannelId, newMaxUses); + + return new InviteUpdateAuditLogData(before, after); + } + + public InviteInfo Before { get; } + public InviteInfo After { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs new file mode 100644 index 000000000..41b5526b8 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs @@ -0,0 +1,23 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class KickAuditLogData : IAuditLogData + { + private KickAuditLogData(RestUser user) + { + Target = user; + } + + internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new KickAuditLogData(RestUser.Create(discord, userInfo)); + } + + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs new file mode 100644 index 000000000..b0f0a1fe1 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class MemberRoleAuditLogData : IAuditLogData + { + private MemberRoleAuditLogData(IReadOnlyCollection roles, IUser target) + { + Roles = roles; + Target = target; + } + + internal static MemberRoleAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var roleInfos = changes.SelectMany(x => x.NewValue.ToObject(), + (model, role) => new { model.ChangedProperty, Role = role }) + .Select(x => new RoleInfo(x.Role.Name, x.Role.Id, x.ChangedProperty == "$add")) + .ToList(); + + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + var user = RestUser.Create(discord, userInfo); + + return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user); + } + + public IReadOnlyCollection Roles { get; } + public IUser Target { get; } + + public struct RoleInfo + { + internal RoleInfo(string name, ulong roleId, bool added) + { + Name = name; + RoleId = roleId; + Added = added; + } + + public string Name { get; } + public ulong RoleId { get; } + public bool Added { get; } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs new file mode 100644 index 000000000..38f078848 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -0,0 +1,35 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; +using ChangeModel = Discord.API.AuditLogChange; + +namespace Discord.Rest +{ + public class MemberUpdateAuditLogData : IAuditLogData + { + private MemberUpdateAuditLogData(IUser target, string newNick, string oldNick) + { + Target = target; + NewNick = newNick; + OldNick = oldNick; + } + + internal static MemberUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "nick"); + + var newNick = changes.NewValue?.ToObject(); + var oldNick = changes.OldValue?.ToObject(); + + var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + var user = RestUser.Create(discord, targetInfo); + + return new MemberUpdateAuditLogData(user, newNick, oldNick); + } + + public IUser Target { get; } + public string NewNick { get; } + public string OldNick { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs new file mode 100644 index 000000000..3949cdd68 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -0,0 +1,22 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class MessageDeleteAuditLogData : IAuditLogData + { + private MessageDeleteAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MessageCount = count; + } + + internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value); + } + + public int MessageCount { get; } + public ulong ChannelId { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs new file mode 100644 index 000000000..d58488136 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs @@ -0,0 +1,37 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class OverwriteCreateAuditLogData : IAuditLogData + { + private OverwriteCreateAuditLogData(Overwrite overwrite) + { + Overwrite = overwrite; + } + + internal static OverwriteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); + var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); + + var deny = denyModel.NewValue.ToObject(); + var allow = allowModel.NewValue.ToObject(); + + var permissions = new OverwritePermissions(allow, deny); + + var id = entry.Options.OverwriteTargetId.Value; + var type = entry.Options.OverwriteType; + + PermissionTarget target = type == "member" ? PermissionTarget.User : PermissionTarget.Role; + + return new OverwriteCreateAuditLogData(new Overwrite(id, target, permissions)); + } + + public Overwrite Overwrite { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs new file mode 100644 index 000000000..445c2e302 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; +using ChangeModel = Discord.API.AuditLogChange; +using OptionModel = Discord.API.AuditLogOptions; + +namespace Discord.Rest +{ + public class OverwriteDeleteAuditLogData : IAuditLogData + { + private OverwriteDeleteAuditLogData(Overwrite deletedOverwrite) + { + Overwrite = deletedOverwrite; + } + + internal static OverwriteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id"); + var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); + + var deny = denyModel.OldValue.ToObject(); + var type = typeModel.OldValue.ToObject(); + var id = idModel.OldValue.ToObject(); + var allow = allowModel.OldValue.ToObject(); + + PermissionTarget target = type == "member" ? PermissionTarget.User : PermissionTarget.Role; + + return new OverwriteDeleteAuditLogData(new Overwrite(id, target, new OverwritePermissions(allow, deny))); + } + + public Overwrite Overwrite { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs new file mode 100644 index 000000000..d000146c3 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs @@ -0,0 +1,44 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class OverwriteUpdateAuditLogData : IAuditLogData + { + private OverwriteUpdateAuditLogData(OverwritePermissions before, OverwritePermissions after, ulong targetId, PermissionTarget targetType) + { + OldPermissions = before; + NewPermissions = after; + OverwriteTargetId = targetId; + OverwriteType = targetType; + } + + internal static OverwriteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); + var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); + + var beforeAllow = allowModel?.OldValue?.ToObject(); + var afterAllow = allowModel?.NewValue?.ToObject(); + var beforeDeny = denyModel?.OldValue?.ToObject(); + var afterDeny = denyModel?.OldValue?.ToObject(); + + var beforePermissions = new OverwritePermissions(beforeAllow ?? 0, beforeDeny ?? 0); + var afterPermissions = new OverwritePermissions(afterAllow ?? 0, afterDeny ?? 0); + + PermissionTarget target = entry.Options.OverwriteType == "member" ? PermissionTarget.User : PermissionTarget.Role; + + return new OverwriteUpdateAuditLogData(beforePermissions, afterPermissions, entry.Options.OverwriteTargetId.Value, target); + } + + public OverwritePermissions OldPermissions { get; } + public OverwritePermissions NewPermissions { get; } + + public ulong OverwriteTargetId { get; } + public PermissionTarget OverwriteType { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs new file mode 100644 index 000000000..0005e304d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs @@ -0,0 +1,22 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class PruneAuditLogData : IAuditLogData + { + private PruneAuditLogData(int pruneDays, int membersRemoved) + { + PruneDays = pruneDays; + MembersRemoved = membersRemoved; + } + + internal static PruneAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new PruneAuditLogData(entry.Options.PruneDeleteMemberDays.Value, entry.Options.PruneMembersRemoved.Value); + } + + public int PruneDays { get; } + public int MembersRemoved { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs new file mode 100644 index 000000000..aa951d6e7 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs @@ -0,0 +1,47 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class RoleCreateAuditLogData : IAuditLogData + { + private RoleCreateAuditLogData(ulong id, RoleInfo props) + { + RoleId = id; + Properties = props; + } + + internal static RoleCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color"); + var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable"); + var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions"); + + uint? colorRaw = colorModel?.NewValue?.ToObject(); + bool? mentionable = mentionableModel?.NewValue?.ToObject(); + bool? hoist = hoistModel?.NewValue?.ToObject(); + string name = nameModel?.NewValue?.ToObject(); + ulong? permissionsRaw = permissionsModel?.NewValue?.ToObject(); + + Color? color = null; + GuildPermissions? permissions = null; + + if (colorRaw.HasValue) + color = new Color(colorRaw.Value); + if (permissionsRaw.HasValue) + permissions = new GuildPermissions(permissionsRaw.Value); + + return new RoleCreateAuditLogData(entry.TargetId.Value, + new RoleInfo(color, mentionable, hoist, name, permissions)); + } + + public ulong RoleId { get; } + public RoleInfo Properties { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs new file mode 100644 index 000000000..e90d70d4d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs @@ -0,0 +1,47 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class RoleDeleteAuditLogData : IAuditLogData + { + private RoleDeleteAuditLogData(ulong id, RoleInfo props) + { + RoleId = id; + Properties = props; + } + + internal static RoleDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color"); + var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable"); + var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions"); + + uint? colorRaw = colorModel?.OldValue?.ToObject(); + bool? mentionable = mentionableModel?.OldValue?.ToObject(); + bool? hoist = hoistModel?.OldValue?.ToObject(); + string name = nameModel?.OldValue?.ToObject(); + ulong? permissionsRaw = permissionsModel?.OldValue?.ToObject(); + + Color? color = null; + GuildPermissions? permissions = null; + + if (colorRaw.HasValue) + color = new Color(colorRaw.Value); + if (permissionsRaw.HasValue) + permissions = new GuildPermissions(permissionsRaw.Value); + + return new RoleDeleteAuditLogData(entry.TargetId.Value, + new RoleInfo(color, mentionable, hoist, name, permissions)); + } + + public ulong RoleId { get; } + public RoleInfo Properties { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs new file mode 100644 index 000000000..2208990e6 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs @@ -0,0 +1,21 @@ +namespace Discord.Rest +{ + public struct RoleInfo + { + internal RoleInfo(Color? color, bool? mentionable, bool? hoist, string name, + GuildPermissions? permissions) + { + Color = color; + Mentionable = mentionable; + Hoist = hoist; + Name = name; + Permissions = permissions; + } + + public Color? Color { get; } + public bool? Mentionable { get; } + public bool? Hoist { get; } + public string Name { get; } + public GuildPermissions? Permissions { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs new file mode 100644 index 000000000..be484e2d5 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs @@ -0,0 +1,62 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class RoleUpdateAuditLogData : IAuditLogData + { + private RoleUpdateAuditLogData(ulong id, RoleInfo oldProps, RoleInfo newProps) + { + RoleId = id; + Before = oldProps; + After = newProps; + } + + internal static RoleUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color"); + var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable"); + var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions"); + + uint? oldColorRaw = colorModel?.OldValue?.ToObject(), + newColorRaw = colorModel?.NewValue?.ToObject(); + bool? oldMentionable = mentionableModel?.OldValue?.ToObject(), + newMentionable = mentionableModel?.NewValue?.ToObject(); + bool? oldHoist = hoistModel?.OldValue?.ToObject(), + newHoist = hoistModel?.NewValue?.ToObject(); + string oldName = nameModel?.OldValue?.ToObject(), + newName = nameModel?.NewValue?.ToObject(); + ulong? oldPermissionsRaw = permissionsModel?.OldValue?.ToObject(), + newPermissionsRaw = permissionsModel?.OldValue?.ToObject(); + + Color? oldColor = null, + newColor = null; + GuildPermissions? oldPermissions = null, + newPermissions = null; + + if (oldColorRaw.HasValue) + oldColor = new Color(oldColorRaw.Value); + if (newColorRaw.HasValue) + newColor = new Color(newColorRaw.Value); + if (oldPermissionsRaw.HasValue) + oldPermissions = new GuildPermissions(oldPermissionsRaw.Value); + if (newPermissionsRaw.HasValue) + newPermissions = new GuildPermissions(newPermissionsRaw.Value); + + var oldProps = new RoleInfo(oldColor, oldMentionable, oldHoist, oldName, oldPermissions); + var newProps = new RoleInfo(newColor, newMentionable, newHoist, newName, newPermissions); + + return new RoleUpdateAuditLogData(entry.TargetId.Value, oldProps, newProps); + } + + public ulong RoleId { get; } + public RoleInfo Before { get; } + public RoleInfo After { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs new file mode 100644 index 000000000..c94f18271 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs @@ -0,0 +1,23 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class UnbanAuditLogData : IAuditLogData + { + private UnbanAuditLogData(IUser user) + { + Target = user; + } + + internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new UnbanAuditLogData(RestUser.Create(discord, userInfo)); + } + + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs new file mode 100644 index 000000000..1ae45fb8c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs @@ -0,0 +1,44 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class WebhookCreateAuditLogData : IAuditLogData + { + private WebhookCreateAuditLogData(IWebhook webhook, WebhookType type, string name, ulong channelId) + { + Webhook = webhook; + Name = name; + Type = type; + ChannelId = channelId; + } + + internal static WebhookCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var channelId = channelIdModel.NewValue.ToObject(); + var type = typeModel.NewValue.ToObject(); + var name = nameModel.NewValue.ToObject(); + + var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId); + var webhook = RestWebhook.Create(discord, (IGuild)null, webhookInfo); + + return new WebhookCreateAuditLogData(webhook, type, name, channelId); + } + + //Corresponds to the *current* data + public IWebhook Webhook { get; } + + //Corresponds to the *audit log* data + public WebhookType Type { get; } + public string Name { get; } + public ulong ChannelId { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs new file mode 100644 index 000000000..4133d5dff --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class WebhookDeleteAuditLogData : IAuditLogData + { + private WebhookDeleteAuditLogData(ulong id, ulong channel, WebhookType type, string name, string avatar) + { + WebhookId = id; + ChannelId = channel; + Name = name; + Type = type; + Avatar = avatar; + } + + internal static WebhookDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash"); + + var channelId = channelIdModel.OldValue.ToObject(); + var type = typeModel.OldValue.ToObject(); + var name = nameModel.OldValue.ToObject(); + var avatarHash = avatarHashModel?.OldValue?.ToObject(); + + return new WebhookDeleteAuditLogData(entry.TargetId.Value, channelId, type, name, avatarHash); + } + + public ulong WebhookId { get; } + public ulong ChannelId { get; } + public WebhookType Type { get; } + public string Name { get; } + public string Avatar { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs new file mode 100644 index 000000000..26975cc7c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs @@ -0,0 +1,16 @@ +namespace Discord.Rest +{ + public struct WebhookInfo + { + internal WebhookInfo(string name, ulong? channelId, string avatar) + { + Name = name; + ChannelId = channelId; + Avatar = avatar; + } + + public string Name { get; } + public ulong? ChannelId { get; } + public string Avatar { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs new file mode 100644 index 000000000..54da42a8b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class WebhookUpdateAuditLogData : IAuditLogData + { + private WebhookUpdateAuditLogData(IWebhook webhook, WebhookInfo before, WebhookInfo after) + { + Webhook = webhook; + Before = before; + After = after; + } + + internal static WebhookUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash"); + + var oldName = nameModel?.OldValue?.ToObject(); + var oldChannelId = channelIdModel?.OldValue?.ToObject(); + var oldAvatar = avatarHashModel?.OldValue?.ToObject(); + var before = new WebhookInfo(oldName, oldChannelId, oldAvatar); + + var newName = nameModel?.NewValue?.ToObject(); + var newChannelId = channelIdModel?.NewValue?.ToObject(); + var newAvatar = avatarHashModel?.NewValue?.ToObject(); + var after = new WebhookInfo(newName, newChannelId, newAvatar); + + var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId); + var webhook = RestWebhook.Create(discord, (IGuild)null, webhookInfo); + + return new WebhookUpdateAuditLogData(webhook, before, after); + } + + //Again, the *current* data + public IWebhook Webhook { get; } + + //And the *audit log* data + public WebhookInfo Before { get; } + public WebhookInfo After { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs new file mode 100644 index 000000000..9e30a5014 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs @@ -0,0 +1,38 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + public class RestAuditLogEntry : RestEntity, IAuditLogEntry + { + private RestAuditLogEntry(BaseDiscordClient discord, Model fullLog, EntryModel model, IUser user) + : base(discord, model.Id) + { + Action = model.Action; + Data = AuditLogHelper.CreateData(discord, fullLog, model); + User = user; + Reason = model.Reason; + } + + internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model) + { + var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId); + IUser user = null; + if (userInfo != null) + user = RestUser.Create(discord, userInfo); + + return new RestAuditLogEntry(discord, fullLog, model, user); + } + + /// + public ActionType Action { get; } + /// + public IAuditLogData Data { get; } + /// + public IUser User { get; } + /// + public string Reason { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 8ab6e9819..b6f891f40 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -7,7 +7,7 @@ namespace Discord.Rest public interface IRestMessageChannel : IMessageChannel { /// Sends a message to this message channel. - new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 00ec4388b..21cd579e6 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -63,7 +63,7 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 451bc1ad5..901016a6b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -81,7 +81,7 @@ namespace Discord.Rest public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index c4e2d8a9c..841aad666 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -58,7 +58,7 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 33dbcdd91..8f69388d4 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -33,13 +33,13 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS, Embed embed = null, RequestOptions options = null) + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed = null, RequestOptions options = null) + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 12fdb075d..c76b41a91 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -111,6 +111,11 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); } + public static async Task GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) + { + var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); + return RestBan.Create(client, model); + } public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, int pruneDays, string reason, RequestOptions options) @@ -140,20 +145,36 @@ namespace Discord.Rest return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray(); } public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, - string name, RequestOptions options) + string name, RequestOptions options, Action func = null) { if (name == null) throw new ArgumentNullException(nameof(name)); - var args = new CreateGuildChannelParams(name, ChannelType.Text); + var props = new TextChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Text) + { + CategoryId = props.CategoryId, + Topic = props.Topic, + IsNsfw = props.IsNsfw + }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); } public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, - string name, RequestOptions options) + string name, RequestOptions options, Action func = null) { if (name == null) throw new ArgumentNullException(nameof(name)); - var args = new CreateGuildChannelParams(name, ChannelType.Voice); + var props = new VoiceChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Voice) + { + CategoryId = props.CategoryId, + Bitrate = props.Bitrate, + UserLimit = props.UserLimit + }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } @@ -263,6 +284,35 @@ namespace Discord.Rest return model.Pruned; } + // Audit logs + public static IAsyncEnumerable> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client, + ulong? from, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxAuditLogEntriesPerBatch, + async (info, ct) => + { + var args = new GetAuditLogsParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.BeforeEntryId = info.Position.Value; + var model = await client.ApiClient.GetAuditLogsAsync(guild.Id, args, options); + return model.Entries.Select((x) => RestAuditLogEntry.Create(client, model, x)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxAuditLogEntriesPerBatch) + return false; + info.Position = lastPage.Min(x => x.Id); + return true; + }, + start: from, + count: limit + ); + } + //Webhooks public static async Task GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 5d12731a6..357e22c85 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -140,6 +140,10 @@ namespace Discord.Rest //Bans public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); + public Task GetBanAsync(IUser user, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, user.Id, options); + public Task GetBanAsync(ulong userId, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, userId, options); public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); @@ -218,10 +222,10 @@ namespace Discord.Rest } return null; } - public Task CreateTextChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); - public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); + public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); @@ -264,6 +268,10 @@ namespace Discord.Rest public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + //Audit logs + public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null) + => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options); + //Webhooks public Task GetWebhookAsync(ulong id, RequestOptions options = null) => GuildHelper.GetWebhookAsync(this, Discord, id, options); @@ -291,6 +299,12 @@ namespace Discord.Rest async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(IUser user, RequestOptions options) + => await GetBanAsync(user, options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) + => await GetBanAsync(userId, options).ConfigureAwait(false); async Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) { @@ -369,10 +383,10 @@ namespace Discord.Rest else return null; } - async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) - => await CreateTextChannelAsync(name, options).ConfigureAwait(false); - async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) - => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) + => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) + => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); @@ -419,6 +433,14 @@ namespace Discord.Rest } Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + async Task> IGuild.GetAuditLogAsync(int limit, CacheMode cacheMode, RequestOptions options) + { + if (cacheMode == CacheMode.AllowDownload) + return (await GetAuditLogsAsync(limit, options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return ImmutableArray.Create(); + } + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options); async Task> IGuild.GetWebhooksAsync(RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs index 47fd2cd19..4e0c3c1ee 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System.Diagnostics; using Model = Discord.API.VoiceRegion; @@ -7,11 +7,16 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestVoiceRegion : RestEntity, IVoiceRegion { + /// public string Name { get; private set; } + /// public bool IsVip { get; private set; } + /// public bool IsOptimal { get; private set; } - public string SampleHostname { get; private set; } - public int SamplePort { get; private set; } + /// + public bool IsDeprecated { get; private set; } + /// + public bool IsCustom { get; private set; } internal RestVoiceRegion(BaseDiscordClient client, string id) : base(client, id) @@ -28,8 +33,8 @@ namespace Discord Name = model.Name; IsVip = model.IsVip; IsOptimal = model.IsOptimal; - SampleHostname = model.SampleHostname; - SamplePort = model.SamplePort; + IsDeprecated = model.IsDeprecated; + IsCustom = model.IsCustom; } public override string ToString() => Name; diff --git a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs index 80a49e34e..ebcd93777 100644 --- a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs +++ b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs @@ -1,14 +1,9 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord.Rest { internal static class InviteHelper { - public static async Task AcceptAsync(IInvite invite, BaseDiscordClient client, - RequestOptions options) - { - await client.ApiClient.AcceptInviteAsync(invite.Code, options).ConfigureAwait(false); - } public static async Task DeleteAsync(IInvite invite, BaseDiscordClient client, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 900d1f0ac..18698c626 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; +using Discord.API.Rest; using Model = Discord.API.Invite; namespace Discord.Rest @@ -10,6 +11,8 @@ namespace Discord.Rest { public string ChannelName { get; private set; } public string GuildName { get; private set; } + public int? PresenceCount { get; private set; } + public int? MemberCount { get; private set; } public ulong ChannelId { get; private set; } public ulong GuildId { get; private set; } internal IChannel Channel { get; private set; } @@ -36,19 +39,21 @@ namespace Discord.Rest ChannelId = model.Channel.Id; GuildName = model.Guild.Name; ChannelName = model.Channel.Name; + MemberCount = model.MemberCount.IsSpecified ? model.MemberCount.Value : null; + PresenceCount = model.PresenceCount.IsSpecified ? model.PresenceCount.Value : null; } public async Task UpdateAsync(RequestOptions options = null) { - var model = await Discord.ApiClient.GetInviteAsync(Code, options).ConfigureAwait(false); + var args = new GetInviteParams(); + if (MemberCount != null || PresenceCount != null) + args.WithCounts = true; + var model = await Discord.ApiClient.GetInviteAsync(Code, args, options).ConfigureAwait(false); Update(model); } public Task DeleteAsync(RequestOptions options = null) => InviteHelper.DeleteAsync(this, Discord, options); - public Task AcceptAsync(RequestOptions options = null) - => InviteHelper.AcceptAsync(this, Discord, options); - public override string ToString() => Url; private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 4059eaf17..3dc3e74e9 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -13,6 +13,9 @@ namespace Discord.Rest public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, RequestOptions options) { + if (msg.Author.Id != client.CurrentUser.Id) + throw new InvalidOperationException("Only the author of a message may change it."); + var args = new MessageProperties(); func(args); var apiArgs = new API.Rest.ModifyMessageParams @@ -45,13 +48,38 @@ namespace Discord.Rest await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); } - public static async Task> GetReactionUsersAsync(IMessage msg, IEmote emote, - Action func, BaseDiscordClient client, RequestOptions options) + public static IAsyncEnumerable> GetReactionUsersAsync(IMessage msg, IEmote emote, + int? limit, BaseDiscordClient client, RequestOptions options) { - var args = new GetReactionUsersParams(); - func(args); - string emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name); - return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => RestUser.Create(client, u)).ToImmutableArray(); + Preconditions.NotNull(emote, nameof(emote)); + var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name); + + return new PagedAsyncEnumerable( + DiscordConfig.MaxUserReactionsPerBatch, + async (info, ct) => + { + var args = new GetReactionUsersParams + { + Limit = info.PageSize + }; + + if (info.Position != null) + args.AfterUserId = info.Position.Value; + + var models = await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false); + return models.Select(x => RestUser.Create(client, x)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxUsersPerBatch) + return false; + + info.Position = lastPage.Max(x => x.Id); + return true; + }, + count: limit + ); + } public static async Task PinAsync(IMessage msg, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index e5eed874e..7354cc4af 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -136,8 +136,8 @@ namespace Discord.Rest => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); public Task PinAsync(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index c6cf6103a..c484986b1 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -60,6 +60,9 @@ namespace Discord.Rest public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 2d96ca796..3346681b5 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -163,7 +163,7 @@ namespace Discord.Net.Queue if (!isRateLimited) throw new TimeoutException(); else - throw new RateLimitedException(request); + ThrowRetryLimit(request); } lock (_lock) @@ -181,13 +181,12 @@ namespace Discord.Net.Queue await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); } - if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) - throw new RateLimitedException(request); + ThrowRetryLimit(request); if (resetAt.HasValue) { if (resetAt > timeoutAt) - throw new RateLimitedException(request); + ThrowRetryLimit(request); int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); @@ -198,7 +197,7 @@ namespace Discord.Net.Queue else { if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) - throw new RateLimitedException(request); + ThrowRetryLimit(request); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); #endif @@ -309,5 +308,11 @@ namespace Discord.Net.Queue } } } + + private void ThrowRetryLimit(RestRequest request) + { + if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) + throw new RateLimitedException(request); + } } } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index e881a7855..c236b1045 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.WebSocket @@ -165,6 +165,13 @@ namespace Discord.WebSocket remove { _userVoiceStateUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + /// Fired when the bot connects to a Discord voice server. + public event Func VoiceServerUpdated + { + add { _voiceServerUpdatedEvent.Add(value); } + remove { _voiceServerUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _voiceServerUpdatedEvent = new AsyncEvent>(); /// Fired when the connected account is updated. public event Func CurrentUserUpdated { add { _selfUpdatedEvent.Add(value); } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 923b2c23b..fb82fe14a 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -55,8 +55,8 @@ namespace Discord.WebSocket public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); /// - public Task GetInviteAsync(string inviteId, RequestOptions options = null) - => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); + public Task GetInviteAsync(string inviteId, bool withCount = false, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, withCount, options ?? RequestOptions.Default); // IDiscordClient async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) @@ -70,8 +70,8 @@ namespace Discord.WebSocket async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync(options).ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) - => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options) + => await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false); Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index e4de43e51..251cdb18b 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -4,7 +4,8 @@ Discord.Net.WebSocket Discord.WebSocket A core Discord.Net library containing the WebSocket client and models. - net45;netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3 + netstandard1.1;netstandard1.3 true @@ -14,4 +15,4 @@ - \ No newline at end of file + diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index ad89067df..dad53b3b7 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -129,7 +129,7 @@ namespace Discord.WebSocket private int GetShardIdFor(ulong guildId) => (int)((guildId >> 22) % (uint)_totalShards); public int GetShardIdFor(IGuild guild) - => GetShardIdFor(guild.Id); + => GetShardIdFor(guild?.Id ?? 0); private DiscordSocketClient GetShardFor(ulong guildId) => GetShard(GetShardIdFor(guildId)); public DiscordSocketClient GetShardFor(IGuild guild) @@ -327,8 +327,8 @@ namespace Discord.WebSocket async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) - => await GetInviteAsync(inviteId).ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options) + => await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false); Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 593f796c2..72b0b022b 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -62,9 +62,9 @@ namespace Discord.WebSocket internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public override IReadOnlyCollection Guilds => State.Guilds; public override IReadOnlyCollection PrivateChannels => State.PrivateChannels; - public IReadOnlyCollection DMChannels + public IReadOnlyCollection DMChannels => State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray(); - public IReadOnlyCollection GroupChannels + public IReadOnlyCollection GroupChannels => State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray(); public override IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); @@ -89,11 +89,11 @@ namespace Discord.WebSocket _stateLock = new SemaphoreSlim(1, 1); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); - _connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout, + _connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); - + _nextAudioId = 1; _connectionGroupLock = groupLock; _parentClient = parentClient; @@ -104,7 +104,7 @@ namespace Discord.WebSocket _gatewayLogger.WarningAsync("Serializer Error", e.ErrorContext.Error).GetAwaiter().GetResult(); e.ErrorContext.Handled = true; }; - + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; @@ -136,7 +136,7 @@ namespace Discord.WebSocket ApiClient.Dispose(); } } - + internal override async Task OnLoginAsync(TokenType tokenType, string token) { if (_parentClient == null) @@ -154,11 +154,11 @@ namespace Discord.WebSocket _voiceRegions = ImmutableDictionary.Create(); } - public override async Task StartAsync() + public override async Task StartAsync() => await _connection.StartAsync().ConfigureAwait(false); - public override async Task StopAsync() + public override async Task StopAsync() => await _connection.StopAsync().ConfigureAwait(false); - + private async Task OnConnectingAsync() { if (_connectionGroupLock != null) @@ -181,11 +181,11 @@ namespace Discord.WebSocket //Wait for READY await _connection.WaitAsync().ConfigureAwait(false); - + await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); await SendStatusAsync().ConfigureAwait(false); } - finally + finally { if (_connectionGroupLock != null) { @@ -230,22 +230,22 @@ namespace Discord.WebSocket } /// - public override async Task GetApplicationInfoAsync(RequestOptions options = null) + public override async Task GetApplicationInfoAsync(RequestOptions options = null) => _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false)); /// - public override SocketGuild GetGuild(ulong id) - => State.GetGuild(id); + public override SocketGuild GetGuild(ulong id) + => State.GetGuild(id); /// - public override SocketChannel GetChannel(ulong id) + public override SocketChannel GetChannel(ulong id) => State.GetChannel(id); - + /// - public override SocketUser GetUser(ulong id) + public override SocketUser GetUser(ulong id) => State.GetUser(id); /// - public override SocketUser GetUser(string username, string discriminator) + public override SocketUser GetUser(string username, string discriminator) => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { @@ -266,7 +266,7 @@ namespace Discord.WebSocket return user; }); } - internal void RemoveUser(ulong id) + internal void RemoveUser(ulong id) => State.RemoveUser(id); /// @@ -340,7 +340,7 @@ namespace Discord.WebSocket Activity = activity; await SendStatusAsync().ConfigureAwait(false); } - + private async Task SendStatusAsync() { if (CurrentUser == null) @@ -374,7 +374,7 @@ namespace Discord.WebSocket if (seq != null) _lastSeq = seq.Value; _lastMessageTime = Environment.TickCount; - + try { switch (opCode) @@ -390,7 +390,7 @@ namespace Discord.WebSocket case GatewayOpCode.Heartbeat: { await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false); - + await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); } break; @@ -415,7 +415,7 @@ namespace Discord.WebSocket _sessionId = null; _lastSeq = 0; - + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } break; @@ -475,18 +475,18 @@ namespace Discord.WebSocket } else if (_connection.CancelToken.IsCancellationRequested) return; - + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); }); - var _ = _connection.CompleteAsync(); + _ = _connection.CompleteAsync(); } break; case "RESUMED": { await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); - var _ = _connection.CompleteAsync(); + _ = _connection.CompleteAsync(); //Notify the client that these guilds are available again foreach (var guild in State.Guilds) @@ -514,7 +514,7 @@ namespace Discord.WebSocket if (guild != null) { guild.Update(State, data); - + if (_unavailableGuildCount != 0) _unavailableGuildCount--; await GuildAvailableAsync(guild).ConfigureAwait(false); @@ -1025,7 +1025,7 @@ namespace Discord.WebSocket SocketUser user = guild.GetUser(data.User.Id); if (user == null) - user = SocketUnknownUser.Create(this, State, data.User); + user = SocketUnknownUser.Create(this, State, data.User); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else @@ -1095,8 +1095,10 @@ namespace Discord.WebSocket else if (channel is SocketGroupChannel) author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); else + { await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); - return; + return; + } } var msg = SocketMessage.Create(this, State, author, channel, data); @@ -1323,7 +1325,7 @@ namespace Discord.WebSocket await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); } } - + var before = user.Clone(); user.Update(State, data, true); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); @@ -1464,16 +1466,29 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); var guild = State.GetGuild(data.GuildId); - if (guild != null) + var isCached = guild != null; + var cachedGuild = new Cacheable(guild, data.GuildId, isCached, + () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); + + var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); + await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); + + if (isCached) { - string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); + var endpoint = data.Endpoint; + + //Only strip out the port if the endpoint contains it + var portBegin = endpoint.LastIndexOf(':'); + if (portBegin > 0) + endpoint = endpoint.Substring(0, portBegin); + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); } else { await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; } + } break; @@ -1796,8 +1811,8 @@ namespace Discord.WebSocket async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) - => await GetInviteAsync(inviteId).ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options) + => await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false); Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 026bd8378..971dbe8f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket IReadOnlyCollection CachedMessages { get; } /// Sends a message to this message channel. - new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index d5a183b1e..e7a165c2f 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -15,10 +15,12 @@ namespace Discord.WebSocket public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { public override IReadOnlyCollection Users - => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + => Guild.Users.Where(x => Permissions.GetValue( + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + ChannelPermission.ViewChannel)).ToImmutableArray(); public IReadOnlyCollection Channels - => Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); + => Guild.Channels.Where(x => x.CategoryId == Id).ToImmutableArray(); internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) @@ -31,14 +33,28 @@ namespace Discord.WebSocket return entity; } + //Users + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user != null) + { + var guildPerms = Permissions.ResolveGuild(Guild, user); + var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) + return user; + } + return null; + } + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; // IGuildChannel IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => Task.FromResult(GetUser(id)); Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => throw new NotSupportedException(); Task> IGuildChannel.GetInvitesAsync(RequestOptions options) @@ -46,8 +62,8 @@ namespace Discord.WebSocket //IChannel IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => Task.FromResult(GetUser(id)); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 7d1c75291..d95d45890 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -67,7 +67,7 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index d4e3acd6d..3d6f60db2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -95,7 +95,7 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 42dd78d8e..9373c431a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -75,7 +75,7 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 259dae5a8..14263f0af 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -288,6 +288,10 @@ namespace Discord.WebSocket //Bans public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); + public Task GetBanAsync(IUser user, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, user.Id, options); + public Task GetBanAsync(ulong userId, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, userId, options); public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); @@ -311,10 +315,10 @@ namespace Discord.WebSocket => GetChannel(id) as SocketTextChannel; public SocketVoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; - public Task CreateTextChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); - public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); + public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); @@ -434,6 +438,10 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + //Audit logs + public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null) + => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options); + //Webhooks public Task GetWebhookAsync(ulong id, RequestOptions options = null) => GuildHelper.GetWebhookAsync(this, Discord, id, options); @@ -641,6 +649,12 @@ namespace Discord.WebSocket async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(IUser user, RequestOptions options) + => await GetBanAsync(user, options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) + => await GetBanAsync(userId, options).ConfigureAwait(false); Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Channels); @@ -664,10 +678,10 @@ namespace Discord.WebSocket => Task.FromResult(EmbedChannel); Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(SystemChannel); - async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) - => await CreateTextChannelAsync(name, options).ConfigureAwait(false); - async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) - => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) + => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) + => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); @@ -693,6 +707,14 @@ namespace Discord.WebSocket Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); + async Task> IGuild.GetAuditLogAsync(int limit, CacheMode cacheMode, RequestOptions options) + { + if (cacheMode == CacheMode.AllowDownload) + return (await GetAuditLogsAsync(limit, options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return ImmutableArray.Create(); + } + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options); async Task> IGuild.GetWebhooksAsync(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index b240645e5..f8c15a986 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -130,8 +130,8 @@ namespace Discord.WebSocket => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 58d5c62a1..00899d47e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Threading.Tasks; using Model = Discord.API.User; @@ -37,23 +37,23 @@ namespace Discord.WebSocket { var newVal = ushort.Parse(model.Discriminator.Value); if (newVal != DiscriminatorValue) - { + { DiscriminatorValue = ushort.Parse(model.Discriminator.Value); hasChanges = true; } } if (model.Bot.IsSpecified && model.Bot.Value != IsBot) - { + { IsBot = model.Bot.Value; hasChanges = true; } if (model.Username.IsSpecified && model.Username.Value != Username) - { + { Username = model.Username.Value; hasChanges = true; } return hasChanges; - } + } public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; @@ -61,6 +61,9 @@ namespace Discord.WebSocket public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 78a29639b..dd80648d2 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -26,6 +26,7 @@ namespace Discord.WebSocket internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) : base(guild.Discord, id) { + Guild = guild; WebhookId = webhookId; } internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) diff --git a/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs b/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs new file mode 100644 index 000000000..57abf1d03 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketVoiceServer + { + public Cacheable Guild { get; private set; } + public string Endpoint { get; private set; } + public string Token { get; private set; } + + internal SocketVoiceServer(Cacheable guild, string endpoint, string token) + { + Guild = guild; + Endpoint = endpoint; + Token = token; + } + + private string DebuggerDisplay => $"SocketVoiceServer ({Guild.Id})"; + } +} diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index ff395a932..e8dc4b5f0 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -1,3 +1,6 @@ +using System.Collections.Immutable; +using System.Linq; + namespace Discord.WebSocket { internal static class EntityExtensions @@ -15,12 +18,13 @@ namespace Discord.WebSocket { Name = model.Name, SessionId = model.SessionId.GetValueOrDefault(), - SyncId = model.SyncId.Value, + TrackId = model.SyncId.Value, + TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value), AlbumTitle = albumText, TrackTitle = model.Details.GetValueOrDefault(), - Artists = model.State.GetValueOrDefault()?.Split(';'), + Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(), Duration = timestamps?.End - timestamps?.Start, - AlbumArt = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, + AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, Type = ActivityType.Listening }; } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 59cc8f3e7..2dea1c1d1 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -63,7 +63,7 @@ namespace Discord.Webhook => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); /// 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, + public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options);