[Feature] Super reactions support (#2707)

* super reactions

* add type to `GetReactionUsers` methods

* add `MeBurst`
This commit is contained in:
Mihail Gribkov
2023-11-18 23:57:11 +03:00
committed by GitHub
parent e3cd340dcc
commit 9fd5c6c27e
16 changed files with 347 additions and 169 deletions

View File

@@ -202,6 +202,7 @@ namespace Discord
#region Reactions (90XXX) #region Reactions (90XXX)
ReactionBlocked = 90001, ReactionBlocked = 90001,
CannotUseBurstReaction = 90002,
#endregion #endregion
#region API Status (130XXX) #region API Status (130XXX)

View File

@@ -323,9 +323,11 @@ namespace Discord
/// <param name="emoji">The emoji that represents the reaction that you wish to get.</param> /// <param name="emoji">The emoji that represents the reaction that you wish to get.</param>
/// <param name="limit">The number of users to request.</param> /// <param name="limit">The number of users to request.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>
/// <param name="type">The type of the reaction you wish to get users for.</param>
/// <returns> /// <returns>
/// Paged collection of users. /// Paged collection of users.
/// </returns> /// </returns>
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null,
ReactionType type = ReactionType.Normal);
} }
} }

View File

@@ -1,13 +1,22 @@
namespace Discord using System.Collections.Generic;
namespace Discord;
/// <summary>
/// Represents a generic reaction object.
/// </summary>
public interface IReaction
{ {
/// <summary> /// <summary>
/// Represents a generic reaction object. /// The <see cref="IEmote" /> used in the reaction.
/// </summary> /// </summary>
public interface IReaction IEmote Emote { get; }
{
/// <summary> /// <summary>
/// The <see cref="IEmote" /> used in the reaction. /// Gets colors used for the super reaction.
/// </summary> /// </summary>
IEmote Emote { get; } /// <remarks>
} /// The collection will be empty if the reaction is a normal reaction.
/// </remarks>
public IReadOnlyCollection<Color> BurstColors { get; }
} }

View File

@@ -1,24 +1,40 @@
namespace Discord using System.Collections.Generic;
namespace Discord;
/// <summary>
/// A metadata containing reaction information.
/// </summary>
public struct ReactionMetadata
{ {
/// <summary> /// <summary>
/// A metadata containing reaction information. /// Gets the number of reactions.
/// </summary> /// </summary>
public struct ReactionMetadata /// <returns>
{ /// An <see cref="int"/> representing the number of this reactions that has been added to this message.
/// <summary> /// </returns>
/// Gets the number of reactions. public int ReactionCount { get; internal set; }
/// </summary>
/// <returns> /// <summary>
/// An <see cref="int"/> representing the number of this reactions that has been added to this message. /// Gets a value that indicates whether the current user has reacted to this.
/// </returns> /// </summary>
public int ReactionCount { get; internal set; } /// <returns>
/// <see langword="true" /> if the user has reacted to the message; otherwise <see langword="false" />.
/// </returns>
public bool IsMe { get; internal set; }
/// <summary> /// <summary>
/// Gets a value that indicates whether the current user has reacted to this. /// Gets the number of burst reactions added to this message.
/// </summary> /// </summary>
/// <returns> public int BurstCount { get; internal set; }
/// <see langword="true" /> if the user has reacted to the message; otherwise <see langword="false" />.
/// </returns> /// <summary>
public bool IsMe { get; internal set; } /// Gets the number of normal reactions added to this message.
} /// </summary>
public int NormalCount { get; internal set; }
/// <summary>
/// Gets colors used for super reaction.
/// </summary>
public IReadOnlyCollection<Color> BurstColors { get; internal set; }
} }

View File

@@ -0,0 +1,14 @@
namespace Discord;
public enum ReactionType
{
/// <summary>
/// The reaction is a normal reaction.
/// </summary>
Normal = 0,
/// <summary>
/// The reaction is a super reaction.
/// </summary>
Burst = 1,
}

View File

@@ -1,14 +1,24 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Discord.API namespace Discord.API;
internal class Reaction
{ {
internal class Reaction [JsonProperty("count")]
{ public int Count { get; set; }
[JsonProperty("count")]
public int Count { get; set; } [JsonProperty("me")]
[JsonProperty("me")] public bool Me { get; set; }
public bool Me { get; set; }
[JsonProperty("emoji")] [JsonProperty("me_burst")]
public Emoji Emoji { get; set; } public bool MeBurst { get; set; }
}
[JsonProperty("emoji")]
public Emoji Emoji { get; set; }
[JsonProperty("count_details")]
public ReactionCountDetails CountDetails { get; set; }
[JsonProperty("burst_colors")]
public Color[] Colors { get; set; }
} }

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Discord.API;
internal class ReactionCountDetails
{
[JsonProperty("normal")]
public int NormalCount { get; set;}
[JsonProperty("burst")]
public int BurstCount { get; set;}
}

View File

@@ -1143,7 +1143,8 @@ namespace Discord.API
await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", ids, options: options).ConfigureAwait(false); await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", ids, options: options).ConfigureAwait(false);
} }
public async Task<IReadOnlyCollection<User>> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null)
public async Task<IReadOnlyCollection<User>> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, ReactionType reactionType, RequestOptions options = null)
{ {
Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(channelId, 0, nameof(channelId));
Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotEqual(messageId, 0, nameof(messageId));
@@ -1158,9 +1159,10 @@ namespace Discord.API
ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); ulong afterUserId = args.AfterUserId.GetValueOrDefault(0);
var ids = new BucketIds(channelId: channelId); var ids = new BucketIds(channelId: channelId);
Expression<Func<string>> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}"; Expression<Func<string>> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}&type={(int)reactionType}";
return await SendAsync<IReadOnlyCollection<User>>("GET", endpoint, ids, options: options).ConfigureAwait(false); return await SendAsync<IReadOnlyCollection<User>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
} }
public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
{ {
Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(channelId, 0, nameof(channelId));

View File

@@ -164,7 +164,7 @@ namespace Discord.Rest
} }
public static IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote, public static IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IMessage msg, IEmote emote,
int? limit, BaseDiscordClient client, RequestOptions options) int? limit, BaseDiscordClient client, ReactionType reactionType, RequestOptions options)
{ {
Preconditions.NotNull(emote, nameof(emote)); Preconditions.NotNull(emote, nameof(emote));
var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name)); var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name));
@@ -181,7 +181,7 @@ namespace Discord.Rest
if (info.Position != null) if (info.Position != null)
args.AfterUserId = info.Position.Value; args.AfterUserId = info.Position.Value;
var models = await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false); var models = await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, reactionType, options).ConfigureAwait(false);
return models.Select(x => RestUser.Create(client, x)).ToImmutableArray(); return models.Select(x => RestUser.Create(client, x)).ToImmutableArray();
}, },
nextPage: (info, lastPage) => nextPage: (info, lastPage) =>

View File

@@ -316,7 +316,14 @@ namespace Discord.Rest
#endregion #endregion
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata
{
ReactionCount = x.Count,
IsMe = x.Me,
BurstColors = x.BurstColors,
BurstCount = x.BurstCount,
NormalCount = x.NormalCount,
});
/// <inheritdoc /> /// <inheritdoc />
public Task AddReactionAsync(IEmote emote, RequestOptions options = null) public Task AddReactionAsync(IEmote emote, RequestOptions options = null)
@@ -334,7 +341,7 @@ namespace Discord.Rest
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options); => MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options);
/// <inheritdoc /> /// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null, ReactionType type = ReactionType.Normal)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, type, options);
} }
} }

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Model = Discord.API.Reaction; using Model = Discord.API.Reaction;
namespace Discord.Rest namespace Discord.Rest
@@ -9,20 +10,44 @@ namespace Discord.Rest
{ {
/// <inheritdoc /> /// <inheritdoc />
public IEmote Emote { get; } public IEmote Emote { get; }
/// <summary> /// <summary>
/// Gets the number of reactions added. /// Gets the number of reactions added.
/// </summary> /// </summary>
public int Count { get; } public int Count { get; }
/// <summary> /// <summary>
/// Gets whether the reactions is added by the user. /// Gets whether the reaction is added by the user.
/// </summary> /// </summary>
public bool Me { get; } public bool Me { get; }
internal RestReaction(IEmote emote, int count, bool me) /// <summary>
/// Gets whether the super-reaction is added by the user.
/// </summary>
public bool MeBurst { get; }
/// <summary>
/// Gets the number of burst reactions added.
/// </summary>
public int BurstCount { get; }
/// <summary>
/// Gets the number of normal reactions added.
/// </summary>
public int NormalCount { get; }
/// <inheritdoc />
public IReadOnlyCollection<Color> BurstColors { get; }
internal RestReaction(IEmote emote, int count, bool me, int burst, int normal, IReadOnlyCollection<Color> colors, bool meBurst)
{ {
Emote = emote; Emote = emote;
Count = count; Count = count;
Me = me; Me = me;
BurstCount = burst;
NormalCount = normal;
BurstColors = colors;
MeBurst = meBurst;
} }
internal static RestReaction Create(Model model) internal static RestReaction Create(Model model)
{ {
@@ -31,7 +56,13 @@ namespace Discord.Rest
emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault());
else else
emote = new Emoji(model.Emoji.Name); emote = new Emoji(model.Emoji.Name);
return new RestReaction(emote, model.Count, model.Me); return new RestReaction(emote,
model.Count,
model.Me,
model.CountDetails.BurstCount,
model.CountDetails.NormalCount,
model.Colors.ToReadOnlyCollection(),
model.MeBurst);
} }
} }
} }

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using System.Globalization;
using System;
namespace Discord.Net.Converters;
internal class ColorConverter : JsonConverter
{
public static readonly ColorConverter Instance = new ();
public override bool CanConvert(Type objectType) => true;
public override bool CanRead => true;
public override bool CanWrite => true;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value is null)
return null;
return new Color(uint.Parse(reader.Value.ToString()!.TrimStart('#'), NumberStyles.HexNumber));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue($"#{(uint)value:X}");
}
}

View File

@@ -93,6 +93,8 @@ namespace Discord.Net.Converters
return DiscordErrorConverter.Instance; return DiscordErrorConverter.Instance;
if (type == typeof(GuildFeatures)) if (type == typeof(GuildFeatures))
return GuildFeaturesConverter.Instance; return GuildFeaturesConverter.Instance;
if(type == typeof(Color))
return ColorConverter.Instance;
//Entities //Entities
var typeInfo = type.GetTypeInfo(); var typeInfo = type.GetTypeInfo();

View File

@@ -1,20 +1,33 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Discord.API.Gateway namespace Discord.API.Gateway;
internal class Reaction
{ {
internal class Reaction [JsonProperty("user_id")]
{ public ulong UserId { get; set; }
[JsonProperty("user_id")]
public ulong UserId { get; set; } [JsonProperty("message_id")]
[JsonProperty("message_id")] public ulong MessageId { get; set; }
public ulong MessageId { get; set; }
[JsonProperty("channel_id")] [JsonProperty("channel_id")]
public ulong ChannelId { get; set; } public ulong ChannelId { get; set; }
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; } [JsonProperty("guild_id")]
[JsonProperty("emoji")] public Optional<ulong> GuildId { get; set; }
public Emoji Emoji { get; set; }
[JsonProperty("member")] [JsonProperty("emoji")]
public Optional<GuildMember> Member { get; set; } public Emoji Emoji { get; set; }
}
[JsonProperty("member")]
public Optional<GuildMember> Member { get; set; }
[JsonProperty("burst")]
public bool IsBurst { get; set; }
[JsonProperty("burst_colors")]
public Optional<Color[]> BurstColors { get; set; }
[JsonProperty("type")]
public ReactionType Type { get; set; }
} }

View File

@@ -374,8 +374,8 @@ namespace Discord.WebSocket
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null)
=> MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options); => MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options);
/// <inheritdoc /> /// <inheritdoc />
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null, ReactionType type = ReactionType.Normal)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, type, options);
#endregion #endregion
} }
} }

View File

@@ -1,113 +1,145 @@
using System;
using System.Collections.Generic;
using Model = Discord.API.Gateway.Reaction; using Model = Discord.API.Gateway.Reaction;
namespace Discord.WebSocket namespace Discord.WebSocket;
/// <summary>
/// Represents a WebSocket-based reaction object.
/// </summary>
public class SocketReaction : IReaction
{ {
/// <summary> /// <summary>
/// Represents a WebSocket-based reaction object. /// Gets the ID of the user who added the reaction.
/// </summary> /// </summary>
public class SocketReaction : IReaction /// <remarks>
/// This property retrieves the snowflake identifier of the user responsible for this reaction. This
/// property will always contain the user identifier in event that
/// <see cref="Discord.WebSocket.SocketReaction.User" /> cannot be retrieved.
/// </remarks>
/// <returns>
/// A user snowflake identifier associated with the user.
/// </returns>
public ulong UserId { get; }
/// <summary>
/// Gets the user who added the reaction if possible.
/// </summary>
/// <remarks>
/// <para>
/// This property attempts to retrieve a WebSocket-cached user that is responsible for this reaction from
/// the client. In other words, when the user is not in the WebSocket cache, this property may not
/// contain a value, leaving the only identifiable information to be
/// <see cref="Discord.WebSocket.SocketReaction.UserId" />.
/// </para>
/// <para>
/// If you wish to obtain an identifiable user object, consider utilizing
/// <see cref="Discord.Rest.DiscordRestClient" /> which will attempt to retrieve the user from REST.
/// </para>
/// </remarks>
/// <returns>
/// A user object where possible; a value is not always returned.
/// </returns>
/// <seealso cref="Optional{T}"/>
public Optional<IUser> User { get; }
/// <summary>
/// Gets the ID of the message that has been reacted to.
/// </summary>
/// <returns>
/// A message snowflake identifier associated with the message.
/// </returns>
public ulong MessageId { get; }
/// <summary>
/// Gets the message that has been reacted to if possible.
/// </summary>
/// <returns>
/// A WebSocket-based message where possible; a value is not always returned.
/// </returns>
/// <seealso cref="Optional{T}"/>
public Optional<SocketUserMessage> Message { get; }
/// <summary>
/// Gets the channel where the reaction takes place in.
/// </summary>
/// <returns>
/// A WebSocket-based message channel.
/// </returns>
public ISocketMessageChannel Channel { get; }
/// <inheritdoc />
public IEmote Emote { get; }
/// <summary>
/// Gets whether the reaction is a super reaction.
/// </summary>
public bool IsBurst { get; }
/// <inheritdoc />
public IReadOnlyCollection<Color> BurstColors { get; }
/// <summary>
/// Gets the type of the reaction.
/// </summary>
public ReactionType ReactionType { get; }
internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional<SocketUserMessage> message, ulong userId, Optional<IUser> user,
IEmote emoji, bool isBurst, IReadOnlyCollection<Color> colors, ReactionType reactionType)
{ {
/// <summary> Channel = channel;
/// Gets the ID of the user who added the reaction. MessageId = messageId;
/// </summary> Message = message;
/// <remarks> UserId = userId;
/// This property retrieves the snowflake identifier of the user responsible for this reaction. This User = user;
/// property will always contain the user identifier in event that Emote = emoji;
/// <see cref="Discord.WebSocket.SocketReaction.User" /> cannot be retrieved. IsBurst = isBurst;
/// </remarks> BurstColors = colors;
/// <returns> ReactionType = reactionType;
/// A user snowflake identifier associated with the user. }
/// </returns>
public ulong UserId { get; }
/// <summary>
/// Gets the user who added the reaction if possible.
/// </summary>
/// <remarks>
/// <para>
/// This property attempts to retrieve a WebSocket-cached user that is responsible for this reaction from
/// the client. In other words, when the user is not in the WebSocket cache, this property may not
/// contain a value, leaving the only identifiable information to be
/// <see cref="Discord.WebSocket.SocketReaction.UserId" />.
/// </para>
/// <para>
/// If you wish to obtain an identifiable user object, consider utilizing
/// <see cref="Discord.Rest.DiscordRestClient" /> which will attempt to retrieve the user from REST.
/// </para>
/// </remarks>
/// <returns>
/// A user object where possible; a value is not always returned.
/// </returns>
/// <seealso cref="Optional{T}"/>
public Optional<IUser> User { get; }
/// <summary>
/// Gets the ID of the message that has been reacted to.
/// </summary>
/// <returns>
/// A message snowflake identifier associated with the message.
/// </returns>
public ulong MessageId { get; }
/// <summary>
/// Gets the message that has been reacted to if possible.
/// </summary>
/// <returns>
/// A WebSocket-based message where possible; a value is not always returned.
/// </returns>
/// <seealso cref="Optional{T}"/>
public Optional<SocketUserMessage> Message { get; }
/// <summary>
/// Gets the channel where the reaction takes place in.
/// </summary>
/// <returns>
/// A WebSocket-based message channel.
/// </returns>
public ISocketMessageChannel Channel { get; }
/// <inheritdoc />
public IEmote Emote { get; }
internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional<SocketUserMessage> message, ulong userId, Optional<IUser> user, IEmote emoji) internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional<SocketUserMessage> message, Optional<IUser> user)
{
IEmote emote;
if (model.Emoji.Id.HasValue)
emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault());
else
emote = new Emoji(model.Emoji.Name);
return new SocketReaction(channel,
model.MessageId,
message,
model.UserId,
user,
emote,
model.IsBurst,
model.BurstColors.GetValueOrDefault(Array.Empty<Color>()).ToReadOnlyCollection(),
model.Type);
}
/// <inheritdoc />
public override bool Equals(object other)
{
if (other == null)
return false;
if (other == this)
return true;
var otherReaction = other as SocketReaction;
if (otherReaction == null)
return false;
return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote);
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{ {
Channel = channel; var hashCode = UserId.GetHashCode();
MessageId = messageId; hashCode = (hashCode * 397) ^ MessageId.GetHashCode();
Message = message; hashCode = (hashCode * 397) ^ Emote.GetHashCode();
UserId = userId; return hashCode;
User = user;
Emote = emoji;
}
internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional<SocketUserMessage> message, Optional<IUser> user)
{
IEmote emote;
if (model.Emoji.Id.HasValue)
emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault());
else
emote = new Emoji(model.Emoji.Name);
return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote);
}
/// <inheritdoc />
public override bool Equals(object other)
{
if (other == null)
return false;
if (other == this)
return true;
var otherReaction = other as SocketReaction;
if (otherReaction == null)
return false;
return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote);
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
var hashCode = UserId.GetHashCode();
hashCode = (hashCode * 397) ^ MessageId.GetHashCode();
hashCode = (hashCode * 397) ^ Emote.GetHashCode();
return hashCode;
}
} }
} }
} }