Browse Source

Merge branch 'discord-net:dev' into dev

pull/1950/head
Cenk Ergen GitHub 3 years ago
parent
commit
d4a4c9accd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 406 additions and 113 deletions
  1. +1
    -1
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  2. +83
    -46
      src/Discord.Net.Commands/CommandService.cs
  3. +47
    -0
      src/Discord.Net.Commands/Results/MatchResult.cs
  4. +1
    -1
      src/Discord.Net.Core/Discord.Net.Core.csproj
  5. +86
    -0
      src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs
  6. +15
    -7
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  7. +3
    -3
      src/Discord.Net.Core/Entities/Users/IPresence.cs
  8. +12
    -1
      src/Discord.Net.Core/Format.cs
  9. +5
    -2
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  10. +3
    -0
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  11. +5
    -4
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  12. +12
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  13. +2
    -2
      src/Discord.Net.WebSocket/ClientState.cs
  14. +61
    -13
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  15. +5
    -0
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  16. +3
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  17. +3
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  18. +19
    -12
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  19. +0
    -6
      src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
  20. +8
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  21. +20
    -9
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  22. +10
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  23. +1
    -0
      test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs
  24. +1
    -0
      test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs

+ 1
- 1
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -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;
}



+ 83
- 46
src/Discord.Net.Commands/CommandService.cs View File

@@ -517,19 +517,83 @@ namespace Discord.Commands
services ??= EmptyServiceProvider.Instance;

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>();

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
@@ -540,19 +604,16 @@ namespace Discord.Commands
{
//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;
.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)
{
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)
{
@@ -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)
.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
.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 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



+ 47
- 0
src/Discord.Net.Commands/Results/MatchResult.cs View File

@@ -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
- 1
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Core</AssemblyName>
<RootNamespace>Discord</RootNamespace>


+ 86
- 0
src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs View File

@@ -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
}
}

+ 15
- 7
src/Discord.Net.Core/Entities/Channels/INestedChannel.cs View File

@@ -60,13 +60,6 @@ namespace Discord
/// <summary>
/// Creates a new invite to this channel.
/// </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="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>
@@ -79,6 +72,21 @@ namespace Discord
/// </returns>
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>
/// Creates a new invite to this channel.
/// </summary>


+ 3
- 3
src/Discord.Net.Core/Entities/Users/IPresence.cs View File

@@ -1,4 +1,4 @@
using System.Collections.Immutable;
using System.Collections.Generic;

namespace Discord
{
@@ -14,10 +14,10 @@ namespace Discord
/// <summary>
/// Gets the set of clients where this user is currently active.
/// </summary>
IImmutableSet<ClientType> ActiveClients { get; }
IReadOnlyCollection<ClientType> ActiveClients { get; }
/// <summary>
/// Gets the list of activities that this user currently has available.
/// </summary>
IImmutableList<IActivity> Activities { get; }
IReadOnlyCollection<IActivity> Activities { get; }
}
}

+ 12
- 1
src/Discord.Net.Core/Format.cs View File

@@ -7,7 +7,8 @@ namespace Discord
public static class Format
{
// Characters which need escaping
private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" };
private static readonly string[] SensitiveCharacters = {
"\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" };

/// <summary> Returns a markdown-formatted string with bold formatting. </summary>
public static string Bold(string text) => $"**{text}**";
@@ -104,5 +105,15 @@ namespace Discord
var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", "");
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}";
}
}
}

+ 5
- 2
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -227,8 +227,11 @@ namespace Discord.Rest
/// <inheritdoc />
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);
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)
=> throw new NotImplementedException();
/// <inheritdoc />


+ 3
- 0
src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs View File

@@ -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)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <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)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />


+ 5
- 4
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -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
/// <inheritdoc />
public virtual UserStatus Status => UserStatus.Offline;
/// <inheritdoc />
public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty;
public virtual IReadOnlyCollection<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty;
public virtual IReadOnlyCollection<IActivity> Activities => ImmutableList<IActivity>.Empty;
/// <inheritdoc />
public virtual bool IsWebhook => false;

@@ -128,8 +129,8 @@ namespace Discord.Rest
/// <returns>
/// A string that resolves to Username#Discriminator of the user.
/// </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

#region IUser


+ 12
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -502,6 +502,18 @@ namespace Discord.WebSocket
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();
#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
/// <summary>
/// Fired when an invite is created.


+ 2
- 2
src/Discord.Net.WebSocket/ClientState.cs View File

@@ -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)


+ 61
- 13
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -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;
/// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> 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);
}

/// <inheritdoc />
@@ -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)
{
@@ -1858,6 +1909,8 @@ namespace Discord.WebSocket

var data = (payload as JToken).ToObject<API.Presence>(_serializer);

SocketUser user = null;

if (data.GuildId.IsSpecified)
{
var guild = State.GetGuild(data.GuildId.Value);
@@ -1872,7 +1925,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 +1943,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<SocketGuildUser, ulong>(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":


+ 5
- 0
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -183,6 +183,11 @@ namespace Discord.WebSocket
/// </remarks>
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>
/// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration.
/// </summary>


+ 3
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -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)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <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)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />


+ 3
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -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)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false);
/// <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)
=> await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false);
/// <inheritdoc />


+ 19
- 12
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -1144,22 +1144,29 @@ namespace Discord.WebSocket
}
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>();
DownloadedMemberCount = _members.Count;

foreach (var member in members)
{
if (member.Id != self?.Id)
member.GlobalUser.RemoveRef(Discord);
}
}

/// <summary>


+ 0
- 6
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs View File

@@ -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;
}


+ 8
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -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<ulong>(roleIds.Length + 1);


+ 20
- 9
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -11,26 +11,37 @@ namespace Discord.WebSocket
/// Represents the WebSocket user's presence status. This may include their online status and their activity.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct SocketPresence : IPresence
public class SocketPresence : IPresence
{
/// <inheritdoc />
public UserStatus Status { get; }
public UserStatus Status { get; private set; }
/// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients { get; }
public IReadOnlyCollection<ClientType> ActiveClients { get; private set; }
/// <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)
{
Status = status;
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty;
Activities = activities ?? ImmutableList<IActivity>.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<ClientType>.Empty;
Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty;
}

/// <summary>
/// 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.
@@ -42,7 +53,7 @@ namespace Discord.WebSocket
/// <returns>
/// A collection of all <see cref="ClientType"/>s that this user is active.
/// </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)
return ImmutableHashSet<ClientType>.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;
}
}

+ 10
- 4
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -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
/// <inheritdoc />
public UserStatus Status => Presence.Status;
/// <inheritdoc />
public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
/// <summary>
/// Gets mutual guilds shared with this user.
/// </summary>
@@ -91,6 +92,11 @@ namespace Discord.WebSocket
return hasChanges;
}

internal virtual void Update(PresenceModel model)
{
Presence.Update(model);
}

/// <inheritdoc />
public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null)
=> await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false);
@@ -109,8 +115,8 @@ namespace Discord.WebSocket
/// <returns>
/// The full name of the user.
/// </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;
}
}

+ 1
- 0
test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs View File

@@ -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> 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<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException();
}
}

+ 1
- 0
test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs View File

@@ -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)
=> 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)
=> throw new NotImplementedException();



Loading…
Cancel
Save