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




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

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




+ 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"> <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>


+ 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> /// <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>


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

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

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

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

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


+ 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) 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
- 4
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

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


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


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

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


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

@@ -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":


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

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


+ 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) 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 />


+ 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) 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 />


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

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


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

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


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

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


+ 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. /// 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;
} }
} }

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

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

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

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




Loading…
Cancel
Save