[Feature] Support for the new username system (#2696)

* update a lot of stuff

* update CDN for new avatar calculation

* whoops, forgot to commit

* handle `null` values

* Remove duplicate line

Co-authored-by: Dmitry <dimson-n@users.noreply.github.com>

* updates

* Apply suggestions from code review

Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>

* Apply suggestion

Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>

* update `SocketThreadUSer`

---------

Co-authored-by: Dmitry <dimson-n@users.noreply.github.com>
Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>
This commit is contained in:
Misha133
2023-06-27 17:11:32 +03:00
committed by GitHub
parent 9ddd922d2f
commit df717e6b2c
22 changed files with 113 additions and 39 deletions

View File

@@ -59,6 +59,17 @@ namespace Discord.Commands
} }
} }
//By global (display) name
{
await channelUsers
.Where(x => string.Equals(input, x.GlobalName, StringComparison.OrdinalIgnoreCase))
.ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.GlobalName == input ? 0.65f : 0.55f))
.ConfigureAwait(false);
foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.GlobalName, StringComparison.OrdinalIgnoreCase)))
AddResult(results, guildUser as T, guildUser.GlobalName == input ? 0.60f : 0.50f);
}
//By Username (0.5-0.6) //By Username (0.5-0.6)
{ {
await channelUsers await channelUsers

View File

@@ -72,6 +72,7 @@ namespace Discord
string extension = FormatToExtension(format, bannerId); string extension = FormatToExtension(format, bannerId);
return $"{DiscordConfig.CDNUrl}banners/{userId}/{bannerId}.{extension}?size={size}"; return $"{DiscordConfig.CDNUrl}banners/{userId}/{bannerId}.{extension}?size={size}";
} }
/// <summary> /// <summary>
/// Returns the default user avatar URL. /// Returns the default user avatar URL.
/// </summary> /// </summary>
@@ -83,6 +84,19 @@ namespace Discord
{ {
return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png";
} }
/// <summary>
/// Returns the default user avatar URL.
/// </summary>
/// <param name="userId">The Id of a user.</param>
/// <returns>
/// A URL pointing to the user's default avatar when one isn't set.
/// </returns>
public static string GetDefaultUserAvatarUrl(ulong userId)
{
return $"{DiscordConfig.CDNUrl}embed/avatars/{(userId >> 22) % 6}.png";
}
/// <summary> /// <summary>
/// Returns an icon URL. /// Returns an icon URL.
/// </summary> /// </summary>

View File

@@ -47,11 +47,11 @@ namespace Discord
/// </returns> /// </returns>
string GetDefaultAvatarUrl(); string GetDefaultAvatarUrl();
/// <summary> /// <summary>
/// Gets the per-username unique ID for this user. /// Gets the per-username unique ID for this user. This will return "0000" for users who have migrated to new username system.
/// </summary> /// </summary>
string Discriminator { get; } string Discriminator { get; }
/// <summary> /// <summary>
/// Gets the per-username unique ID for this user. /// Gets the per-username unique ID for this user. This will return 0 for users who have migrated to new username system.
/// </summary> /// </summary>
ushort DiscriminatorValue { get; } ushort DiscriminatorValue { get; }
/// <summary> /// <summary>
@@ -87,6 +87,14 @@ namespace Discord
/// </returns> /// </returns>
UserProperties? PublicFlags { get; } UserProperties? PublicFlags { get; }
/// <summary>
/// Gets the user's display name, if it is set. For bots, this will get the application name.
/// </summary>
/// <remarks>
/// This property will be <see langword="null"/> if user has no display name set.
/// </remarks>
string GlobalName { get; }
/// <summary> /// <summary>
/// Creates the direct message channel of this user. /// Creates the direct message channel of this user.
/// </summary> /// </summary>

View File

@@ -27,7 +27,7 @@ namespace Discord
/// <summary> Fills the embed author field with the provided user's full username and avatar URL. </summary> /// <summary> Fills the embed author field with the provided user's full username and avatar URL. </summary>
public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) =>
builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl()); builder.WithAuthor(user.DiscriminatorValue != 0 ? $"{user.Username}#{user.Discriminator}" : user.Username, user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl());
/// <summary> Converts a <see cref="EmbedType.Rich"/> <see cref="IEmbed"/> object to a <see cref="EmbedBuilder"/>. </summary> /// <summary> Converts a <see cref="EmbedType.Rich"/> <see cref="IEmbed"/> object to a <see cref="EmbedBuilder"/>. </summary>
/// <exception cref="InvalidOperationException">The embed type is not <see cref="EmbedType.Rich"/>.</exception> /// <exception cref="InvalidOperationException">The embed type is not <see cref="EmbedType.Rich"/>.</exception>

View File

@@ -108,16 +108,18 @@ namespace Discord
} }
/// <summary> /// <summary>
/// Formats a user's username + discriminator. /// Formats a user's username and optional discriminator.
/// </summary> /// </summary>
/// <param name="doBidirectional">To format the string in bidirectional unicode or not</param> /// <param name="doBidirectional">To format the string in bidirectional unicode or not</param>
/// <param name="user">The user whose username and discriminator to format</param> /// <param name="user">The user whose username and discriminator to format</param>
/// <returns>The username + discriminator</returns> /// <returns>The username + optional discriminator.</returns>
public static string UsernameAndDiscriminator(IUser user, bool doBidirectional) public static string UsernameAndDiscriminator(IUser user, bool doBidirectional)
{ {
if (user.DiscriminatorValue != 0)
return doBidirectional return doBidirectional
? $"\u2066{user.Username}\u2069#{user.Discriminator}" ? $"\u2066{user.Username}\u2069#{user.Discriminator}"
: $"{user.Username}#{user.Discriminator}"; : $"{user.Username}#{user.Discriminator}";
return user.Username;
} }
} }
} }

View File

@@ -12,14 +12,14 @@ namespace Discord
private const char SanitizeChar = '\x200b'; private const char SanitizeChar = '\x200b';
//If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake)
internal static string MentionUser(string id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; internal static string MentionUser(string id) => $"<@{id}>";
/// <summary> /// <summary>
/// Returns a mention string based on the user ID. /// Returns a mention string based on the user ID.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A user mention string (e.g. &lt;@80351110224678912&gt;). /// A user mention string (e.g. &lt;@80351110224678912&gt;).
/// </returns> /// </returns>
public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); public static string MentionUser(ulong id) => MentionUser(id.ToString());
internal static string MentionChannel(string id) => $"<#{id}>"; internal static string MentionChannel(string id) => $"<#{id}>";
/// <summary> /// <summary>
/// Returns a mention string based on the channel ID. /// Returns a mention string based on the channel ID.
@@ -192,19 +192,19 @@ namespace Discord
return ""; return "";
case TagHandling.FullName: case TagHandling.FullName:
if (user != null) if (user != null)
return $"@{user.Username}#{user.Discriminator}"; return user.DiscriminatorValue != 0 ? $"@{user.Username}#{user.Discriminator}" : user.Username;
else else
return ""; return "";
case TagHandling.FullNameNoPrefix: case TagHandling.FullNameNoPrefix:
if (user != null) if (user != null)
return $"{user.Username}#{user.Discriminator}"; return user.DiscriminatorValue != 0 ? $"@{user.Username}#{user.Discriminator}" : user.Username;
else else
return ""; return "";
case TagHandling.Sanitize: case TagHandling.Sanitize:
if (guildUser != null && guildUser.Nickname == null) if (guildUser != null && guildUser.Nickname == null)
return MentionUser($"{SanitizeChar}{tag.Key}", false); return MentionUser($"{SanitizeChar}{tag.Key}");
else else
return MentionUser($"{SanitizeChar}{tag.Key}", true); return MentionUser($"{SanitizeChar}{tag.Key}");
} }
} }
return ""; return "";

View File

@@ -19,6 +19,9 @@ namespace Discord.API
[JsonProperty("accent_color")] [JsonProperty("accent_color")]
public Optional<uint?> AccentColor { get; set; } public Optional<uint?> AccentColor { get; set; }
[JsonProperty("global_name")]
public Optional<string> GlobalName { get; set; }
//CurrentUser //CurrentUser
[JsonProperty("verified")] [JsonProperty("verified")]
public Optional<bool> Verified { get; set; } public Optional<bool> Verified { get; set; }

View File

@@ -19,8 +19,10 @@ namespace Discord.Rest
private long? _timedOutTicks; private long? _timedOutTicks;
private long? _joinedAtTicks; private long? _joinedAtTicks;
private ImmutableArray<ulong> _roleIds; private ImmutableArray<ulong> _roleIds;
/// <inheritdoc />
public string DisplayName => Nickname ?? Username; /// <inheritdoc cref="IGuildUser.DisplayName"/>
public string DisplayName => Nickname ?? GlobalName ?? Username;
/// <inheritdoc /> /// <inheritdoc />
public string Nickname { get; private set; } public string Nickname { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -30,6 +30,8 @@ namespace Discord.Rest
public Color? AccentColor { get; private set; } public Color? AccentColor { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public UserProperties? PublicFlags { get; private set; } public UserProperties? PublicFlags { get; private set; }
/// <inheritdoc />
public string GlobalName { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
@@ -85,13 +87,15 @@ namespace Discord.Rest
if (model.AccentColor.IsSpecified) if (model.AccentColor.IsSpecified)
AccentColor = model.AccentColor.Value; AccentColor = model.AccentColor.Value;
if (model.Discriminator.IsSpecified) if (model.Discriminator.IsSpecified)
DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); DiscriminatorValue = ushort.Parse(model.Discriminator.GetValueOrDefault(null) ?? "0", NumberStyles.None, CultureInfo.InvariantCulture);
if (model.Bot.IsSpecified) if (model.Bot.IsSpecified)
IsBot = model.Bot.Value; IsBot = model.Bot.Value;
if (model.Username.IsSpecified) if (model.Username.IsSpecified)
Username = model.Username.Value; Username = model.Username.Value;
if (model.PublicFlags.IsSpecified) if (model.PublicFlags.IsSpecified)
PublicFlags = model.PublicFlags.Value; PublicFlags = model.PublicFlags.Value;
if (model.GlobalName.IsSpecified)
GlobalName = model.GlobalName.Value;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -121,7 +125,9 @@ namespace Discord.Rest
/// <inheritdoc /> /// <inheritdoc />
public string GetDefaultAvatarUrl() public string GetDefaultAvatarUrl()
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); => DiscriminatorValue != 0
? CDN.GetDefaultUserAvatarUrl(DiscriminatorValue)
: CDN.GetDefaultUserAvatarUrl(Id);
/// <summary> /// <summary>
/// Gets the Username#Discriminator of the user. /// Gets the Username#Discriminator of the user.

View File

@@ -201,9 +201,10 @@ namespace Discord.Rest
Username = command.User.Username, Username = command.User.Username,
Avatar = command.User.AvatarId, Avatar = command.User.AvatarId,
Bot = command.User.IsBot, Bot = command.User.IsBot,
Discriminator = command.User.Discriminator, Discriminator = command.User.Discriminator == "0000" ? Optional<string>.Unspecified : command.User.Discriminator,
PublicFlags = command.User.PublicFlags.HasValue ? command.User.PublicFlags.Value : Optional<UserProperties>.Unspecified, PublicFlags = command.User.PublicFlags.HasValue ? command.User.PublicFlags.Value : Optional<UserProperties>.Unspecified,
Id = command.User.Id, Id = command.User.Id,
GlobalName = command.User.GlobalName,
} }
}; };
} }

View File

@@ -140,7 +140,7 @@ namespace Discord.WebSocket
/// <returns> /// <returns>
/// A generic WebSocket-based user; <see langword="null" /> when the user cannot be found. /// A generic WebSocket-based user; <see langword="null" /> when the user cannot be found.
/// </returns> /// </returns>
public abstract SocketUser GetUser(string username, string discriminator); public abstract SocketUser GetUser(string username, string discriminator = null);
/// <summary> /// <summary>
/// Gets a channel. /// Gets a channel.
/// </summary> /// </summary>

View File

@@ -343,7 +343,7 @@ namespace Discord.WebSocket
return null; return null;
} }
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(string username, string discriminator) public override SocketUser GetUser(string username, string discriminator = null)
{ {
for (int i = 0; i < _shards.Length; i++) for (int i = 0; i < _shards.Length; i++)
{ {

View File

@@ -425,8 +425,8 @@ namespace Discord.WebSocket
public override SocketUser GetUser(ulong id) public override SocketUser GetUser(ulong id)
=> State.GetUser(id); => State.GetUser(id);
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(string username, string discriminator) public override SocketUser GetUser(string username, string discriminator = null)
=> State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); => State.Users.FirstOrDefault(x => (discriminator is null || x.Discriminator == discriminator) && x.Username == username);
/// <summary> /// <summary>
/// Gets a global application command. /// Gets a global application command.

View File

@@ -48,7 +48,10 @@ namespace Discord.WebSocket
} }
} }
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; private string DebuggerDisplay => DiscriminatorValue != 0
? $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"
: $"{Username} ({Id}{(IsBot ? ", Bot" : "")}, Guild)";
internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser;
} }
} }

View File

@@ -48,7 +48,10 @@ namespace Discord.WebSocket
return entity; return entity;
} }
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; private string DebuggerDisplay => DiscriminatorValue != 0
? $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"
: $"{Username} ({Id}{(IsBot ? ", Bot" : "")}, Group)";
internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser;
#endregion #endregion

View File

@@ -29,8 +29,10 @@ namespace Discord.WebSocket
/// Gets the guild the user is in. /// Gets the guild the user is in.
/// </summary> /// </summary>
public SocketGuild Guild { get; } public SocketGuild Guild { get; }
/// <inheritdoc />
public string DisplayName => Nickname ?? Username; /// <inheritdoc cref="IGuildUser.DisplayName"/>
public string DisplayName => Nickname ?? GlobalName ?? Username;
/// <inheritdoc /> /// <inheritdoc />
public string Nickname { get; private set; } public string Nickname { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
@@ -265,7 +267,9 @@ namespace Discord.WebSocket
public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); => CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format);
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; private string DebuggerDisplay => DiscriminatorValue != 0
? $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"
: $"{Username} ({Id}{(IsBot ? ", Bot" : "")}, Guild)";
internal new SocketGuildUser Clone() internal new SocketGuildUser Clone()
{ {

View File

@@ -91,7 +91,10 @@ namespace Discord.WebSocket
public Task ModifyAsync(Action<SelfUserProperties> func, RequestOptions options = null) public Task ModifyAsync(Action<SelfUserProperties> func, RequestOptions options = null)
=> UserHelper.ModifyAsync(this, Discord, func, options); => UserHelper.ModifyAsync(this, Discord, func, options);
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; private string DebuggerDisplay => DiscriminatorValue != 0
? $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"
: $"{Username} ({Id}{(IsBot ? ", Bot" : "")}, Self)";
internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser;
} }
} }

View File

@@ -31,7 +31,7 @@ namespace Discord.WebSocket
/// <inheritdoc/> /// <inheritdoc/>
public string DisplayName public string DisplayName
=> GuildUser.Nickname ?? GuildUser.Username; => GuildUser.Nickname ?? GuildUser.GlobalName ?? GuildUser.Username;
/// <inheritdoc/> /// <inheritdoc/>
public string Nickname public string Nickname

View File

@@ -42,7 +42,10 @@ namespace Discord.WebSocket
return entity; return entity;
} }
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; private string DebuggerDisplay => DiscriminatorValue != 0
? $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"
: $"{Username} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)";
internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser;
} }
} }

View File

@@ -32,6 +32,9 @@ namespace Discord.WebSocket
internal abstract SocketGlobalUser GlobalUser { get; set; } internal abstract SocketGlobalUser GlobalUser { get; set; }
internal abstract SocketPresence Presence { get; set; } internal abstract SocketPresence Presence { get; set; }
/// <inheritdoc />
public string GlobalName { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc /> /// <inheritdoc />
@@ -68,10 +71,10 @@ namespace Discord.WebSocket
} }
if (model.Discriminator.IsSpecified) if (model.Discriminator.IsSpecified)
{ {
var newVal = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); var newVal = ushort.Parse(model.Discriminator.GetValueOrDefault(null) ?? "0", NumberStyles.None, CultureInfo.InvariantCulture);
if (newVal != DiscriminatorValue) if (newVal != DiscriminatorValue)
{ {
DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); DiscriminatorValue = ushort.Parse(model.Discriminator.GetValueOrDefault(null) ?? "0", NumberStyles.None, CultureInfo.InvariantCulture);
hasChanges = true; hasChanges = true;
} }
} }
@@ -90,6 +93,11 @@ namespace Discord.WebSocket
PublicFlags = model.PublicFlags.Value; PublicFlags = model.PublicFlags.Value;
hasChanges = true; hasChanges = true;
} }
if (model.GlobalName.IsSpecified)
{
GlobalName = model.GlobalName.Value;
hasChanges = true;
}
return hasChanges; return hasChanges;
} }
@@ -109,7 +117,9 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public string GetDefaultAvatarUrl() public string GetDefaultAvatarUrl()
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); => DiscriminatorValue != 0
? CDN.GetDefaultUserAvatarUrl(DiscriminatorValue)
: CDN.GetDefaultUserAvatarUrl(Id);
/// <summary> /// <summary>
/// Gets the full name of the user (e.g. Example#0001). /// Gets the full name of the user (e.g. Example#0001).

View File

@@ -49,7 +49,10 @@ namespace Discord.WebSocket
return entity; return entity;
} }
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; private string DebuggerDisplay => DiscriminatorValue != 0
? $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"
: $"{Username} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)";
internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser;
#endregion #endregion

View File

@@ -16,10 +16,8 @@ namespace Discord
[Fact] [Fact]
public void MentionUser() public void MentionUser()
{ {
Assert.Equal("<@!123>", MentionUtils.MentionUser(123u)); Assert.Equal("<@123>", MentionUtils.MentionUser(123u));
Assert.Equal("<@!123>", MentionUtils.MentionUser("123")); Assert.Equal("<@123>", MentionUtils.MentionUser("123"));
Assert.Equal("<@!123>", MentionUtils.MentionUser("123", true));
Assert.Equal("<@123>", MentionUtils.MentionUser("123", false));
} }
/// <summary> /// <summary>
/// Tests <see cref="MentionUtils.MentionChannel(string)"/> /// Tests <see cref="MentionUtils.MentionChannel(string)"/>