| @@ -54,7 +54,7 @@ namespace Discord.Commands.Builders | |||||
| if (type.GetTypeInfo().IsValueType) | if (type.GetTypeInfo().IsValueType) | ||||
| DefaultValue = Activator.CreateInstance(type); | DefaultValue = Activator.CreateInstance(type); | ||||
| else if (type.IsArray) | else if (type.IsArray) | ||||
| type = ParameterType.GetElementType(); | |||||
| DefaultValue = Array.CreateInstance(type.GetElementType(), 0); | |||||
| ParameterType = type; | ParameterType = type; | ||||
| } | } | ||||
| @@ -517,19 +517,83 @@ namespace Discord.Commands | |||||
| services ??= EmptyServiceProvider.Instance; | services ??= EmptyServiceProvider.Instance; | ||||
| var searchResult = Search(input); | var searchResult = Search(input); | ||||
| if (!searchResult.IsSuccess) | |||||
| var validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); | |||||
| if (validationResult is SearchResult result) | |||||
| { | |||||
| await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, result).ConfigureAwait(false); | |||||
| return result; | |||||
| } | |||||
| if (validationResult is MatchResult matchResult) | |||||
| { | |||||
| return await HandleCommandPipeline(matchResult, context, services); | |||||
| } | |||||
| return validationResult; | |||||
| } | |||||
| private async Task<IResult> 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) | |||||
| { | |||||
| float argValuesScore = 0, paramValuesScore = 0; | |||||
| if (match.Command.Parameters.Count > 0) | |||||
| { | { | ||||
| await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false); | |||||
| return searchResult; | |||||
| 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; | |||||
| } | |||||
| var commands = searchResult.Commands; | |||||
| /// <summary> | |||||
| /// Validates and gets the best <see cref="CommandMatch"/> from a specified <see cref="SearchResult"/> | |||||
| /// </summary> | |||||
| /// <param name="matches">The SearchResult.</param> | |||||
| /// <param name="context">The context of the command.</param> | |||||
| /// <param name="provider">The service provider to be used on the command's dependency injection.</param> | |||||
| /// <param name="multiMatchHandling">The handling mode when multiple command matches are found.</param> | |||||
| /// <returns>A task that represents the asynchronous validation operation. The task result contains the result of the | |||||
| /// command validation as a <see cref="MatchResult"/> or a <see cref="SearchResult"/> if no matches were found.</returns> | |||||
| public async Task<IResult> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||||
| { | |||||
| if (!matches.IsSuccess) | |||||
| return matches; | |||||
| var commands = matches.Commands; | |||||
| var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | ||||
| foreach (var match in commands) | |||||
| foreach (var command in commands) | |||||
| { | { | ||||
| preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); | |||||
| preconditionResults[command] = await command.CheckPreconditionsAsync(context, provider); | |||||
| } | } | ||||
| var successfulPreconditions = preconditionResults | var successfulPreconditions = preconditionResults | ||||
| @@ -540,19 +604,16 @@ namespace Discord.Commands | |||||
| { | { | ||||
| //All preconditions failed, return the one from the highest priority command | //All preconditions failed, return the one from the highest priority command | ||||
| var bestCandidate = preconditionResults | 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; | |||||
| .OrderByDescending(x => x.Key.Command.Priority) | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return MatchResult.FromSuccess(bestCandidate.Key,bestCandidate.Value); | |||||
| } | } | ||||
| //If we get this far, at least one precondition was successful. | |||||
| var parseResults = new Dictionary<CommandMatch, ParseResult>(); | |||||
| var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); | |||||
| foreach (var pair in successfulPreconditions) | foreach (var pair in successfulPreconditions) | ||||
| { | { | ||||
| var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); | |||||
| var parseResult = await pair.Key.ParseAsync(context, matches, pair.Value, provider).ConfigureAwait(false); | |||||
| if (parseResult.Error == CommandError.MultipleMatches) | if (parseResult.Error == CommandError.MultipleMatches) | ||||
| { | { | ||||
| @@ -567,51 +628,27 @@ 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; | |||||
| parseResults[pair.Key] = parseResult; | |||||
| } | } | ||||
| //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 weightedParseResults = parseResults | |||||
| .OrderByDescending(x => CalculateScore(x.Key, x.Value)); | |||||
| var successfulParses = parseResults | |||||
| var successfulParses = weightedParseResults | |||||
| .Where(x => x.Value.IsSuccess) | .Where(x => x.Value.IsSuccess) | ||||
| .ToArray(); | .ToArray(); | ||||
| if (successfulParses.Length == 0) | |||||
| if(successfulParses.Length == 0) | |||||
| { | { | ||||
| //All parses failed, return the one from the highest priority command, using score as a tie breaker | |||||
| var bestMatch = parseResults | var bestMatch = parseResults | ||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | .FirstOrDefault(x => !x.Value.IsSuccess); | ||||
| await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); | |||||
| return bestMatch.Value; | |||||
| return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value); | |||||
| } | } | ||||
| //If we get this far, at least one parse was successful. Execute the most likely overload. | |||||
| var chosenOverload = successfulParses[0]; | 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)) // successful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deferred execution) | |||||
| await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); | |||||
| return result; | |||||
| return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value); | |||||
| } | } | ||||
| #endregion | #endregion | ||||
| @@ -0,0 +1,47 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public class MatchResult : IResult | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the command that may have matched during the command execution. | |||||
| /// </summary> | |||||
| public CommandMatch? Match { get; } | |||||
| /// <summary> | |||||
| /// Gets on which pipeline stage the command may have matched or failed. | |||||
| /// </summary> | |||||
| public IResult? Pipeline { get; } | |||||
| /// <inheritdoc /> | |||||
| public CommandError? Error { get; } | |||||
| /// <inheritdoc /> | |||||
| public string ErrorReason { get; } | |||||
| /// <inheritdoc /> | |||||
| 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}"; | |||||
| } | |||||
| } | |||||
| @@ -1,6 +1,6 @@ | |||||
| <Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
| <Import Project="../../Discord.Net.targets" /> | <Import Project="../../Discord.Net.targets" /> | ||||
| <Import Project="../../StyleAnalyzer.targets"/> | |||||
| <Import Project="../../StyleAnalyzer.targets" /> | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <AssemblyName>Discord.Net.Core</AssemblyName> | <AssemblyName>Discord.Net.Core</AssemblyName> | ||||
| <RootNamespace>Discord</RootNamespace> | <RootNamespace>Discord</RootNamespace> | ||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Watch youtube together. | |||||
| /// </summary> | |||||
| Youtube = 880218394199220334, | |||||
| /// <summary> | |||||
| /// Youtube development application. | |||||
| /// </summary> | |||||
| YoutubeDev = 880218832743055411, | |||||
| /// <summary> | |||||
| /// Poker! | |||||
| /// </summary> | |||||
| Poker = 755827207812677713, | |||||
| /// <summary> | |||||
| /// Betrayal: A Party Adventure. Betrayal is a social deduction game inspired by Werewolf, Town of Salem, and Among Us. | |||||
| /// </summary> | |||||
| Betrayal = 773336526917861400, | |||||
| /// <summary> | |||||
| /// Sit back, relax, and do some fishing! | |||||
| /// </summary> | |||||
| Fishing = 814288819477020702, | |||||
| /// <summary> | |||||
| /// The queens gambit. | |||||
| /// </summary> | |||||
| Chess = 832012774040141894, | |||||
| /// <summary> | |||||
| /// Development version of chess. | |||||
| /// </summary> | |||||
| ChessDev = 832012586023256104, | |||||
| /// <summary> | |||||
| /// LetterTile is a version of scrabble. | |||||
| /// </summary> | |||||
| LetterTile = 879863686565621790, | |||||
| /// <summary> | |||||
| /// Find words in a jumble of letters in coffee. | |||||
| /// </summary> | |||||
| WordSnack = 879863976006127627, | |||||
| /// <summary> | |||||
| /// It's like skribbl.io. | |||||
| /// </summary> | |||||
| DoodleCrew = 878067389634314250, | |||||
| /// <summary> | |||||
| /// It's like cards against humanity. | |||||
| /// </summary> | |||||
| Awkword = 879863881349087252, | |||||
| /// <summary> | |||||
| /// A word-search like game where you unscramble words and score points in a scrabble fashion. | |||||
| /// </summary> | |||||
| SpellCast = 852509694341283871, | |||||
| /// <summary> | |||||
| /// Classic checkers | |||||
| /// </summary> | |||||
| Checkers = 832013003968348200, | |||||
| /// <summary> | |||||
| /// The development version of poker. | |||||
| /// </summary> | |||||
| PokerDev = 763133495793942528, | |||||
| /// <summary> | |||||
| /// SketchyArtist. | |||||
| /// </summary> | |||||
| SketchyArtist = 879864070101172255 | |||||
| } | |||||
| } | |||||
| @@ -60,13 +60,6 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new invite to this channel. | /// Creates a new invite to this channel. | ||||
| /// </summary> | /// </summary> | ||||
| /// <example> | |||||
| /// <para>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.</para> | |||||
| /// <code language="cs"> | |||||
| /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="applicationId">The id of the embedded application to open for this invite.</param> | /// <param name="applicationId">The id of the embedded application to open for this invite.</param> | ||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | ||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | ||||
| @@ -79,6 +72,21 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Creates a new invite to this channel. | |||||
| /// </summary> | |||||
| /// <param name="application">The application to open for this invite.</param> | |||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | |||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | |||||
| /// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param> | |||||
| /// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous invite creation operation. The task result contains an invite | |||||
| /// metadata object containing information for the created invite. | |||||
| /// </returns> | |||||
| Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new invite to this channel. | /// Creates a new invite to this channel. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -1,4 +1,4 @@ | |||||
| using System.Collections.Immutable; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -14,10 +14,10 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the set of clients where this user is currently active. | /// Gets the set of clients where this user is currently active. | ||||
| /// </summary> | /// </summary> | ||||
| IImmutableSet<ClientType> ActiveClients { get; } | |||||
| IReadOnlyCollection<ClientType> ActiveClients { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the list of activities that this user currently has available. | /// Gets the list of activities that this user currently has available. | ||||
| /// </summary> | /// </summary> | ||||
| IImmutableList<IActivity> Activities { get; } | |||||
| IReadOnlyCollection<IActivity> Activities { get; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,7 +7,8 @@ namespace Discord | |||||
| public static class Format | public static class Format | ||||
| { | { | ||||
| // Characters which need escaping | // Characters which need escaping | ||||
| private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" }; | |||||
| private static readonly string[] SensitiveCharacters = { | |||||
| "\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" }; | |||||
| /// <summary> Returns a markdown-formatted string with bold formatting. </summary> | /// <summary> Returns a markdown-formatted string with bold formatting. </summary> | ||||
| public static string Bold(string text) => $"**{text}**"; | public static string Bold(string text) => $"**{text}**"; | ||||
| @@ -104,5 +105,15 @@ namespace Discord | |||||
| var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", ""); | var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", ""); | ||||
| return newText; | return newText; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Formats a user's username + discriminator while maintaining bidirectional unicode | |||||
| /// </summary> | |||||
| /// <param name="user">The user whos username and discriminator to format</param> | |||||
| /// <returns>The username + discriminator</returns> | |||||
| public static string UsernameAndDiscriminator(IUser user) | |||||
| { | |||||
| return $"\u2066{user.Username}\u2069#{user.Discriminator}"; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -227,8 +227,11 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public virtual async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); | ||||
| public virtual Task<IInviteMetadata> 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<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IInviteMetadata> 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<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | ||||
| => throw new NotImplementedException(); | => throw new NotImplementedException(); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -78,6 +78,9 @@ namespace Discord.Rest | |||||
| public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -5,6 +5,7 @@ using System.Globalization; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using EventUserModel = Discord.API.GuildScheduledEventUser; | using EventUserModel = Discord.API.GuildScheduledEventUser; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| @@ -41,9 +42,9 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual UserStatus Status => UserStatus.Offline; | public virtual UserStatus Status => UserStatus.Offline; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; | |||||
| public virtual IReadOnlyCollection<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty; | |||||
| public virtual IReadOnlyCollection<IActivity> Activities => ImmutableList<IActivity>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual bool IsWebhook => false; | public virtual bool IsWebhook => false; | ||||
| @@ -128,8 +129,8 @@ namespace Discord.Rest | |||||
| /// <returns> | /// <returns> | ||||
| /// A string that resolves to Username#Discriminator of the user. | /// A string that resolves to Username#Discriminator of the user. | ||||
| /// </returns> | /// </returns> | ||||
| 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 | #endregion | ||||
| #region IUser | #region IUser | ||||
| @@ -502,6 +502,18 @@ namespace Discord.WebSocket | |||||
| internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | ||||
| #endregion | #endregion | ||||
| #region Presence | |||||
| /// <summary> Fired when a users presence is updated. </summary> | |||||
| public event Func<SocketUser, SocketPresence, SocketPresence, Task> PresenceUpdated | |||||
| { | |||||
| add { _presenceUpdated.Add(value); } | |||||
| remove { _presenceUpdated.Remove(value); } | |||||
| } | |||||
| internal readonly AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>> _presenceUpdated = new AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>>(); | |||||
| #endregion | |||||
| #region Invites | #region Invites | ||||
| /// <summary> | /// <summary> | ||||
| /// Fired when an invite is created. | /// Fired when an invite is created. | ||||
| @@ -115,7 +115,7 @@ namespace Discord.WebSocket | |||||
| if (_guilds.TryRemove(id, out SocketGuild guild)) | if (_guilds.TryRemove(id, out SocketGuild guild)) | ||||
| { | { | ||||
| guild.PurgeChannelCache(this); | guild.PurgeChannelCache(this); | ||||
| guild.PurgeGuildUserCache(); | |||||
| guild.PurgeUserCache(); | |||||
| return guild; | return guild; | ||||
| } | } | ||||
| return null; | return null; | ||||
| @@ -140,7 +140,7 @@ namespace Discord.WebSocket | |||||
| internal void PurgeUsers() | internal void PurgeUsers() | ||||
| { | { | ||||
| foreach (var guild in _guilds.Values) | foreach (var guild in _guilds.Values) | ||||
| guild.PurgeGuildUserCache(); | |||||
| guild.PurgeUserCache(); | |||||
| } | } | ||||
| internal SocketApplicationCommand GetCommand(ulong id) | internal SocketApplicationCommand GetCommand(ulong id) | ||||
| @@ -76,6 +76,7 @@ namespace Discord.WebSocket | |||||
| internal int? HandlerTimeout { get; private set; } | internal int? HandlerTimeout { get; private set; } | ||||
| internal bool AlwaysDownloadDefaultStickers { get; private set; } | internal bool AlwaysDownloadDefaultStickers { get; private set; } | ||||
| internal bool AlwaysResolveStickers { get; private set; } | internal bool AlwaysResolveStickers { get; private set; } | ||||
| internal bool LogGatewayIntentWarnings { get; private set; } | |||||
| internal new DiscordSocketApiClient ApiClient => base.ApiClient; | internal new DiscordSocketApiClient ApiClient => base.ApiClient; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | ||||
| @@ -147,6 +148,7 @@ namespace Discord.WebSocket | |||||
| AlwaysDownloadUsers = config.AlwaysDownloadUsers; | AlwaysDownloadUsers = config.AlwaysDownloadUsers; | ||||
| AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; | AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; | ||||
| AlwaysResolveStickers = config.AlwaysResolveStickers; | AlwaysResolveStickers = config.AlwaysResolveStickers; | ||||
| LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; | |||||
| HandlerTimeout = config.HandlerTimeout; | HandlerTimeout = config.HandlerTimeout; | ||||
| State = new ClientState(0, 0); | State = new ClientState(0, 0); | ||||
| Rest = new DiscordSocketRestClient(config, ApiClient); | Rest = new DiscordSocketRestClient(config, ApiClient); | ||||
| @@ -238,6 +240,9 @@ namespace Discord.WebSocket | |||||
| _defaultStickers = builder.ToImmutable(); | _defaultStickers = builder.ToImmutable(); | ||||
| } | } | ||||
| if(LogGatewayIntentWarnings) | |||||
| await LogGatewayIntentsWarning().ConfigureAwait(false); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -708,6 +713,52 @@ namespace Discord.WebSocket | |||||
| game); | 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 | #region ProcessMessageAsync | ||||
| private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) | private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) | ||||
| { | { | ||||
| @@ -1858,6 +1909,8 @@ namespace Discord.WebSocket | |||||
| var data = (payload as JToken).ToObject<API.Presence>(_serializer); | var data = (payload as JToken).ToObject<API.Presence>(_serializer); | ||||
| SocketUser user = null; | |||||
| if (data.GuildId.IsSpecified) | if (data.GuildId.IsSpecified) | ||||
| { | { | ||||
| var guild = State.GetGuild(data.GuildId.Value); | var guild = State.GetGuild(data.GuildId.Value); | ||||
| @@ -1872,7 +1925,7 @@ namespace Discord.WebSocket | |||||
| return; | return; | ||||
| } | } | ||||
| var user = guild.GetUser(data.User.Id); | |||||
| user = guild.GetUser(data.User.Id); | |||||
| if (user == null) | if (user == null) | ||||
| { | { | ||||
| if (data.Status == UserStatus.Offline) | if (data.Status == UserStatus.Offline) | ||||
| @@ -1890,26 +1943,21 @@ namespace Discord.WebSocket | |||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | ||||
| } | } | ||||
| } | } | ||||
| var before = user.Clone(); | |||||
| user.Update(State, data, true); | |||||
| var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => Task.FromResult(user)); | |||||
| await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); | |||||
| } | } | ||||
| else | 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); | await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); | ||||
| return; | 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; | break; | ||||
| case "TYPING_START": | case "TYPING_START": | ||||
| @@ -183,6 +183,11 @@ namespace Discord.WebSocket | |||||
| /// </remarks> | /// </remarks> | ||||
| public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; | public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; | ||||
| /// <summary> | |||||
| /// Gets or sets whether or not to log warnings related to guild intents and events. | |||||
| /// </summary> | |||||
| public bool LogGatewayIntentWarnings { get; set; } = true; | |||||
| /// <summary> | /// <summary> | ||||
| /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -324,6 +324,9 @@ namespace Discord.WebSocket | |||||
| public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public virtual async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public virtual async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -100,6 +100,9 @@ namespace Discord.WebSocket | |||||
| public async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public async Task<IInviteMetadata> 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); | => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -1144,22 +1144,29 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal void PurgeGuildUserCache() | |||||
| /// <summary> | |||||
| /// Purges this guild's user cache. | |||||
| /// </summary> | |||||
| public void PurgeUserCache() => PurgeUserCache(_ => true); | |||||
| /// <summary> | |||||
| /// Purges this guild's user cache. | |||||
| /// </summary> | |||||
| /// <param name="predicate">The predicate used to select which users to clear.</param> | |||||
| public void PurgeUserCache(Func<SocketGuildUser, bool> 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<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
| DownloadedMemberCount = _members.Count; | DownloadedMemberCount = _members.Count; | ||||
| foreach (var member in members) | |||||
| { | |||||
| if (member.Id != self?.Id) | |||||
| member.GlobalUser.RemoveRef(Discord); | |||||
| } | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -1,7 +1,6 @@ | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord.WebSocket | 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)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | ||||
| internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | ||||
| } | } | ||||
| @@ -164,8 +164,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (updatePresence) | if (updatePresence) | ||||
| { | { | ||||
| Presence = SocketPresence.Create(model); | |||||
| GlobalUser.Update(state, model); | |||||
| Update(model); | |||||
| } | } | ||||
| if (model.Nick.IsSpecified) | if (model.Nick.IsSpecified) | ||||
| Nickname = model.Nick.Value; | Nickname = model.Nick.Value; | ||||
| @@ -174,6 +173,13 @@ namespace Discord.WebSocket | |||||
| if (model.PremiumSince.IsSpecified) | if (model.PremiumSince.IsSpecified) | ||||
| _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | ||||
| } | } | ||||
| internal override void Update(PresenceModel model) | |||||
| { | |||||
| Presence.Update(model); | |||||
| GlobalUser.Update(model); | |||||
| } | |||||
| private void UpdateRoles(ulong[] roleIds) | private void UpdateRoles(ulong[] roleIds) | ||||
| { | { | ||||
| var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | ||||
| @@ -11,26 +11,37 @@ namespace Discord.WebSocket | |||||
| /// Represents the WebSocket user's presence status. This may include their online status and their activity. | /// Represents the WebSocket user's presence status. This may include their online status and their activity. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public struct SocketPresence : IPresence | |||||
| public class SocketPresence : IPresence | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status { get; } | |||||
| public UserStatus Status { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableSet<ClientType> ActiveClients { get; } | |||||
| public IReadOnlyCollection<ClientType> ActiveClients { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableList<IActivity> Activities { get; } | |||||
| public IReadOnlyCollection<IActivity> Activities { get; private set; } | |||||
| internal SocketPresence() { } | |||||
| internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | ||||
| { | { | ||||
| Status = status; | Status = status; | ||||
| ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | ||||
| Activities = activities ?? ImmutableList<IActivity>.Empty; | Activities = activities ?? ImmutableList<IActivity>.Empty; | ||||
| } | } | ||||
| internal static SocketPresence Create(Model model) | 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<ClientType>.Empty; | |||||
| Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types | /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types | ||||
| /// where a user is active from the data supplied in the Presence update frame. | /// where a user is active from the data supplied in the Presence update frame. | ||||
| @@ -42,7 +53,7 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A collection of all <see cref="ClientType"/>s that this user is active. | /// A collection of all <see cref="ClientType"/>s that this user is active. | ||||
| /// </returns> | /// </returns> | ||||
| private static IImmutableSet<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict) | |||||
| private static IReadOnlyCollection<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict) | |||||
| { | { | ||||
| if (clientTypesDict == null || clientTypesDict.Count == 0) | if (clientTypesDict == null || clientTypesDict.Count == 0) | ||||
| return ImmutableHashSet<ClientType>.Empty; | return ImmutableHashSet<ClientType>.Empty; | ||||
| @@ -84,6 +95,6 @@ namespace Discord.WebSocket | |||||
| public override string ToString() => Status.ToString(); | public override string ToString() => Status.ToString(); | ||||
| private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | ||||
| internal SocketPresence Clone() => this; | |||||
| internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,6 +7,7 @@ using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -40,9 +41,9 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status => Presence.Status; | public UserStatus Status => Presence.Status; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets mutual guilds shared with this user. | /// Gets mutual guilds shared with this user. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -91,6 +92,11 @@ namespace Discord.WebSocket | |||||
| return hasChanges; | return hasChanges; | ||||
| } | } | ||||
| internal virtual void Update(PresenceModel model) | |||||
| { | |||||
| Presence.Update(model); | |||||
| } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | ||||
| => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | ||||
| @@ -109,8 +115,8 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// The full name of the user. | /// The full name of the user. | ||||
| /// </returns> | /// </returns> | ||||
| 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; | internal SocketUser Clone() => MemberwiseClone() as SocketUser; | ||||
| } | } | ||||
| } | } | ||||
| @@ -214,5 +214,6 @@ namespace Discord | |||||
| public Task<IUserMessage> 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<IUserMessage> 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<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> 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<IUserMessage> SendFilesAsync(IEnumerable<FileAttachment> 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<IThreadChannel> 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<IThreadChannel> 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<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -50,6 +50,7 @@ namespace Discord | |||||
| } | } | ||||
| public Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | ||||
| => throw new NotImplementedException(); | => throw new NotImplementedException(); | ||||
| public Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); | |||||
| public Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | public Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | ||||
| => throw new NotImplementedException(); | => throw new NotImplementedException(); | ||||