diff --git a/CHANGELOG.md b/CHANGELOG.md index ac159d86f..886754052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # Changelog +## [2.2.0] - 2020-04-16 +### Added +- #1247 Implement Client Status Support (9da11b4) +- #1310 id overload for RemoveReactionAsync (c88b1da) +- #1319 BOOST (faf23de) +- #1326 Added a Rest property to DiscordShardedClient (9fede34) +- #1348 Add Quote Formatting (265da99) +- #1354 Add support for setting X-RateLimit-Precision (9482204) +- #1355 Provide ParameterInfo with error ParseResult (3755a02) +- #1357 add the "Stream" permission. (b00da3d) +- #1358 Add ChannelFollowAdd MessageType (794eba5) +- #1369 Add SelfStream voice state property (9bb08c9) +- #1372 support X-RateLimit-Reset-After (7b9029d) +- #1373 update audit log models (c54867f) +- #1377 Support filtering audit log entries on user, action type, and before entry id (68eb71c) +- #1386 support guild subscription opt-out (0d54207) +- #1387 #1381 Guild PreferredLocale support (a61adb0) +- #1406 CustomStatusGame Activity (79a0ea9) +- #1413 Implemented Message Reference Property (f86c39d) +- #1414 add StartedAt, EndsAt, Elapsed and Remaining to SpotifyGame. (2bba324) +- #1432 Add ability to modify the banner for guilds (d734ce0) +- suppress messages (cd28892) + +### Fixed +- #1318 #1314 Don't parse tags within code blocks (c977f2e) +- #1333 Remove null coalescing on ToEmbedBuilder Color (120c0f7) +- #1337 Fixed attempting to access a non-present optional value (4edda5b) +- #1346 CommandExecuted event will fire when a parameter precondition fails like what happens when standard precondition fails. (e8cb031) +- #1371 Fix keys of guild update audit (b0a595b) +- #1375 Use double precision for X-Reset-After, set CultureInfo when parsing numeric types (606dac3) +- #1392 patch todo in NamedTypeReader (0bda8a4) +- #1405 add .NET Standard 2.1 support for Color (7f0c0c9) +- #1412 GetUsersAsync to use MaxUsersPerBatch const as limit instead of MaxMessagesPerBatch. (5439cba) +- #1416 false-positive detection of CustomStatusGame based on Id property (a484651) +- #1418 #1335 Add isMentionable parameter to CreateRoleAsync in non-breaking manner (1c63fd4) +- #1421 (3ff4e3d) +- include MessageFlags and SuppressEmbedParams (d6d4429) + +### Changed +- #1368 Update ISystemMessage interface to allow reactions (07f4d5f) +- #1417 fix #1415 Re-add support for overwrite permissions for news channels (e627f07) +- use millisecond precision by default (bcb3534) + +### Misc +- #1290 Split Unit and Integration tests into separate projects (a797be9) +- #1328 Fix #1327 Color.ToString returns wrong value (1e8aa08) +- #1329 Fix invalid cref values in docs (363d1c6) +- #1330 Fix spelling mistake in ExclusiveBulkDelete warning (c864f48) +- #1331 Change token explanation (0484fe8) +- #1349 Fixed a spelling error. (af79ed5) +- #1353 [ci skip] Removed duplicate "any" from the readme (15b2a36) +- #1359 Fixing GatewayEncoding comment (52565ed) +- #1379 September 2019 Documentation Update (fd3810e) +- #1382 Fix .NET Core 3.0 compatibility + Drop NS1.3 (d199d93) +- #1388 fix coercion error with DateTime/Offset (3d39704) +- #1393 Utilize ValueTuples (99d7135) +- #1400 Fix #1394 Misworded doc for command params args (1c6ee72) +- #1401 Fix package publishing in azure pipelines (a08d529) +- #1402 Fix packaging (65223a6) +- #1403 Cache regex instances in MessageHelper (007b011) +- #1424 Fix the Comparer descriptions not linking the type (911523d) +- #1426 Fix incorrect and missing colour values for Color fields (9ede6b9) +- #1470 Added System.Linq reference (adf823c) +- temporary sanity checking in SocketGuild (c870e67) +- build and deploy docs automatically (2981d6b) +- 2.2.0 (4b602b4) +- target the Process env-var scope (3c6b376) +- fix metapackage build (1794f95) +- copy only _site to docs-static (a8cdadc) +- do not exit on failed robocopy (fd204ee) +- add idn debugger (91aec9f) +- rename IsStream to IsStreaming (dcd9cdd) +- feature (40844b9) + ## [2.1.1] - 2019-06-08 ### Fixed - #994: Remainder parameters now ignore character escaping, as there is no reason to escape characters here (2e95c49) diff --git a/Discord.Net.sln b/Discord.Net.sln index 54a788f7d..1a32f1270 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers.Tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src\Discord.Net.Examples\Discord.Net.Examples.csproj", "{47820065-3CFB-401C-ACEA-862BD564A404}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,6 +220,18 @@ Global {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x64.Build.0 = Release|Any CPU {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.ActiveCfg = Release|Any CPU {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -236,6 +250,7 @@ Global {E169E15A-E82C-45BF-8C24-C2CADB7093AA} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} {FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} {47820065-3CFB-401C-ACEA-862BD564A404} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/Discord.Net.targets b/Discord.Net.targets index 98751ec22..adb42f7a9 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,9 +1,9 @@ - 2.2.0 + 2.3.0 dev - RogueException - 8.0 + latest + Discord.Net Contributors discord;discordapp https://github.com/RogueException/Discord.Net http://opensource.org/licenses/MIT diff --git a/docs/_overwrites/Common/DiscordComparers.Overwrites.md b/docs/_overwrites/Common/DiscordComparers.Overwrites.md new file mode 100644 index 000000000..cbff7cf74 --- /dev/null +++ b/docs/_overwrites/Common/DiscordComparers.Overwrites.md @@ -0,0 +1,29 @@ +--- +uid: Discord.DiscordComparers.ChannelComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare channels. + +--- +uid: Discord.DiscordComparers.GuildComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare guilds. + +--- +uid: Discord.DiscordComparers.MessageComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare messages. + +--- +uid: Discord.DiscordComparers.RoleComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare roles. + +--- +uid: Discord.DiscordComparers.UserComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare users. diff --git a/docs/docfx.json b/docs/docfx.json index 17cf72388..759dc174f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -9,7 +9,7 @@ "dest": "api", "filter": "filterConfig.yml", "properties": { - "TargetFramework": "netstandard1.3" + "TargetFramework": "netstandard2.0" } }], "build": { @@ -51,7 +51,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2019 2.1.1", + "_appFooter": "Discord.Net (c) 2015-2020 2.2.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md index 93811977f..35c71709f 100644 --- a/docs/faq/basics/basic-operations.md +++ b/docs/faq/basics/basic-operations.md @@ -88,7 +88,7 @@ implement [IEmote] and are valid options. *** -[AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* +[AddReactionAsync]: xref:Discord.IMessage.AddReactionAsync* ## What is a "preemptive rate limit?" diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md index f8ff1b596..abe7065c1 100644 --- a/docs/guides/commands/intro.md +++ b/docs/guides/commands/intro.md @@ -71,11 +71,11 @@ By now, your module should look like this: > [!WARNING] > **Avoid using long-running code** in your modules wherever possible. -> You should **not** be implementing very much logic into your -> modules, instead, outsource to a service for that. +> Long-running code, by default, within a command module +> can cause gateway thread to be blocked; therefore, interrupting +> the bot's connection to Discord. > -> If you are unfamiliar with Inversion of Control, it is recommended -> to read the MSDN article on [IoC] and [Dependency Injection]. +> You may read more about it in @FAQ.Commands.General . The next step to creating commands is actually creating the commands. diff --git a/docs/guides/commands/namedarguments.md b/docs/guides/commands/namedarguments.md new file mode 100644 index 000000000..890a8463f --- /dev/null +++ b/docs/guides/commands/namedarguments.md @@ -0,0 +1,79 @@ +--- +uid: Guides.Commands.NamedArguments +title: Named Arguments +--- + +# Named Arguments + +By default, arguments for commands are parsed positionally, meaning +that the order matters. But sometimes you may want to define a command +with many optional parameters, and it'd be easier for end-users +to only specify what they want to set, instead of needing them +to specify everything by hand. + +## Setting up Named Arguments + +In order to be able to specify different arguments by name, you have +to create a new class that contains all of the optional values that +the command will use, and apply an instance of +[NamedArgumentTypeAttribute] on it. + +### Example - Creating a Named Arguments Type + +```cs +[NamedArgumentType] +public class NamableArguments +{ + public string First { get; set; } + public string Second { get; set; } + public string Third { get; set; } + public string Fourth { get; set; } +} +``` + +## Usage in a Command + +The command where you want to use these values can be declared like so: +```cs +[Command("act")] +public async Task Act(int requiredArg, NamableArguments namedArgs) +``` + +The command can now be invoked as +`.act 42 first: Hello fourth: "A string with spaces must be wrapped in quotes" second: World`. + +A TypeReader for the named arguments container type is +automatically registered. +It's important that any other arguments that would be required +are placed before the container type. + +> [!IMPORTANT] +> A single command can have only __one__ parameter of a +> type annotated with [NamedArgumentTypeAttribute], and it +> **MUST** be the last parameter in the list. +> A command parameter of such an annotated type +> is automatically treated as if that parameter +> has [RemainderAttribute](xref:Discord.Commands.RemainderAttribute) +> applied. + +## Complex Types + +The TypeReader for Named Argument Types will look for a TypeReader +of every property type, meaning any other command parameter type +will work just the same. + +You can also read multiple values into a single property +by making that property an `IEnumerable`. So for example, if your +Named Argument Type has the following field, +```cs +public IEnumerable Numbers { get; set; } +``` +then the command can be invoked as +`.cmd numbers: "1, 2, 4, 8, 16, 32"` + +## Additional Notes + +The use of [`[OverrideTypeReader]`](xref:Discord.Commands.OverrideTypeReaderAttribute) +is also supported on the properties of a Named Argument Type. + +[NamedArgumentTypeAttribute]: xref:Discord.Commands.NamedArgumentTypeAttribute diff --git a/docs/guides/commands/samples/preconditions/require_role.cs b/docs/guides/commands/samples/preconditions/require_role.cs index 77d09b525..d9a393ace 100644 --- a/docs/guides/commands/samples/preconditions/require_role.cs +++ b/docs/guides/commands/samples/preconditions/require_role.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Discord.Commands; using Discord.WebSocket; diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 01c245301..a6c38768f 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -27,6 +27,8 @@ topicUid: Guides.Commands.Intro - name: TypeReaders topicUid: Guides.Commands.TypeReaders + - name: Named Arguments + topicUid: Guides.Commands.NamedArguments - name: Preconditions topicUid: Guides.Commands.Preconditions - name: Dependency Injection diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs index 4d6674e97..7fbe04993 100644 --- a/samples/01_basic_ping_bot/Program.cs +++ b/samples/01_basic_ping_bot/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -43,7 +44,7 @@ namespace _01_basic_ping_bot await _client.StartAsync(); // Block the program until it is closed. - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } private Task LogAsync(LogMessage log) diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs index ccbc8e165..67cb87764 100644 --- a/samples/02_commands_framework/Program.cs +++ b/samples/02_commands_framework/Program.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Discord; @@ -45,7 +46,7 @@ namespace _02_commands_framework // Here we initialize the logic required to register our commands. await services.GetRequiredService().InitializeAsync(); - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } } diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs index 7a2f99168..753f400a1 100644 --- a/samples/03_sharded_client/Program.cs +++ b/samples/03_sharded_client/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using _03_sharded_client.Services; using Discord; @@ -45,7 +46,7 @@ namespace _03_sharded_client await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); await client.StartAsync(); - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } } diff --git a/samples/idn/Inspector.cs b/samples/idn/Inspector.cs new file mode 100644 index 000000000..3806e0e79 --- /dev/null +++ b/samples/idn/Inspector.cs @@ -0,0 +1,74 @@ +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace idn +{ + public static class Inspector + { + public static string Inspect(object value) + { + var builder = new StringBuilder(); + if (value != null) + { + var type = value.GetType().GetTypeInfo(); + builder.AppendLine($"[{type.Namespace}.{type.Name}]"); + builder.AppendLine($"{InspectProperty(value)}"); + + if (value is IEnumerable) + { + var items = (value as IEnumerable).Cast().ToArray(); + if (items.Length > 0) + { + builder.AppendLine(); + foreach (var item in items) + builder.AppendLine($"- {InspectProperty(item)}"); + } + } + else + { + var groups = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.GetIndexParameters().Length == 0) + .GroupBy(x => x.Name) + .OrderBy(x => x.Key) + .ToArray(); + if (groups.Length > 0) + { + builder.AppendLine(); + int pad = groups.Max(x => x.Key.Length) + 1; + foreach (var group in groups) + builder.AppendLine($"{group.Key.PadRight(pad, ' ')}{InspectProperty(group.First().GetValue(value))}"); + } + } + } + else + builder.AppendLine("null"); + return builder.ToString(); + } + + private static string InspectProperty(object obj) + { + if (obj == null) + return "null"; + + var type = obj.GetType(); + + var debuggerDisplay = type.GetProperty("DebuggerDisplay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (debuggerDisplay != null) + return debuggerDisplay.GetValue(obj).ToString(); + + var toString = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == "ToString" && x.DeclaringType != typeof(object)) + .FirstOrDefault(); + if (toString != null) + return obj.ToString(); + + var count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (count != null) + return $"[{count.GetValue(obj)} Items]"; + + return obj.ToString(); + } + } +} diff --git a/samples/idn/Program.cs b/samples/idn/Program.cs new file mode 100644 index 000000000..ffd8fd1af --- /dev/null +++ b/samples/idn/Program.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Discord; +using Discord.WebSocket; +using System.Collections.Concurrent; +using System.Threading; +using System.Text; +using System.Diagnostics; + +namespace idn +{ + public class Program + { + public static readonly string[] Imports = + { + "System", + "System.Collections.Generic", + "System.Linq", + "System.Threading.Tasks", + "System.Diagnostics", + "System.IO", + "Discord", + "Discord.Rest", + "Discord.WebSocket", + "idn" + }; + + static async Task Main(string[] args) + { + var token = File.ReadAllText("token.ignore"); + var client = new DiscordSocketClient(new DiscordSocketConfig { LogLevel = LogSeverity.Debug }); + var logQueue = new ConcurrentQueue(); + var logCancelToken = new CancellationTokenSource(); + int presenceUpdates = 0; + + client.Log += msg => + { + logQueue.Enqueue(msg); + return Task.CompletedTask; + }; + Console.CancelKeyPress += (_ev, _s) => + { + logCancelToken.Cancel(); + }; + + var logTask = Task.Run(async () => + { + var fs = new FileStream("idn.log", FileMode.Append); + var logStringBuilder = new StringBuilder(200); + string logString = ""; + + byte[] helloBytes = Encoding.UTF8.GetBytes($"### new log session: {DateTime.Now} ###\n\n"); + await fs.WriteAsync(helloBytes); + + while (!logCancelToken.IsCancellationRequested) + { + if (logQueue.TryDequeue(out var msg)) + { + if (msg.Message?.IndexOf("PRESENCE_UPDATE)") > 0) + { + presenceUpdates++; + continue; + } + + _ = msg.ToString(builder: logStringBuilder); + logStringBuilder.AppendLine(); + logString = logStringBuilder.ToString(); + + Debug.Write(logString, "DNET"); + await fs.WriteAsync(Encoding.UTF8.GetBytes(logString)); + } + await fs.FlushAsync(); + try + { + await Task.Delay(100, logCancelToken.Token); + } + finally { } + } + + byte[] goodbyeBytes = Encoding.UTF8.GetBytes($"#!! end log session: {DateTime.Now} !!#\n\n\n"); + await fs.WriteAsync(goodbyeBytes); + await fs.DisposeAsync(); + }); + + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + var options = ScriptOptions.Default + .AddReferences(GetAssemblies().ToArray()) + .AddImports(Imports); + + var globals = new ScriptGlobals + { + Client = client, + PUCount = -1, + }; + + while (true) + { + Console.Write("> "); + string input = Console.ReadLine(); + + if (input == "quit!") + { + break; + } + + object eval; + try + { + globals.PUCount = presenceUpdates; + eval = await CSharpScript.EvaluateAsync(input, options, globals); + } + catch (Exception e) + { + eval = e; + } + Console.WriteLine(Inspector.Inspect(eval)); + } + + await client.StopAsync(); + client.Dispose(); + logCancelToken.Cancel(); + try + { await logTask; } + finally { Console.WriteLine("goodbye!"); } + } + + static IEnumerable GetAssemblies() + { + var Assemblies = Assembly.GetEntryAssembly().GetReferencedAssemblies(); + foreach (var a in Assemblies) + { + var asm = Assembly.Load(a); + yield return asm; + } + yield return Assembly.GetEntryAssembly(); + } + + public class ScriptGlobals + { + public DiscordSocketClient Client { get; set; } + public int PUCount { get; set; } + } + } +} diff --git a/samples/idn/idn.csproj b/samples/idn/idn.csproj new file mode 100644 index 000000000..984c86383 --- /dev/null +++ b/samples/idn/idn.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + diff --git a/samples/idn/logview.ps1 b/samples/idn/logview.ps1 new file mode 100644 index 000000000..0857475f5 --- /dev/null +++ b/samples/idn/logview.ps1 @@ -0,0 +1 @@ +Get-Content .\bin\Debug\netcoreapp3.1\idn.log -Tail 3 -Wait \ No newline at end of file diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 3a7da3862..1d4b0e15a 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -36,7 +36,7 @@ namespace Discord.Commands internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); /// - /// Occurs when a command is successfully executed without any error. + /// Occurs when a command is executed. /// /// /// This event is fired when a command has been executed, successfully or not. When a command fails to @@ -49,7 +49,7 @@ namespace Discord.Commands private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; private readonly ConcurrentDictionary _defaultTypeReaders; - private readonly ImmutableList> _entityTypeReaders; //TODO: Candidate for C#7 Tuple + private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders; private readonly HashSet _moduleDefs; private readonly CommandMap _map; @@ -124,11 +124,11 @@ namespace Discord.Commands _defaultTypeReaders[typeof(string)] = new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); - var entityTypeReaders = ImmutableList.CreateBuilder>(); - entityTypeReaders.Add(new Tuple(typeof(IMessage), typeof(MessageTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IChannel), typeof(ChannelTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IRole), typeof(RoleTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IUser), typeof(UserTypeReader<>))); + var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>(); + entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>))); + entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>))); + entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>))); + entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>))); _entityTypeReaders = entityTypeReaders.ToImmutable(); } @@ -408,7 +408,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.IsEnum) return true; - return _entityTypeReaders.Any(x => type == x.Item1 || typeInfo.ImplementedInterfaces.Contains(x.Item2)); + return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType)); } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { @@ -439,9 +439,9 @@ namespace Discord.Commands //Is this an entity? for (int i = 0; i < _entityTypeReaders.Count; i++) { - if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1)) + if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) { - reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader; + reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader; _defaultTypeReaders[type] = reader; return reader; } diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 2dedceaa5..3c62063c8 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -44,7 +44,7 @@ namespace Discord.Commands /// /// /// - /// QuotationMarkAliasMap = new Dictionary<char, char%gt;() + /// QuotationMarkAliasMap = new Dictionary<char, char>() /// { /// {'\"', '\"' }, /// {'“', '”' }, diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 9cd4ea15d..ec1722c70 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -31,9 +31,13 @@ namespace Discord.Commands /// /// Specifies if Discord should read this aloud using text-to-speech. /// An embed to be displayed alongside the . - protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + /// + /// Specifies if notifications are sent for mentioned users and roles in the . + /// If null, all mentioned roles and users will be notified. + /// + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { - return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); + return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// /// The method to execute before executing the command. diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index b603e27a4..8c44f49e3 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -20,6 +20,10 @@ namespace Discord /// /// The user is watching some form of media. /// - Watching = 3 + Watching = 3, + /// + /// The user has set a custom status. + /// + CustomStatus = 4, } } diff --git a/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs b/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs new file mode 100644 index 000000000..7bd2664a2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for their custom status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class CustomStatusGame : Game + { + internal CustomStatusGame() { } + + /// + /// Gets the emote, if it is set. + /// + /// + /// An containing the or set by the user. + /// + public IEmote Emote { get; internal set; } + + /// + /// Gets the timestamp of when this status was created. + /// + /// + /// A containing the time when this status was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the state of the status. + /// + public string State { get; internal set; } + + public override string ToString() + => $"{Emote} {State}"; + + private string DebuggerDisplay => $"{Name}"; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index 23f88687d..4eab34fa2 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -31,6 +31,23 @@ namespace Discord /// A string containing the name of the song (e.g. Lonely Together (feat. Rita Ora)). /// public string TrackTitle { get; internal set; } + + /// + /// Gets the date when the track started playing. + /// + /// + /// A containing the start timestamp of the song. + /// + public DateTimeOffset? StartedAt { get; internal set; } + + /// + /// Gets the date when the track ends. + /// + /// + /// A containing the finish timestamp of the song. + /// + public DateTimeOffset? EndsAt { get; internal set; } + /// /// Gets the duration of the song. /// @@ -39,6 +56,22 @@ namespace Discord /// public TimeSpan? Duration { get; internal set; } + /// + /// Gets the elapsed duration of the song. + /// + /// + /// A containing the elapsed duration of the song. + /// + public TimeSpan? Elapsed => DateTimeOffset.UtcNow - StartedAt; + + /// + /// Gets the remaining duration of the song. + /// + /// + /// A containing the remaining duration of the song. + /// + public TimeSpan? Remaining => EndsAt - DateTimeOffset.UtcNow; + /// /// Gets the track ID of the song. /// diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs index 2561a0970..1728b2021 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -61,6 +61,18 @@ namespace Discord /// A guild member's role collection was updated. /// MemberRoleUpdated = 25, + /// + /// A guild member moved to a voice channel. + /// + MemberMoved = 26, + /// + /// A guild member disconnected from a voice channel. + /// + MemberDisconnected = 27, + /// + /// A bot was added to this guild. + /// + BotAdded = 28, /// /// A role was created in this guild. @@ -117,6 +129,18 @@ namespace Discord /// /// A message was deleted from this guild. /// - MessageDeleted = 72 + MessageDeleted = 72, + /// + /// Multiple messages were deleted from this guild. + /// + MessageBulkDeleted = 73, + /// + /// A message was pinned from this guild. + /// + MessagePinned = 74, + /// + /// A message was unpinned from this guild. + /// + MessageUnpinned = 75, } } diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index b5aa69d55..f5b986295 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -23,11 +23,15 @@ namespace Discord /// Determines whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index e38e1db41..2c9503db1 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -40,8 +40,8 @@ namespace Discord /// Creates a new invite to this channel. /// /// - /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only - /// be used 3 times throughout its lifespan. + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. /// /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); /// @@ -60,8 +60,8 @@ namespace Discord /// Gets a collection of all invites to this channel. /// B /// - /// The following example gets all of the invites that have been created in this channel and selects the - /// most used invite. + /// The following example gets all of the invites that have been created in this channel and selects the + /// most used invite. /// /// var invites = await channel.GetInvitesAsync(); /// if (invites.Count == 0) return; diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 29c764e3f..a2baf6990 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -30,7 +30,7 @@ namespace Discord /// Gets the current slow-mode delay for this channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// int SlowModeInterval { get; } @@ -39,7 +39,7 @@ namespace Discord /// Bulk-deletes multiple messages. /// /// - /// The following example gets 250 messages from the channel and deletes them. + /// The following example gets 250 messages from the channel and deletes them. /// /// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync(); /// await textChannel.DeleteMessagesAsync(messages); diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index ec31019af..981e1198c 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -38,6 +38,10 @@ namespace Discord /// public Optional Icon { get; set; } /// + /// Gets or sets the banner of the guild. + /// + public Optional Banner { get; set; } + /// /// Gets or sets the guild's splash image. /// /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 213091ad4..b39a49776 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -510,7 +510,7 @@ namespace Discord /// Creates a new text channel in this guild. /// /// - /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. /// /// @@ -598,6 +598,21 @@ namespace Discord /// role. /// Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); + // TODO remove CreateRoleAsync overload that does not have isMentionable when breaking change is acceptable + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, bool isMentionable = false, RequestOptions options = null); /// /// Adds a user to this guild. diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs new file mode 100644 index 000000000..3ce6531b7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs @@ -0,0 +1,24 @@ +using System; + +namespace Discord +{ + /// + /// Specifies the type of mentions that will be notified from the message content. + /// + [Flags] + public enum AllowedMentionTypes + { + /// + /// Controls role mentions. + /// + Roles, + /// + /// Controls user mentions. + /// + Users, + /// + /// Controls @everyone and @here mentions. + /// + Everyone, + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..9b168bbd0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Defines which mentions and types of mentions that will notify users from the message content. + /// + public class AllowedMentions + { + private static readonly Lazy none = new Lazy(() => new AllowedMentions()); + private static readonly Lazy all = new Lazy(() => + new AllowedMentions(AllowedMentionTypes.Everyone | AllowedMentionTypes.Users | AllowedMentionTypes.Roles)); + + /// + /// Gets a value which indicates that no mentions in the message content should notify users. + /// + public static AllowedMentions None => none.Value; + + /// + /// Gets a value which indicates that all mentions in the message content should notify users. + /// + public static AllowedMentions All => all.Value; + + /// + /// Gets or sets the type of mentions that will be parsed from the message content. + /// + /// + /// The flag is mutually exclusive with the + /// property, and the flag is mutually exclusive with the + /// property. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentionTypes? AllowedTypes { get; set; } + + /// + /// Gets or sets the list of all role ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List RoleIds { get; set; } + + /// + /// Gets or sets the list of all user ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List UserIds { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The types of mentions to parse from the message content. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentions(AllowedMentionTypes? allowedTypes = null) + { + AllowedTypes = allowedTypes; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 1eba1e076..aac526831 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -140,6 +140,18 @@ namespace Discord /// MessageApplication Application { get; } + /// + /// Gets the reference to the original message if it was crossposted. + /// + /// + /// Sent with Cross-posted messages, meaning they were published from news channels + /// and received by subscriber channels. + /// + /// + /// A message's reference, if any is associated. + /// + MessageReference Reference { get; } + /// /// Gets all reactions included in this message. /// @@ -149,7 +161,7 @@ namespace Discord /// Adds a reaction to this message. /// /// - /// The following example adds the reaction, 💕, to the message. + /// The following example adds the reaction, 💕, to the message. /// /// await msg.AddReactionAsync(new Emoji("\U0001f495")); /// @@ -165,7 +177,7 @@ namespace Discord /// Removes a reaction from message. /// /// - /// The following example removes the reaction, 💕, added by the message author from the message. + /// The following example removes the reaction, 💕, added by the message author from the message. /// /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author); /// @@ -182,7 +194,7 @@ namespace Discord /// Removes a reaction from message. /// /// - /// The following example removes the reaction, 💕, added by the user with ID 84291986575613952 from the message. + /// The following example removes the reaction, 💕, added by the user with ID 84291986575613952 from the message. /// /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952); /// @@ -207,8 +219,25 @@ namespace Discord /// /// Gets all users that reacted to a message with a given emote. /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of reactions specified under . + /// The library will attempt to split up the requests according to your and + /// . In other words, should the user request 500 reactions, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// /// - /// The following example gets the users that have reacted with the emoji 💕 to the message. + /// The following example gets the users that have reacted with the emoji 💕 to the message. /// /// var emoji = new Emoji("\U0001f495"); /// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync(); @@ -218,9 +247,7 @@ namespace Discord /// The number of users to request. /// The options to be used when sending the request. /// - /// A paged collection containing a read-only collection of users that has reacted to this message. - /// Flattening the paginated response into a collection of users with - /// is required if you wish to access the users. + /// Paged collection of users. /// IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index be2523b21..bc52dd01c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -17,7 +17,7 @@ namespace Discord /// method and what properties are available, please refer to . /// /// - /// The following example replaces the content of the message with Hello World!. + /// The following example replaces the content of the message with Hello World!. /// /// await msg.ModifyAsync(x => x.Content = "Hello World!"); /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs new file mode 100644 index 000000000..57a508a7c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// Contains the IDs sent from a crossposted message. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageReference + { + /// + /// Gets the Message ID of the original message. + /// + public Optional MessageId { get; internal set; } + + /// + /// Gets the Channel ID of the original message. + /// + public ulong ChannelId { get; internal set; } + + /// + /// Gets the Guild ID of the original message. + /// + public Optional GuildId { get; internal set; } + + private string DebuggerDisplay + => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index b522ae47e..7c2d152a4 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -16,60 +16,61 @@ namespace Discord /// A color struct with the hex value of 1ABC9C. public static readonly Color Teal = new Color(0x1ABC9C); /// Gets the dark teal color value. + /// A color struct with the hex value of 11806A. public static readonly Color DarkTeal = new Color(0x11806A); /// Gets the green color value. - /// A color struct with the hex value of 11806A. + /// A color struct with the hex value of 2ECC71. public static readonly Color Green = new Color(0x2ECC71); /// Gets the dark green color value. - /// A color struct with the hex value of 2ECC71. + /// A color struct with the hex value of 1F8B4C. public static readonly Color DarkGreen = new Color(0x1F8B4C); /// Gets the blue color value. - /// A color struct with the hex value of 1F8B4C. + /// A color struct with the hex value of 3498DB. public static readonly Color Blue = new Color(0x3498DB); /// Gets the dark blue color value. - /// A color struct with the hex value of 3498DB. + /// A color struct with the hex value of 206694. public static readonly Color DarkBlue = new Color(0x206694); /// Gets the purple color value. - /// A color struct with the hex value of 206694. + /// A color struct with the hex value of 9B59B6. public static readonly Color Purple = new Color(0x9B59B6); /// Gets the dark purple color value. - /// A color struct with the hex value of 9B59B6. + /// A color struct with the hex value of 71368A. public static readonly Color DarkPurple = new Color(0x71368A); /// Gets the magenta color value. - /// A color struct with the hex value of 71368A. + /// A color struct with the hex value of E91E63. public static readonly Color Magenta = new Color(0xE91E63); /// Gets the dark magenta color value. - /// A color struct with the hex value of E91E63. + /// A color struct with the hex value of AD1457. public static readonly Color DarkMagenta = new Color(0xAD1457); /// Gets the gold color value. - /// A color struct with the hex value of AD1457. + /// A color struct with the hex value of F1C40F. public static readonly Color Gold = new Color(0xF1C40F); /// Gets the light orange color value. - /// A color struct with the hex value of F1C40F. + /// A color struct with the hex value of C27C0E. public static readonly Color LightOrange = new Color(0xC27C0E); /// Gets the orange color value. - /// A color struct with the hex value of C27C0E. + /// A color struct with the hex value of E67E22. public static readonly Color Orange = new Color(0xE67E22); /// Gets the dark orange color value. - /// A color struct with the hex value of E67E22. + /// A color struct with the hex value of A84300. public static readonly Color DarkOrange = new Color(0xA84300); /// Gets the red color value. - /// A color struct with the hex value of A84300. + /// A color struct with the hex value of E74C3C. public static readonly Color Red = new Color(0xE74C3C); /// Gets the dark red color value. - /// A color struct with the hex value of E74C3C. - public static readonly Color DarkRed = new Color(0x992D22); - /// Gets the light grey color value. /// A color struct with the hex value of 992D22. + public static readonly Color DarkRed = new Color(0x992D22); + /// Gets the light grey color value. + /// A color struct with the hex value of 979C9F. public static readonly Color LightGrey = new Color(0x979C9F); /// Gets the lighter grey color value. - /// A color struct with the hex value of 979C9F. + /// A color struct with the hex value of 95A5A6. public static readonly Color LighterGrey = new Color(0x95A5A6); /// Gets the dark grey color value. - /// A color struct with the hex value of 95A5A6. + /// A color struct with the hex value of 607D8B. public static readonly Color DarkGrey = new Color(0x607D8B); /// Gets the darker grey color value. - /// A color struct with the hex value of 607D8B. + /// A color struct with the hex value of 546E7A. public static readonly Color DarkerGrey = new Color(0x546E7A); /// Gets the encoded value for this color. diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index ae682afd5..60fa06cbd 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -72,8 +72,8 @@ namespace Discord /// Gets the level permissions granted to this user to a given channel. /// /// - /// The following example checks if the current user has the ability to send a message with attachment in - /// this channel; if so, uploads a file via . + /// The following example checks if the current user has the ability to send a message with attachment in + /// this channel; if so, uploads a file via . /// /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// await targetChannel.SendFileAsync("fortnite.png"); diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index c59a75de1..c36fb2326 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -21,8 +21,8 @@ namespace Discord /// example). /// /// - /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is - /// not set, a default avatar for this user will be returned instead. + /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is + /// not set, a default avatar for this user will be returned instead. /// /// @@ -90,8 +90,8 @@ namespace Discord /// /// /// - /// The following example attempts to send a direct message to the target user and logs the incident should - /// it fail. + /// The following example attempts to send a direct message to the target user and logs the incident should + /// it fail. /// /// diff --git a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs index e5d6025c2..f6ba7624d 100644 --- a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -15,7 +15,7 @@ namespace Discord //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) - => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + => new CollectionWrapper(source.Values, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new CollectionWrapper(query, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) diff --git a/src/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/Discord.Net.Core/Extensions/MessageExtensions.cs index 90ebea92f..64a1d89ab 100644 --- a/src/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -39,7 +39,7 @@ namespace Discord /// /// A task that represents the asynchronous operation for adding a reaction to this message. /// - /// + /// /// public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) { @@ -51,7 +51,7 @@ namespace Discord /// /// /// This method does not bulk remove reactions! If you want to clear reactions from a message, - /// + /// /// /// /// @@ -64,7 +64,7 @@ namespace Discord /// /// A task that represents the asynchronous operation for removing a reaction to this message. /// - /// + /// /// public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) { diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index f98bf7227..90f26828a 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -28,6 +28,10 @@ namespace Discord /// Whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents the asynchronous send operation. The task result contains the sent message. /// @@ -35,17 +39,18 @@ namespace Discord string text = null, bool isTTS = false, Embed embed = null, - RequestOptions options = null) + RequestOptions options = null, + AllowedMentions allowedMentions = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// /// Sends a file to this message channel with an optional caption. /// /// - /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a - /// rich embed to the channel. + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. /// /// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index e1c900680..f972cd71d 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -270,7 +270,7 @@ namespace Discord /// /// The options to be used when sending the request. /// - /// A task that represents the asynchronous get operation. The task result contains an + /// A task that represents the asynchronous get operation. The task result contains an /// that represents the number of shards that should be used with this account. /// Task GetRecommendedShardCountAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 14b41cce1..6791af354 100644 --- a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets void SetCancelToken(CancellationToken cancelToken); Task ConnectAsync(string host); - Task DisconnectAsync(); + Task DisconnectAsync(int closeCode = 1000); Task SendAsync(byte[] data, int index, int count, bool isText); } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 6aa0eea12..1b05df2a3 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -49,8 +49,7 @@ namespace Discord /// clock for rate-limiting. Defaults to true. /// /// - /// This property can also be set in . - /// + /// This property can also be set in . /// On a per-request basis, the system clock should only be disabled /// when millisecond precision is especially important, and the /// hosting system is known to have a desynced clock. diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index ef99c8045..50f19b778 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -40,7 +40,7 @@ namespace Discord.Net.Providers.WS4Net { if (disposing) { - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); _lock?.Dispose(); _cancelTokenSource?.Dispose(); } @@ -92,19 +92,19 @@ namespace Discord.Net.Providers.WS4Net _waitUntilConnect.Wait(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private Task DisconnectInternalAsync(bool isDisposing = false) + private Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { _disconnectCancelTokenSource.Cancel(); if (_client == null) @@ -112,7 +112,7 @@ namespace Discord.Net.Providers.WS4Net if (_client.State == WebSocketState.Open) { - try { _client.Close(1000, ""); } + try { _client.Close(closeCode, ""); } catch { } } diff --git a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs index 24141d90c..b666215e2 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs @@ -4,11 +4,12 @@ namespace Discord.API { internal class AuditLogOptions { - //Message delete [JsonProperty("count")] - public int? MessageDeleteCount { get; set; } + public int? Count { get; set; } [JsonProperty("channel_id")] - public ulong? MessageDeleteChannelId { get; set; } + public ulong? ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong? MessageId { get; set; } //Prune [JsonProperty("delete_member_days")] diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 2ec1e3846..d3a618697 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -35,6 +35,12 @@ namespace Discord.API public Optional SessionId { get; set; } [JsonProperty("Flags")] public Optional Flags { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("emoji")] + public Optional Emoji { get; set; } + [JsonProperty("created_at")] + public Optional CreatedAt { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index a5016f923..b4529d457 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -50,7 +50,11 @@ namespace Discord.API // sent with Rich Presence-related chat embeds [JsonProperty("application")] public Optional Application { get; set; } + [JsonProperty("message_reference")] + public Optional Reference { get; set; } [JsonProperty("flags")] public Optional Flags { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageReference.cs b/src/Discord.Net.Rest/API/Common/MessageReference.cs new file mode 100644 index 000000000..8c0f8fe14 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageReference.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class MessageReference + { + [JsonProperty("message_id")] + public Optional MessageId { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index d77bff8ca..4b56658d6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -15,6 +15,8 @@ namespace Discord.API.Rest public Optional IsTTS { get; set; } [JsonProperty("embed")] public Optional Embed { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs index 6341b63b6..cfb107bcd 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -22,6 +22,8 @@ namespace Discord.API.Rest public Optional SystemChannelId { get; set; } [JsonProperty("icon")] public Optional Icon { get; set; } + [JsonProperty("banner")] + public Optional Banner { get; set; } [JsonProperty("splash")] public Optional Splash { get; set; } [JsonProperty("afk_channel_id")] diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ff6d17240..a726ef75d 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -47,7 +47,7 @@ namespace Discord.API internal ulong? CurrentUserId { get; set; } public RateLimitPrecision RateLimitPrecision { get; private set; } internal bool UseSystemClock { get; set; } - + internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. @@ -164,7 +164,7 @@ namespace Discord.API try { _loginCancelToken?.Cancel(false); } catch { } - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); @@ -175,7 +175,7 @@ namespace Discord.API } internal virtual Task ConnectInternalAsync() => Task.Delay(0); - internal virtual Task DisconnectInternalAsync() => Task.Delay(0); + internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0); //Core internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, @@ -602,13 +602,8 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); - if (args.Content.IsSpecified) - { - if (!args.Embed.IsSpecified) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); @@ -1062,7 +1057,7 @@ namespace Discord.API { foreach (var roleId in args.RoleIds.Value) Preconditions.NotEqual(roleId, 0, nameof(roleId)); - } + } options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs index 7936343f3..696917203 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -27,6 +27,9 @@ namespace Discord.Rest [ActionType.Unban] = UnbanAuditLogData.Create, [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, + [ActionType.MemberMoved] = MemberMoveAuditLogData.Create, + [ActionType.MemberDisconnected] = MemberDisconnectAuditLogData.Create, + [ActionType.BotAdded] = BotAddAuditLogData.Create, [ActionType.RoleCreated] = RoleCreateAuditLogData.Create, [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, @@ -45,6 +48,9 @@ namespace Discord.Rest [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, + [ActionType.MessageBulkDeleted] = MessageBulkDeleteAuditLogData.Create, + [ActionType.MessagePinned] = MessagePinAuditLogData.Create, + [ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create, }; public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs new file mode 100644 index 000000000..0d12e4609 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a adding a bot to a guild. + /// + public class BotAddAuditLogData : IAuditLogData + { + private BotAddAuditLogData(IUser bot) + { + Target = bot; + } + + internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the bot that was added. + /// + /// + /// A user object representing the bot. + /// + 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 index f432b4ca5..5c2f81ae6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -25,7 +25,6 @@ namespace Discord.Rest 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"); @@ -34,23 +33,17 @@ namespace Discord.Rest var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + var overwrites = overwritesModel.NewValue.ToObject(discord.ApiClient.Serializer) + .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) + .ToList(); var type = typeModel.NewValue.ToObject(discord.ApiClient.Serializer); var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); bool? nsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); int? bitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var id = entry.TargetId.Value; - foreach (var overwrite in overwritesModel.NewValue) - { - var deny = overwrite["deny"].ToObject(discord.ApiClient.Serializer); - var permType = overwrite["type"].ToObject(discord.ApiClient.Serializer); - var id = overwrite["id"].ToObject(discord.ApiClient.Serializer); - var allow = overwrite["allow"].ToObject(discord.ApiClient.Serializer); - - overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); - } - - return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); + return new ChannelCreateAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); } /// @@ -78,7 +71,7 @@ namespace Discord.Rest /// Gets the current slow-mode delay of the created channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -95,7 +88,7 @@ namespace Discord.Rest /// Gets the bit-rate that the clients in the created voice channel are requested to use. /// /// - /// An representing the bit-rate (bps) that the created voice channel defines and requests the + /// An representing the bit-rate (bps) that the created voice channel defines and requests the /// client(s) to use. /// null if this is not mentioned in this entry. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 390749929..81ae7155b 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -71,7 +71,7 @@ namespace Discord.Rest /// Gets the slow-mode delay of the deleted channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -88,7 +88,7 @@ namespace Discord.Rest /// Gets the bit-rate of this channel if applicable. /// /// - /// An representing the bit-rate set of the voice channel. + /// An representing the bit-rate set of the voice channel. /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index d6d2fb4b3..0284b63f5 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -32,7 +32,7 @@ namespace Discord.Rest /// Gets the current slow-mode delay of this channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -49,7 +49,7 @@ namespace Discord.Rest /// Gets the bit-rate of this channel if applicable. /// /// - /// An representing the bit-rate set for the voice channel; + /// An representing the bit-rate set for the voice channel; /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs new file mode 100644 index 000000000..b0374dc86 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to disconnecting members from voice channels. + /// + public class MemberDisconnectAuditLogData : IAuditLogData + { + private MemberDisconnectAuditLogData(int count) + { + MemberCount = count; + } + + internal static MemberDisconnectAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberDisconnectAuditLogData(entry.Options.Count.Value); + } + + /// + /// Gets the number of members that were disconnected. + /// + /// + /// An representing the number of members that were disconnected from a voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs new file mode 100644 index 000000000..f5373d34d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs @@ -0,0 +1,37 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to moving members between voice channels. + /// + public class MemberMoveAuditLogData : IAuditLogData + { + private MemberMoveAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MemberCount = count; + } + + internal static MemberMoveAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberMoveAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the members were moved to. + /// + /// + /// A representing the snowflake identifier for the channel that the members were moved to. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of members that were moved. + /// + /// + /// An representing the number of members that were moved to another voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs new file mode 100644 index 000000000..7a9846349 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs @@ -0,0 +1,38 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to message deletion(s). + /// + public class MessageBulkDeleteAuditLogData : IAuditLogData + { + private MessageBulkDeleteAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MessageCount = count; + } + + internal static MessageBulkDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MessageBulkDeleteAuditLogData(entry.TargetId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the messages were deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the messages were + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of messages that were deleted. + /// + /// + /// An representing the number of messages that were deleted from the channel. + /// + public int MessageCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index c6b2e1053..66b3f7d83 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -8,16 +10,17 @@ namespace Discord.Rest /// public class MessageDeleteAuditLogData : IAuditLogData { - private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId) + private MessageDeleteAuditLogData(ulong channelId, int count, IUser user) { ChannelId = channelId; MessageCount = count; - AuthorId = authorId; + Target = user; } internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { - return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value); + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); } /// @@ -36,11 +39,11 @@ namespace Discord.Rest /// public ulong ChannelId { get; } /// - /// Gets the author of the messages that were deleted. + /// Gets the user of the messages that were deleted. /// /// - /// A representing the snowflake identifier for the user that created the deleted messages. + /// A user object representing the user that created the deleted messages. /// - public ulong AuthorId { get; } + public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs new file mode 100644 index 000000000..020171152 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -0,0 +1,48 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a pinned message. + /// + public class MessagePinAuditLogData : IAuditLogData + { + private MessagePinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the ID of the messages that was pinned. + /// + /// + /// A representing the snowflake identifier for the messages that was pinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was pinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was pinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was pinned. + /// + /// + /// A user object representing the user that created the pinned message. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs new file mode 100644 index 000000000..1b3ff96f3 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -0,0 +1,48 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an unpinned message. + /// + public class MessageUnpinAuditLogData : IAuditLogData + { + private MessageUnpinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the ID of the messages that was unpinned. + /// + /// + /// A representing the snowflake identifier for the messages that was unpinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was unpinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was unpinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was unpinned. + /// + /// + /// A user object representing the user that created the unpinned message. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs index a193e76ce..dc8948d37 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -21,16 +21,17 @@ namespace Discord.Rest 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(discord.ApiClient.Serializer); - var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); - var id = idModel.OldValue.ToObject(discord.ApiClient.Serializer); var allow = allowModel.OldValue.ToObject(discord.ApiClient.Serializer); - return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, new OverwritePermissions(allow, deny))); + var permissions = new OverwritePermissions(allow, deny); + + var id = entry.Options.OverwriteTargetId.Value; + var type = entry.Options.OverwriteType; + + return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions)); } /// diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 5fb150cda..aa90b2eee 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -167,9 +167,28 @@ namespace Discord.Rest /// Message content is too long, length must be less or equal to . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, RequestOptions options) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options) { - var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel(), AllowedMentions = allowedMentions?.ToModel() }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index a28170eed..195fa92df 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -20,11 +20,15 @@ namespace Discord.Rest /// Determines whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 446410b70..732af2d81 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -93,8 +93,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -206,8 +206,8 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IChannel /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 5cfe03f15..3c21bd95f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -95,8 +95,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -183,8 +183,9 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IAudioChannel /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs index f4984a0d2..8a334fae5 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs @@ -25,29 +25,5 @@ namespace Discord.Rest return entity; } public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode."); - public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override OverwritePermissions? GetPermissionOverwrite(IRole role) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override OverwritePermissions? GetPermissionOverwrite(IUser user) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index dc86327bd..cecd0a4d2 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -101,8 +101,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -273,8 +273,8 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IGuildChannel /// diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 05a520547..790b1e5c3 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -32,6 +32,7 @@ namespace Discord.Rest Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), Name = args.Name, Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), + Banner = args.Banner.IsSpecified ? args.Banner.Value?.ToModel() : Optional.Create(), VerificationLevel = args.VerificationLevel, ExplicitContentFilter = args.ExplicitContentFilter, SystemChannelFlags = args.SystemChannelFlags @@ -57,6 +58,8 @@ namespace Discord.Rest else if (args.RegionId.IsSpecified) apiArgs.RegionId = args.RegionId.Value; + if (!apiArgs.Banner.IsSpecified && guild.BannerId != null) + apiArgs.Banner = new ImageModel(guild.BannerId); if (!apiArgs.Splash.IsSpecified && guild.SplashId != null) apiArgs.Splash = new ImageModel(guild.SplashId); if (!apiArgs.Icon.IsSpecified && guild.IconId != null) @@ -257,7 +260,7 @@ namespace Discord.Rest //Roles /// is null. public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, - string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) + string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) { if (name == null) throw new ArgumentNullException(paramName: nameof(name)); @@ -270,6 +273,7 @@ namespace Discord.Rest x.Permissions = (permissions ?? role.Permissions); x.Color = (color ?? Color.Default); x.Hoist = isHoisted; + x.Mentionable = isMentionable; }, options).ConfigureAwait(false); return role; @@ -349,7 +353,7 @@ namespace Discord.Rest ulong? fromUserId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( - DiscordConfig.MaxMessagesPerBatch, + DiscordConfig.MaxUsersPerBatch, async (info, ct) => { var args = new GetGuildMembersParams @@ -363,7 +367,7 @@ namespace Discord.Rest }, nextPage: (info, lastPage) => { - if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + if (lastPage.Count != DiscordConfig.MaxUsersPerBatch) return false; info.Position = lastPage.Max(x => x.Id); return true; diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index e9e4d3290..900f5045e 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -530,6 +530,11 @@ namespace Discord.Rest return null; } + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => CreateRoleAsync(name, permissions, color, isHoisted, false, options); + /// /// Creates a new role with the provided name. /// @@ -538,14 +543,15 @@ namespace Discord.Rest /// The color of the role. /// Whether the role is separated from others on the sidebar. /// The options to be used when sending the request. + /// Whether the role can be mentioned. /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// role. /// public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) { - var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); + var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); _roles = _roles.Add(role.Id, role); return role; } @@ -833,7 +839,10 @@ namespace Discord.Rest => GetRole(id); /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) - => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); /// async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..5ab96032f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class AllowedMentions + { + [JsonProperty("parse")] + public Optional Parse { get; set; } + // Roles and Users have a max size of 100 + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("users")] + public Optional Users { get; set; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 75892defb..b29eca62e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -32,6 +32,11 @@ namespace Discord.Rest var args = new MessageProperties(); func(args); + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content); + bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any(); + if (!hasText && !hasEmbed) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + var apiArgs = new API.Rest.ModifyMessageParams { Content = args.Content, diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 29a9c9bd2..f457f4f7a 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -62,6 +62,8 @@ namespace Discord.Rest public MessageActivity Activity { get; private set; } /// public MessageApplication Application { get; private set; } + /// + public MessageReference Reference { get; private set; } internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) @@ -108,6 +110,17 @@ namespace Discord.Rest }; } + if(model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + ChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } + if (model.Reactions.IsSpecified) { var value = model.Reactions.Value; diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 4d164df96..abdfc9d4f 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -5,6 +6,13 @@ namespace Discord.Rest { internal static class EntityExtensions { + public static IEmote ToIEmote(this API.Emoji model) + { + if (model.Id.HasValue) + return model.ToEntity(); + return new Emoji(model.Name); + } + public static GuildEmote ToEntity(this API.Emoji model) => new GuildEmote(model.Id.Value, model.Name, @@ -53,6 +61,24 @@ namespace Discord.Rest model.Video = entity.Video.Value.ToModel(); return model; } + public static API.AllowedMentions ToModel(this AllowedMentions entity) + { + return new API.AllowedMentions() + { + Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(), + Roles = entity.RoleIds?.ToArray(), + Users = entity.UserIds?.ToArray(), + }; + } + public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) + { + if (mentionTypes.HasFlag(AllowedMentionTypes.Everyone)) + yield return "everyone"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Roles)) + yield return "roles"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) + yield return "users"; + } public static EmbedAuthor ToEntity(this API.EmbedAuthor model) { return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index dad185d66..f2e370d02 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -82,6 +82,20 @@ namespace Discord.WebSocket } return null; } + internal void PurgeAllChannels() + { + foreach (var guild in _guilds.Values) + guild.PurgeChannelCache(this); + + PurgeDMChannels(); + } + internal void PurgeDMChannels() + { + foreach (var channel in _dmChannels.Values) + _channels.TryRemove(channel.Id, out _); + + _dmChannels.Clear(); + } internal SocketGuild GetGuild(ulong id) { @@ -96,7 +110,11 @@ namespace Discord.WebSocket internal SocketGuild RemoveGuild(ulong id) { if (_guilds.TryRemove(id, out SocketGuild guild)) + { + guild.PurgeChannelCache(this); + guild.PurgeGuildUserCache(); return guild; + } return null; } @@ -116,5 +134,10 @@ namespace Discord.WebSocket return user; return null; } + internal void PurgeUsers() + { + foreach (var guild in _guilds.Values) + guild.PurgeGuildUserCache(); + } } } diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 0877abfd9..8359ca048 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -389,8 +389,11 @@ namespace Discord.WebSocket { if (disposing) { - foreach (var client in _shards) - client?.Dispose(); + if (_shards != null) + { + foreach (var client in _shards) + client?.Dispose(); + } _connectionGroupLock?.Dispose(); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 9313f0711..ef97615e2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -164,26 +164,17 @@ namespace Discord.API } } - public async Task DisconnectAsync() + public async Task DisconnectAsync(Exception ex = null) { await _stateLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally { _stateLock.Release(); } - } - public async Task DisconnectAsync(Exception ex) - { - await _stateLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(ex).ConfigureAwait(false); } finally { _stateLock.Release(); } } /// This client is not configured with WebSocket support. - internal override async Task DisconnectInternalAsync() + internal override async Task DisconnectInternalAsync(Exception ex = null) { if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with WebSocket support."); @@ -194,6 +185,9 @@ namespace Discord.API try { _connectCancelToken?.Cancel(false); } catch { } + if (ex is GatewayReconnectException) + await WebSocketClient.DisconnectAsync(4000); + else await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index ed142d001..be7432bc3 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -264,7 +264,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); - await ApiClient.DisconnectAsync().ConfigureAwait(false); + await ApiClient.DisconnectAsync(ex).ConfigureAwait(false); //Wait for tasks to complete await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); @@ -306,6 +306,14 @@ namespace Discord.WebSocket /// public override SocketChannel GetChannel(ulong id) => State.GetChannel(id); + /// + /// Clears all cached channels from the client. + /// + public void PurgeChannelCache() => State.PurgeAllChannels(); + /// + /// Clears cached DM channels from the client. + /// + public void PurgeDMChannelCache() => State.PurgeDMChannels(); /// public override SocketUser GetUser(ulong id) @@ -313,6 +321,10 @@ namespace Discord.WebSocket /// public override SocketUser GetUser(string username, string discriminator) => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); + /// + /// Clears cached users from the client. + /// + public void PurgeUserCache() => State.PurgeUsers(); internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { return state.GetOrAddUser(model.Id, x => @@ -511,7 +523,7 @@ namespace Discord.WebSocket case GatewayOpCode.Reconnect: { await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - _connection.Error(new Exception("Server requested a reconnect")); + _connection.Error(new GatewayReconnectException("Server requested a reconnect")); } break; case GatewayOpCode.Dispatch: @@ -628,6 +640,7 @@ namespace Discord.WebSocket if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else { @@ -1689,7 +1702,7 @@ namespace Discord.WebSocket { if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) { - _connection.Error(new Exception("Server missed last heartbeat")); + _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); return; } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index b88d0106b..378478dcc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -29,11 +29,15 @@ namespace Discord.WebSocket /// Determines whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 838fb8ef2..11259a31e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -135,8 +135,8 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -235,8 +235,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 26fcbe83c..c57c37db2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -163,8 +163,8 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -299,8 +299,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IAudioChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs index f84c35cae..815a99ce7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket /// /// /// - /// Most of the properties and methods featured may not be supported due to the nature of the channel. + /// The property is not supported for news channels. /// /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] @@ -35,53 +35,5 @@ namespace Discord.WebSocket /// public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This property is not supported by this type. Attempting to use this property will result in a . - /// - /// - public override IReadOnlyCollection PermissionOverwrites - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task SyncPermissionsAsync(RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ca7ca11dc..1b3b5bcd7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -161,8 +161,8 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -308,8 +308,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); // INestedChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 054348ef1..fb0a56c24 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -623,6 +623,13 @@ namespace Discord.WebSocket return state.RemoveChannel(id) as SocketGuildChannel; return null; } + internal void PurgeChannelCache(ClientState state) + { + foreach (var channelId in _channels) + state.RemoveChannel(channelId); + + _channels.Clear(); + } //Voice Regions /// @@ -679,6 +686,10 @@ namespace Discord.WebSocket return null; } + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options); /// /// Creates a new role with the provided name. /// @@ -686,6 +697,7 @@ namespace Discord.WebSocket /// The guild permission that the role should possess. /// The color of the role. /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. /// The options to be used when sending the request. /// is null. /// @@ -693,8 +705,8 @@ namespace Discord.WebSocket /// role. /// public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) - => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); internal SocketRole AddRole(RoleModel model) { var role = SocketRole.Create(this, Discord.State, model); @@ -792,6 +804,21 @@ namespace Discord.WebSocket } return null; } + internal void PurgeGuildUserCache() + { + var members = Users; + var self = CurrentUser; + _members.Clear(); + _members.TryAdd(self.Id, self); + + DownloadedMemberCount = _members.Count; + + foreach (var member in members) + { + if (member.Id != self.Id) + member.GlobalUser.RemoveRef(Discord); + } + } /// public async Task DownloadUsersAsync() @@ -1151,7 +1178,10 @@ namespace Discord.WebSocket => GetRole(id); /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) - => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); /// Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index ae42d9d61..7900b7ee7 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -53,6 +53,9 @@ namespace Discord.WebSocket /// public MessageApplication Application { get; private set; } + /// + public MessageReference Reference { get; private set; } + /// /// Returns all attachments included in this message. /// @@ -140,6 +143,17 @@ namespace Discord.WebSocket PartyId = model.Activity.Value.PartyId.Value }; } + + if (model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + ChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } } /// diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index d4798bedd..09c4165f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -40,7 +40,7 @@ namespace Discord.WebSocket /// public UserStatus Status => Presence.Status; /// - public IImmutableSet ActiveClients => Presence.ActiveClients; + public IImmutableSet ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; /// /// Gets mutual guilds shared with this user. /// diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index fd91ba987..cbe575075 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -1,3 +1,5 @@ +using Discord.Rest; +using System; using System.Collections.Immutable; using System.Linq; @@ -7,6 +9,19 @@ namespace Discord.WebSocket { public static IActivity ToEntity(this API.Game model) { + // Custom Status Game + if (model.Id.IsSpecified && model.Id.Value == "custom") + { + return new CustomStatusGame() + { + Type = ActivityType.CustomStatus, + Name = model.Name, + State = model.State.IsSpecified ? model.State.Value : null, + Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, + CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), + }; + } + // Spotify Game if (model.SyncId.IsSpecified) { @@ -23,6 +38,8 @@ namespace Discord.WebSocket AlbumTitle = albumText, TrackTitle = model.Details.GetValueOrDefault(), Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(), + StartedAt = timestamps?.Start, + EndsAt = timestamps?.End, Duration = timestamps?.End - timestamps?.Start, AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, Type = ActivityType.Listening, diff --git a/src/Discord.Net.WebSocket/GatewayReconnectException.cs b/src/Discord.Net.WebSocket/GatewayReconnectException.cs new file mode 100644 index 000000000..1a8024558 --- /dev/null +++ b/src/Discord.Net.WebSocket/GatewayReconnectException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord.WebSocket +{ + /// + /// An exception thrown when the gateway client has been requested to + /// reconnect. + /// + public class GatewayReconnectException : Exception + { + /// + /// Creates a new instance of the + /// type. + /// + /// + /// The reason why the gateway has been requested to reconnect. + /// + public GatewayReconnectException(string message) + : base(message) + { } + } +} diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 36a6fea4f..4723ae57a 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -44,7 +44,7 @@ namespace Discord.Net.WebSockets { if (disposing) { - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); _disconnectTokenSource?.Dispose(); _cancelTokenSource?.Dispose(); _lock?.Dispose(); @@ -94,19 +94,19 @@ namespace Discord.Net.WebSockets _task = RunAsync(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private async Task DisconnectInternalAsync(bool isDisposing = false) + private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { try { _disconnectTokenSource.Cancel(false); } catch { } @@ -117,7 +117,8 @@ namespace Discord.Net.WebSockets { if (!isDisposing) { - try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + var status = (WebSocketCloseStatus)closeCode; + try { await _client.CloseOutputAsync(status, "", new CancellationToken()); } catch { } } try { _client.Dispose(); } @@ -141,7 +142,7 @@ namespace Discord.Net.WebSockets await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync(false); + await DisconnectInternalAsync(isDisposing: false); } finally { diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 542ec7997..9c90df565 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -85,7 +85,7 @@ namespace Discord.Webhook } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - /// Sends a message using to the channel for this webhook. + /// Sends a message to the channel for this webhook. /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 4b7717e58..5c5ea4072 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,10 +2,10 @@ Discord.Net - 2.2.0-dev$suffix$ + 2.3.0-dev$suffix$ Discord.Net Discord.Net Contributors - RogueException + foxbot An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/RogueException/Discord.Net @@ -14,25 +14,25 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 724bc84ef..c8d68fb4d 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -83,7 +83,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index 8b4e8b0d0..5a26b713f 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -91,7 +91,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ca84219fd..a57c72899 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -177,7 +177,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); }