From 574b503e9e87caee54c6df8143ada14268f4dcbb Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 13:38:00 -0300 Subject: [PATCH 01/15] Moves CalculateScore function to outer scope. --- src/Discord.Net.Commands/CommandService.cs | 37 +++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 1d4b0e15a..15f0c866d 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -511,7 +511,6 @@ namespace Discord.Commands await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); return searchResult; } - var commands = searchResult.Commands; var preconditionResults = new Dictionary(); @@ -559,24 +558,6 @@ namespace Discord.Commands parseResultsDict[pair.Key] = parseResult; } - // Calculates the 'score' of a command given a parse result - float CalculateScore(CommandMatch match, ParseResult parseResult) - { - float argValuesScore = 0, paramValuesScore = 0; - - if (match.Command.Parameters.Count > 0) - { - var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; - var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; - - argValuesScore = argValuesSum / match.Command.Parameters.Count; - paramValuesScore = paramValuesSum / match.Command.Parameters.Count; - } - - var totalArgsScore = (argValuesScore + paramValuesScore) / 2; - return match.Command.Priority + totalArgsScore * 0.99f; - } - //Order the parse results by their score so that we choose the most likely result to execute var parseResults = parseResultsDict .OrderByDescending(x => CalculateScore(x.Key, x.Value)); @@ -603,6 +584,24 @@ namespace Discord.Commands return result; } + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + } + + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } + protected virtual void Dispose(bool disposing) { if (!_isDisposed) From 7955a0909043b964875796142bcfc5d9997e4798 Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 13:52:53 -0300 Subject: [PATCH 02/15] Creates ValidateAndGetBestMatch function This function will validate all commands from a SearchResult and return the result of said validation, along with the command matched, if a valid match was found. --- src/Discord.Net.Commands/CommandService.cs | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 15f0c866d..be83b955f 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -602,6 +602,83 @@ namespace Discord.Commands return match.Command.Priority + totalArgsScore * 0.99f; } + /// + /// Validates and gets the best from a specified + /// + /// The SearchResult. + /// The context of the command. + /// The service provider to be used on the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// A task that represents the asynchronous validation operation. The task result contains the result of the + /// command validation and the command matched, if a match was found. + public async Task<(IResult, Optional)> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + { + if (!matches.IsSuccess) + return (matches, Optional.Create()); + + var commands = matches.Commands; + var preconditionResults = new Dictionary(); + + foreach (var command in commands) + { + preconditionResults[command] = await command.CheckPreconditionsAsync(context, provider); + } + + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulPreconditions.Length == 0) + { + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + + return (bestCandidate.Value, bestCandidate.Key); + } + + var parseResults = new Dictionary(); + + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, matches, pair.Value, provider).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) + { + IReadOnlyList argList, paramList; + switch (multiMatchHandling) + { + case MultiMatchHandling.Best: + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + parseResult = ParseResult.FromSuccess(argList, paramList); + break; + } + } + + parseResults[pair.Key] = parseResult; + } + + var weightedParseResults = parseResults + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + + var successfulParses = weightedParseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if(successfulParses.Length == 0) + { + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + + return (bestMatch.Value, bestMatch.Key); + } + + var chosenOverload = successfulParses[0]; + + return (chosenOverload.Value, chosenOverload.Key); + } + protected virtual void Dispose(bool disposing) { if (!_isDisposed) From c455b5033123825c639d8f18e52033b69b5595ba Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 14:10:39 -0300 Subject: [PATCH 03/15] Make use of new ValidateAndGetBestMatch api on ExecuteAsync --- src/Discord.Net.Commands/CommandService.cs | 80 ++++------------------ 1 file changed, 12 insertions(+), 68 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index be83b955f..6fbf0a9bd 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -506,82 +506,26 @@ namespace Discord.Commands services = services ?? EmptyServiceProvider.Instance; var searchResult = Search(input); - if (!searchResult.IsSuccess) - { - await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); - return searchResult; - } - var commands = searchResult.Commands; - var preconditionResults = new Dictionary(); - - foreach (var match in commands) - { - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); - } - - var successfulPreconditions = preconditionResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + //Since ValidateAndGetBestMatch is deterministic on the return type, we can use pattern matching on the type for infering the code flow. + var (validationResult, commandMatch) = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); - if (successfulPreconditions.Length == 0) + if(validationResult is SearchResult) { - //All preconditions failed, return the one from the highest priority command - var bestCandidate = preconditionResults - .OrderByDescending(x => x.Key.Command.Priority) - .FirstOrDefault(x => !x.Value.IsSuccess); - - await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); - return bestCandidate.Value; + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); } - - //If we get this far, at least one precondition was successful. - - var parseResultsDict = new Dictionary(); - foreach (var pair in successfulPreconditions) + else if(validationResult is not ParseResult parseResult) { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); - - if (parseResult.Error == CommandError.MultipleMatches) - { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) - { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; - } - } - - parseResultsDict[pair.Key] = parseResult; + await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command,context,validationResult).ConfigureAwait(false); } - - //Order the parse results by their score so that we choose the most likely result to execute - var parseResults = parseResultsDict - .OrderByDescending(x => CalculateScore(x.Key, x.Value)); - - var successfulParses = parseResults - .Where(x => x.Value.IsSuccess) - .ToArray(); - - if (successfulParses.Length == 0) + else { - //All parses failed, return the one from the highest priority command, using score as a tie breaker - var bestMatch = parseResults - .FirstOrDefault(x => !x.Value.IsSuccess); - - await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); - return bestMatch.Value; + var result = await commandMatch.Value.Command.ExecuteAsync(context, parseResult, services).ConfigureAwait(false); + if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, result); + return result; } - - //If we get this far, at least one parse was successful. Execute the most likely overload. - var chosenOverload = successfulParses[0]; - var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); - if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) - await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); - return result; + return validationResult; } // Calculates the 'score' of a command given a parse result From 56d16397f7da77df31b6e1e2a1fd1a318c081cfd Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 18:42:23 -0300 Subject: [PATCH 04/15] Fixes Azure linux build failing due to a CS8652. --- src/Discord.Net.Commands/CommandService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 6fbf0a9bd..f54072811 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -514,17 +514,17 @@ namespace Discord.Commands { await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); } - else if(validationResult is not ParseResult parseResult) - { - await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command,context,validationResult).ConfigureAwait(false); - } - else + else if(validationResult is ParseResult parseResult) { var result = await commandMatch.Value.Command.ExecuteAsync(context, parseResult, services).ConfigureAwait(false); if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, result); return result; } + else + { + await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, validationResult).ConfigureAwait(false); + } return validationResult; } From d5f5ae132ca4cd9a6bd156cd4bc57cca354c6afd Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Thu, 25 Nov 2021 18:22:50 +0300 Subject: [PATCH 05/15] fix sharded client current user (#1947) --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 307f9a009..1e71ce853 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -495,9 +495,12 @@ namespace Discord.WebSocket client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2); client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2); } -#endregion + #endregion #region IDiscordClient + /// + ISelfUser IDiscordClient.CurrentUser => CurrentUser; + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); From 143ca6db437ac18d4615c0752e00464b566f6264 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu, 25 Nov 2021 11:23:33 -0400 Subject: [PATCH 06/15] fix NRE when adding parameters thru builders (#1946) --- src/Discord.Net.Commands/Builders/ParameterBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 9ee1a748c..afe3a5af6 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -54,7 +54,7 @@ namespace Discord.Commands.Builders if (type.GetTypeInfo().IsValueType) DefaultValue = Activator.CreateInstance(type); else if (type.IsArray) - type = ParameterType.GetElementType(); + DefaultValue = Array.CreateInstance(type.GetElementType(), 0); ParameterType = type; } From 10afd96e6ec6dcb2bba8c1f5d54a696f60bac989 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu, 25 Nov 2021 11:24:44 -0400 Subject: [PATCH 07/15] feature: Handle bidirectional usernames (#1943) * Initial implementation * Update summary --- src/Discord.Net.Core/Format.cs | 10 ++++++++++ src/Discord.Net.Rest/Entities/Users/RestUser.cs | 4 ++-- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index 63f9d15a6..73be20108 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -104,5 +104,15 @@ namespace Discord var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", ""); return newText; } + + /// + /// Formats a user's username + discriminator while maintaining bidirectional unicode + /// + /// The user whos username and discriminator to format + /// The username + discriminator + public static string UsernameAndDiscriminator(IUser user) + { + return $"\u2066{user.Username}\u2069#{user.Discriminator}"; + } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 872bab392..9cf42814c 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -128,8 +128,8 @@ namespace Discord.Rest /// /// A string that resolves to Username#Discriminator of the user. /// - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + public override string ToString() => Format.UsernameAndDiscriminator(this); + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; #endregion #region IUser diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 025daf29a..5e5e5cf0c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -109,8 +109,8 @@ namespace Discord.WebSocket /// /// The full name of the user. /// - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + public override string ToString() => Format.UsernameAndDiscriminator(this); + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; } } From 9d6dc6279db499c48811e80e9c385f7015d00333 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu, 25 Nov 2021 11:25:19 -0400 Subject: [PATCH 08/15] Update socket presence and add new presence event (#1945) --- .../Entities/Users/IPresence.cs | 6 ++-- .../Entities/Users/RestUser.cs | 5 ++-- .../BaseSocketClient.Events.cs | 12 ++++++++ .../DiscordSocketClient.cs | 23 +++++++-------- .../Entities/Users/SocketGlobalUser.cs | 6 ---- .../Entities/Users/SocketGuildUser.cs | 10 +++++-- .../Entities/Users/SocketPresence.cs | 29 +++++++++++++------ .../Entities/Users/SocketUser.cs | 10 +++++-- 8 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 6972037f0..45babf481 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Generic; namespace Discord { @@ -14,10 +14,10 @@ namespace Discord /// /// Gets the set of clients where this user is currently active. /// - IImmutableSet ActiveClients { get; } + IReadOnlyCollection ActiveClients { get; } /// /// Gets the list of activities that this user currently has available. /// - IImmutableList Activities { get; } + IReadOnlyCollection Activities { get; } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 9cf42814c..70f990fe7 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Threading.Tasks; using Model = Discord.API.User; using EventUserModel = Discord.API.GuildScheduledEventUser; +using System.Collections.Generic; namespace Discord.Rest { @@ -41,9 +42,9 @@ namespace Discord.Rest /// public virtual UserStatus Status => UserStatus.Offline; /// - public virtual IImmutableSet ActiveClients => ImmutableHashSet.Empty; + public virtual IReadOnlyCollection ActiveClients => ImmutableHashSet.Empty; /// - public virtual IImmutableList Activities => ImmutableList.Empty; + public virtual IReadOnlyCollection Activities => ImmutableList.Empty; /// public virtual bool IsWebhook => false; diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 4ad25d4d5..91fb24021 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -502,6 +502,18 @@ namespace Discord.WebSocket internal readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); #endregion + #region Presence + + /// Fired when a users presence is updated. + public event Func PresenceUpdated + { + add { _presenceUpdated.Add(value); } + remove { _presenceUpdated.Remove(value); } + } + internal readonly AsyncEvent> _presenceUpdated = new AsyncEvent>(); + + #endregion + #region Invites /// /// Fired when an invite is created. diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 03c85ffc7..444e69a26 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1858,6 +1858,8 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); + SocketUser user = null; + if (data.GuildId.IsSpecified) { var guild = State.GetGuild(data.GuildId.Value); @@ -1872,7 +1874,7 @@ namespace Discord.WebSocket return; } - var user = guild.GetUser(data.User.Id); + user = guild.GetUser(data.User.Id); if (user == null) { if (data.Status == UserStatus.Offline) @@ -1890,26 +1892,21 @@ namespace Discord.WebSocket await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); } } - - var before = user.Clone(); - user.Update(State, data, true); - var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(user)); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { - var globalUser = State.GetUser(data.User.Id); - if (globalUser == null) + user = State.GetUser(data.User.Id); + if (user == null) { await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); return; } - - var before = globalUser.Clone(); - globalUser.Update(State, data.User); - globalUser.Update(State, data); - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); } + + var before = user.Presence.Clone(); + user.Update(State, data.User); + user.Update(data); + await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); } break; case "TYPING_START": diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 3a1ad23b6..525ae0b34 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using System.Linq; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -48,11 +47,6 @@ namespace Discord.WebSocket } } - internal void Update(ClientState state, PresenceModel model) - { - Presence = SocketPresence.Create(model); - } - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 147456cb0..ae3319227 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -164,8 +164,7 @@ namespace Discord.WebSocket { if (updatePresence) { - Presence = SocketPresence.Create(model); - GlobalUser.Update(state, model); + Update(model); } if (model.Nick.IsSpecified) Nickname = model.Nick.Value; @@ -174,6 +173,13 @@ namespace Discord.WebSocket if (model.PremiumSince.IsSpecified) _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; } + + internal override void Update(PresenceModel model) + { + Presence.Update(model); + GlobalUser.Update(model); + } + private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index fe672a4d6..5250e15ad 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -11,26 +11,37 @@ namespace Discord.WebSocket /// Represents the WebSocket user's presence status. This may include their online status and their activity. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct SocketPresence : IPresence + public class SocketPresence : IPresence { /// - public UserStatus Status { get; } + public UserStatus Status { get; private set; } /// - public IImmutableSet ActiveClients { get; } + public IReadOnlyCollection ActiveClients { get; private set; } /// - public IImmutableList Activities { get; } + public IReadOnlyCollection Activities { get; private set; } + + internal SocketPresence() { } internal SocketPresence(UserStatus status, IImmutableSet activeClients, IImmutableList activities) { Status = status; ActiveClients = activeClients ?? ImmutableHashSet.Empty; Activities = activities ?? ImmutableList.Empty; } + internal static SocketPresence Create(Model model) { - var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); - var activities = ConvertActivitiesList(model.Activities); - return new SocketPresence(model.Status, clients, activities); + var entity = new SocketPresence(); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Status = model.Status; + ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray.Empty; + Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray.Empty; } + /// /// Creates a new containing all of the client types /// where a user is active from the data supplied in the Presence update frame. @@ -42,7 +53,7 @@ namespace Discord.WebSocket /// /// A collection of all s that this user is active. /// - private static IImmutableSet ConvertClientTypesDict(IDictionary clientTypesDict) + private static IReadOnlyCollection ConvertClientTypesDict(IDictionary clientTypesDict) { if (clientTypesDict == null || clientTypesDict.Count == 0) return ImmutableHashSet.Empty; @@ -84,6 +95,6 @@ namespace Discord.WebSocket public override string ToString() => Status.ToString(); private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; - internal SocketPresence Clone() => this; + internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 5e5e5cf0c..b38bd8a4a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Discord.Rest; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -40,9 +41,9 @@ namespace Discord.WebSocket /// public UserStatus Status => Presence.Status; /// - public IImmutableSet ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; + public IReadOnlyCollection ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; /// - public IImmutableList Activities => Presence.Activities ?? ImmutableList.Empty; + public IReadOnlyCollection Activities => Presence.Activities ?? ImmutableList.Empty; /// /// Gets mutual guilds shared with this user. /// @@ -91,6 +92,11 @@ namespace Discord.WebSocket return hasChanges; } + internal virtual void Update(PresenceModel model) + { + Presence.Update(model); + } + /// public async Task CreateDMChannelAsync(RequestOptions options = null) => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); From d1b31c8f529033afda8d4a9e320788de0d034693 Mon Sep 17 00:00:00 2001 From: roridev Date: Thu, 25 Nov 2021 15:31:48 -0300 Subject: [PATCH 09/15] Add `MatchResult` --- .../Results/MatchResult.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Discord.Net.Commands/Results/MatchResult.cs diff --git a/src/Discord.Net.Commands/Results/MatchResult.cs b/src/Discord.Net.Commands/Results/MatchResult.cs new file mode 100644 index 000000000..fb266efa6 --- /dev/null +++ b/src/Discord.Net.Commands/Results/MatchResult.cs @@ -0,0 +1,47 @@ +using System; + +namespace Discord.Commands +{ + public class MatchResult : IResult + { + /// + /// Gets the command that may have matched during the command execution. + /// + public CommandMatch? Match { get; } + + /// + /// Gets on which pipeline stage the command may have matched or failed. + /// + public IResult? Pipeline { get; } + + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + /// + public bool IsSuccess => !Error.HasValue; + + private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) + { + Match = match; + Error = error; + Pipeline = pipeline; + ErrorReason = errorReason; + } + + public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) + => new MatchResult(match,pipeline,null, null); + public static MatchResult FromError(CommandError error, string reason) + => new MatchResult(null,null,error, reason); + public static MatchResult FromError(Exception ex) + => FromError(CommandError.Exception, ex.Message); + public static MatchResult FromError(IResult result) + => new MatchResult(null, null,result.Error, result.ErrorReason); + public static MatchResult FromError(IResult pipeline, CommandError error, string reason) + => new MatchResult(null, pipeline, error, reason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + + } +} From a92ec56d884ffe41f74c1f6b9541c1d70d6d823d Mon Sep 17 00:00:00 2001 From: roridev Date: Thu, 25 Nov 2021 16:42:18 -0300 Subject: [PATCH 10/15] Add requested changes Changes: - Use IResult instead of Optional CommandMatch - Rework branching workflow --- src/Discord.Net.Commands/CommandService.cs | 55 ++++++++++++++-------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f54072811..18f553559 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -507,27 +507,44 @@ namespace Discord.Commands var searchResult = Search(input); - //Since ValidateAndGetBestMatch is deterministic on the return type, we can use pattern matching on the type for infering the code flow. - var (validationResult, commandMatch) = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); + var validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); - if(validationResult is SearchResult) + if (validationResult is SearchResult result) { - await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); - } - else if(validationResult is ParseResult parseResult) - { - var result = await commandMatch.Value.Command.ExecuteAsync(context, parseResult, services).ConfigureAwait(false); - if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) - await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, result); + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, result).ConfigureAwait(false); return result; } - else + + if (validationResult is MatchResult matchResult) { - await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, validationResult).ConfigureAwait(false); + return await handleCommandPipeline(matchResult, context, services); } + return validationResult; } + private async Task handleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) + { + if (!matchResult.IsSuccess) + return matchResult; + + if (matchResult.Pipeline is ParseResult parseResult) + { + var executeResult = await matchResult.Match.Value.ExecuteAsync(context, parseResult, services); + + if (!executeResult.IsSuccess && !(executeResult is RuntimeResult || executeResult is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, executeResult); + return executeResult; + } + + if (matchResult.Pipeline is PreconditionResult preconditionResult) + { + await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false); + } + + return matchResult; + } + // Calculates the 'score' of a command given a parse result float CalculateScore(CommandMatch match, ParseResult parseResult) { @@ -554,11 +571,11 @@ namespace Discord.Commands /// The service provider to be used on the command's dependency injection. /// The handling mode when multiple command matches are found. /// A task that represents the asynchronous validation operation. The task result contains the result of the - /// command validation and the command matched, if a match was found. - public async Task<(IResult, Optional)> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + /// command validation as a or a if no matches were found. + public async Task ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { if (!matches.IsSuccess) - return (matches, Optional.Create()); + return matches; var commands = matches.Commands; var preconditionResults = new Dictionary(); @@ -574,11 +591,11 @@ namespace Discord.Commands if (successfulPreconditions.Length == 0) { + //All preconditions failed, return the one from the highest priority command var bestCandidate = preconditionResults .OrderByDescending(x => x.Key.Command.Priority) .FirstOrDefault(x => !x.Value.IsSuccess); - - return (bestCandidate.Value, bestCandidate.Key); + return MatchResult.FromSuccess(bestCandidate.Key,bestCandidate.Value); } var parseResults = new Dictionary(); @@ -615,12 +632,12 @@ namespace Discord.Commands var bestMatch = parseResults .FirstOrDefault(x => !x.Value.IsSuccess); - return (bestMatch.Value, bestMatch.Key); + return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value); } var chosenOverload = successfulParses[0]; - return (chosenOverload.Value, chosenOverload.Key); + return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value); } protected virtual void Dispose(bool disposing) From adf3a9c459958254cd7241acf27a8c2afbeea822 Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 26 Nov 2021 09:26:53 -0300 Subject: [PATCH 11/15] Fix incorrect casing on `HandleCommandPipeline` --- src/Discord.Net.Commands/CommandService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 18f553559..7d03c4059 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -517,13 +517,13 @@ namespace Discord.Commands if (validationResult is MatchResult matchResult) { - return await handleCommandPipeline(matchResult, context, services); + return await HandleCommandPipeline(matchResult, context, services); } return validationResult; } - private async Task handleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) + private async Task HandleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) { if (!matchResult.IsSuccess) return matchResult; From 82276e351aa306920e6a0ea7535c81428dd2003c Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri, 26 Nov 2021 11:29:53 -0400 Subject: [PATCH 12/15] feature: default application games (#1949) * Initial implementation * Add missing summary --- src/Discord.Net.Core/Discord.Net.Core.csproj | 2 +- .../Activities/DefaultApplications.cs | 86 +++++++++++++++++++ .../Entities/Channels/INestedChannel.cs | 22 +++-- .../Entities/Channels/RestTextChannel.cs | 7 +- .../Entities/Channels/RestVoiceChannel.cs | 3 + .../Entities/Channels/SocketTextChannel.cs | 3 + .../Entities/Channels/SocketVoiceChannel.cs | 3 + .../MockedEntities/MockedTextChannel.cs | 1 + .../MockedEntities/MockedVoiceChannel.cs | 1 + 9 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 29868e1c7..7dc55b1cf 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,6 +1,6 @@  - + Discord.Net.Core Discord diff --git a/src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs b/src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs new file mode 100644 index 000000000..80f128fa8 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum DefaultApplications : ulong + { + /// + /// Watch youtube together. + /// + Youtube = 880218394199220334, + + /// + /// Youtube development application. + /// + YoutubeDev = 880218832743055411, + + /// + /// Poker! + /// + Poker = 755827207812677713, + + /// + /// Betrayal: A Party Adventure. Betrayal is a social deduction game inspired by Werewolf, Town of Salem, and Among Us. + /// + Betrayal = 773336526917861400, + + /// + /// Sit back, relax, and do some fishing! + /// + Fishing = 814288819477020702, + + /// + /// The queens gambit. + /// + Chess = 832012774040141894, + + /// + /// Development version of chess. + /// + ChessDev = 832012586023256104, + + /// + /// LetterTile is a version of scrabble. + /// + LetterTile = 879863686565621790, + + /// + /// Find words in a jumble of letters in coffee. + /// + WordSnack = 879863976006127627, + + /// + /// It's like skribbl.io. + /// + DoodleCrew = 878067389634314250, + + /// + /// It's like cards against humanity. + /// + Awkword = 879863881349087252, + + /// + /// A word-search like game where you unscramble words and score points in a scrabble fashion. + /// + SpellCast = 852509694341283871, + + /// + /// Classic checkers + /// + Checkers = 832013003968348200, + + /// + /// The development version of poker. + /// + PokerDev = 763133495793942528, + + /// + /// SketchyArtist. + /// + SketchyArtist = 879864070101172255 + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index 563acd4f8..511d2bf51 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -60,13 +60,6 @@ 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. - /// - /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); - /// - /// /// The id of the embedded application to open for this invite. /// The time (in seconds) until the invite expires. Set to null to never expire. /// The max amount of times this invite may be used. Set to null to have unlimited uses. @@ -79,6 +72,21 @@ namespace Discord /// Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + /// + /// Creates a new invite to this channel. + /// + /// The application to open for this invite. + /// The time (in seconds) until the invite expires. Set to null to never expire. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. + /// If true, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + /// /// Creates a new invite to this channel. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index f14bc2ecf..57de0eb45 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -227,8 +227,11 @@ namespace Discord.Rest /// public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public virtual Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 48fc11dcb..239c00467 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -78,6 +78,9 @@ namespace Discord.Rest public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + /// public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 8722b569d..aea1bfda5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -324,6 +324,9 @@ namespace Discord.WebSocket public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + /// public virtual async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index e57051e80..08b976bfe 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -100,6 +100,9 @@ namespace Discord.WebSocket public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + /// public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); /// diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index 6d08a4478..ad0af04b2 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -214,5 +214,6 @@ namespace Discord public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index 61a32e391..4514dfc97 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -50,6 +50,7 @@ namespace Discord } public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); From 51e06e9ce190ac46aeaeb8650d71d58eba0b5412 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri, 26 Nov 2021 11:30:19 -0400 Subject: [PATCH 13/15] feature: warn on invalid gateway intents (#1948) --- .../DiscordSocketClient.cs | 51 +++++++++++++++++++ .../DiscordSocketConfig.cs | 5 ++ 2 files changed, 56 insertions(+) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 444e69a26..9ef827778 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -76,6 +76,7 @@ namespace Discord.WebSocket internal int? HandlerTimeout { get; private set; } internal bool AlwaysDownloadDefaultStickers { get; private set; } internal bool AlwaysResolveStickers { get; private set; } + internal bool LogGatewayIntentWarnings { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// public override IReadOnlyCollection Guilds => State.Guilds; @@ -147,6 +148,7 @@ namespace Discord.WebSocket AlwaysDownloadUsers = config.AlwaysDownloadUsers; AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; AlwaysResolveStickers = config.AlwaysResolveStickers; + LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); Rest = new DiscordSocketRestClient(config, ApiClient); @@ -238,6 +240,9 @@ namespace Discord.WebSocket _defaultStickers = builder.ToImmutable(); } + + if(LogGatewayIntentWarnings) + await LogGatewayIntentsWarning().ConfigureAwait(false); } /// @@ -708,6 +713,52 @@ namespace Discord.WebSocket game); } + private async Task LogGatewayIntentsWarning() + { + if(_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && !_presenceUpdated.HasSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the GuildPresences intent without listening to the PresenceUpdate event, consider removing the intent from your config.").ConfigureAwait(false); + } + + if(!_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && _presenceUpdated.HasSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the PresenceUpdate event without specifying the GuildPresences intent, consider adding the intent to your config.").ConfigureAwait(false); + } + + bool hasGuildScheduledEventsSubscribers = + _guildScheduledEventCancelled.HasSubscribers || + _guildScheduledEventUserRemove.HasSubscribers || + _guildScheduledEventCompleted.HasSubscribers || + _guildScheduledEventCreated.HasSubscribers || + _guildScheduledEventStarted.HasSubscribers || + _guildScheduledEventUpdated.HasSubscribers || + _guildScheduledEventUserAdd.HasSubscribers; + + if(_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && !hasGuildScheduledEventsSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the GuildScheduledEvents gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false); + } + + if(!_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && hasGuildScheduledEventsSubscribers) + { + await _gatewayLogger.WarningAsync("You're using events related to the GuildScheduledEvents gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false); + } + + bool hasInviteEventSubscribers = + _inviteCreatedEvent.HasSubscribers || + _inviteDeletedEvent.HasSubscribers; + + if (_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && !hasInviteEventSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the GuildInvites gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false); + } + + if (!_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && hasInviteEventSubscribers) + { + await _gatewayLogger.WarningAsync("You're using events related to the GuildInvites gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false); + } + } + #region ProcessMessageAsync private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 8615eac71..f0e6dc857 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -183,6 +183,11 @@ namespace Discord.WebSocket /// public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; + /// + /// Gets or sets whether or not to log warnings related to guild intents and events. + /// + public bool LogGatewayIntentWarnings { get; set; } = true; + /// /// Initializes a new instance of the class with the default configuration. /// From b9274d115dfcec28a732fd3c9122ca65ebe4951e Mon Sep 17 00:00:00 2001 From: Monica S Date: Fri, 26 Nov 2021 15:41:08 +0000 Subject: [PATCH 14/15] Add characters commonly use in links to Sanitize (#1152) Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/Format.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index 73be20108..a5951aa73 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -7,7 +7,8 @@ namespace Discord public static class Format { // Characters which need escaping - private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" }; + private static readonly string[] SensitiveCharacters = { + "\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" }; /// Returns a markdown-formatted string with bold formatting. public static string Bold(string text) => $"**{text}**"; From 19a66bf8782e7b8879c6b8b372134e01be0090d7 Mon Sep 17 00:00:00 2001 From: Daniel Baynton <49287178+230Daniel@users.noreply.github.com> Date: Fri, 26 Nov 2021 15:41:55 +0000 Subject: [PATCH 15/15] feature: Add method to clear guild user cache (#1767) * Add method to clear a SocketGuild's user cache * Add optional predicate * Compress overload to be consistant * Fix global user not clearing (may cause other issues) * Remove debug code and add param documentation * Standardise doc string * Remove old hack-fix * Rename new method for consistency * Add missing line to reset downloaderPromise * Undo accidental whitespace changes * Rider better actually keep the tab this time Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.WebSocket/ClientState.cs | 4 +-- .../Entities/Guilds/SocketGuild.cs | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index 7129feb48..c40ae3f92 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -115,7 +115,7 @@ namespace Discord.WebSocket if (_guilds.TryRemove(id, out SocketGuild guild)) { guild.PurgeChannelCache(this); - guild.PurgeGuildUserCache(); + guild.PurgeUserCache(); return guild; } return null; @@ -140,7 +140,7 @@ namespace Discord.WebSocket internal void PurgeUsers() { foreach (var guild in _guilds.Values) - guild.PurgeGuildUserCache(); + guild.PurgeUserCache(); } internal SocketApplicationCommand GetCommand(ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 03c655a34..697d5fe82 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1144,22 +1144,29 @@ namespace Discord.WebSocket } return null; } - internal void PurgeGuildUserCache() + + /// + /// Purges this guild's user cache. + /// + public void PurgeUserCache() => PurgeUserCache(_ => true); + /// + /// Purges this guild's user cache. + /// + /// The predicate used to select which users to clear. + public void PurgeUserCache(Func predicate) { - var members = Users; - var self = CurrentUser; - _members.Clear(); - if (self != null) - _members.TryAdd(self.Id, self); + var membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); + var membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); + + foreach (var member in membersToPurge) + if(_members.TryRemove(member.Id, out _)) + member.GlobalUser.RemoveRef(Discord); + + foreach (var member in membersToKeep) + _members.TryAdd(member.Id, member); _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = _members.Count; - - foreach (var member in members) - { - if (member.Id != self?.Id) - member.GlobalUser.RemoveRef(Discord); - } } ///