diff --git a/src/Discord.Net.Core/API/Common/Emoji.cs b/src/Discord.Net.Core/API/Common/Emoji.cs index 032ae51eb..c04786039 100644 --- a/src/Discord.Net.Core/API/Common/Emoji.cs +++ b/src/Discord.Net.Core/API/Common/Emoji.cs @@ -6,7 +6,7 @@ namespace Discord.API public class Emoji { [JsonProperty("id")] - public ulong Id { get; set; } + public ulong? Id { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("roles")] diff --git a/src/Discord.Net.Core/API/Common/Message.cs b/src/Discord.Net.Core/API/Common/Message.cs index 2c19780b1..f812d0622 100644 --- a/src/Discord.Net.Core/API/Common/Message.cs +++ b/src/Discord.Net.Core/API/Common/Message.cs @@ -36,5 +36,7 @@ namespace Discord.API public Optional Embeds { get; set; } [JsonProperty("pinned")] public Optional Pinned { get; set; } + [JsonProperty("reactions")] + public Optional Reactions { get; set; } } } diff --git a/src/Discord.Net.Core/API/Common/Reaction.cs b/src/Discord.Net.Core/API/Common/Reaction.cs new file mode 100644 index 000000000..e143004ef --- /dev/null +++ b/src/Discord.Net.Core/API/Common/Reaction.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Reaction + { + [JsonProperty("count")] + public int Count { get; set; } + [JsonProperty("me")] + public bool Me { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/DiscordRestApiClient.cs b/src/Discord.Net.Core/API/DiscordRestApiClient.cs index 16ea37d92..bc7436cd4 100644 --- a/src/Discord.Net.Core/API/DiscordRestApiClient.cs +++ b/src/Discord.Net.Core/API/DiscordRestApiClient.cs @@ -515,6 +515,59 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("PUT", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{userId}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false); + } + public async Task> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); + + var ids = new BucketIds(channelId: channelId); + Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); diff --git a/src/Discord.Net.Core/API/Rest/GetReactionUsersParams.cs b/src/Discord.Net.Core/API/Rest/GetReactionUsersParams.cs new file mode 100644 index 000000000..bb9b22ab8 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/GetReactionUsersParams.cs @@ -0,0 +1,8 @@ +namespace Discord.API.Rest +{ + public class GetReactionUsersParams + { + public Optional Limit { get; set; } + public Optional AfterUserId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs index 8b2bbd9c2..94f79afa5 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs @@ -24,7 +24,7 @@ namespace Discord } internal static GuildEmoji Create(Model model) { - return new GuildEmoji(model.Id, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmoji(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Messages/Emoji.cs b/src/Discord.Net.Core/Entities/Messages/Emoji.cs index 612e99f29..dddbe65f1 100644 --- a/src/Discord.Net.Core/Entities/Messages/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Messages/Emoji.cs @@ -19,6 +19,11 @@ namespace Discord Name = name; } + internal static Emoji FromApi(API.Emoji emoji) + { + return new Emoji(emoji.Id.GetValueOrDefault(), emoji.Name); + } + public static Emoji Parse(string text) { Emoji result; diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs new file mode 100644 index 000000000..66832760b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IReaction + { + Emoji Emoji { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index c33b3e359..346f1a45e 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -11,7 +12,22 @@ namespace Discord Task PinAsync(RequestOptions options = null); /// Removes this message from its channel's pinned messages. Task UnpinAsync(RequestOptions options = null); - + + /// Returns all reactions included in this message. + IReadOnlyDictionary Reactions { get; } + + /// Adds a reaction to this message. + Task AddReactionAsync(Emoji emoji, RequestOptions options = null); + /// Adds a reaction to this message. + Task AddReactionAsync(string emoji, RequestOptions options = null); + /// Removes a reaction from message. + Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null); + /// Removes a reaction from this message. + Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null); + /// Removes all reactions from this message. + Task RemoveAllReactionsAsync(RequestOptions options = null); + Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); + /// Transforms this message's text into a human readable form by resolving its tags. string Resolve( TagHandling userHandling = TagHandling.Name, diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 5bedfbfae..7698390f1 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -11,6 +11,7 @@ //ManageGuild = 5, //Text + AddReactions = 6, ReadMessages = 10, SendMessages = 11, SendTTSMessages = 12, diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index bada89a32..06635feed 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -10,7 +10,7 @@ namespace Discord //TODO: C#7 Candidate for binary literals private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001011100110000000000", 2)); private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000010001", 2)); - private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000001111111110000010001", 2)); + private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000001111111110001010001", 2)); private static ChannelPermissions _allGroup { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001111110110000000000", 2)); /// Gets a blank ChannelPermissions that grants no permissions. @@ -35,6 +35,8 @@ namespace Discord /// If True, a user may create, delete and modify this channel. public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannel); + /// If true, a user may add reactions. + public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); /// If True, a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, ChannelPermission.ReadMessages); /// If True, a user may send messages. diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index e74a4da49..3975c1b8b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -11,6 +11,7 @@ ManageGuild = 5, //Text + AddReactions = 6, ReadMessages = 10, SendMessages = 11, SendTTSMessages = 12, diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 5941fde97..92fca96bd 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -11,7 +11,7 @@ namespace Discord public static readonly GuildPermissions None = new GuildPermissions(); /// Gets a GuildPermissions that grants all permissions. //TODO: C#7 Candidate for binary literals - public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110000111111", 2)); + public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110001111111", 2)); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } @@ -28,7 +28,9 @@ namespace Discord public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); /// If True, a user may adjust guild properties. public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); - + + /// If true, a user may add reactions. + public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); /// If True, a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, GuildPermission.ReadMessages); /// If True, a user may send messages. diff --git a/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs b/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs index a2e409292..fa22da656 100644 --- a/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs +++ b/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs @@ -16,7 +16,7 @@ namespace Discord.Net.Converters { object value = reader.Value; if (value != null) - return ulong.Parse((string)value, NumberStyles.None, CultureInfo.InvariantCulture); + return ulong.Parse(value.ToString(), NumberStyles.None, CultureInfo.InvariantCulture); else return null; } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 44d4e381b..e88a006b8 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -43,6 +43,7 @@ namespace Discord if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } } + private static ArgumentException CreateNotEmptyException(string name, string msg) { diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 91ca2ce1b..e7115babd 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -28,6 +28,34 @@ namespace Discord.Rest await client.ApiClient.DeleteMessageAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } + public static async Task AddReactionAsync(IMessage msg, Emoji emoji, BaseDiscordClient client, RequestOptions options) + => await AddReactionAsync(msg, $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); + public static async Task AddReactionAsync(IMessage msg, string emoji, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emoji, options).ConfigureAwait(false); + } + + public static async Task RemoveReactionAsync(IMessage msg, IUser user, Emoji emoji, BaseDiscordClient client, RequestOptions options) + => await RemoveReactionAsync(msg, user, $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); + public static async Task RemoveReactionAsync(IMessage msg, IUser user, string emoji, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emoji, options).ConfigureAwait(false); + } + + public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); + } + + public static async Task> GetReactionUsersAsync(IMessage msg, string emoji, + Action func, BaseDiscordClient client, RequestOptions options) + { + var args = new GetReactionUsersParams(); + func(args); + return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => u as IUser).Where(u => u != null).ToImmutableArray(); + } + public static async Task PinAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs new file mode 100644 index 000000000..7512dd4d8 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Reaction; + +namespace Discord +{ + public class RestReaction : IReaction + { + internal RestReaction(Model model) + { + Emoji = Emoji.FromApi(model.Emoji); + Count = model.Count; + Me = model.Me; + } + + public Emoji Emoji { get; private set; } + public int Count { get; private set; } + public bool Me { get; private set; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index de90448c7..b95253b8e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -17,6 +17,7 @@ namespace Discord.Rest private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; + private ImmutableArray _reactions; public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; @@ -28,6 +29,7 @@ namespace Discord.Rest public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; + public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => x.Count); internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) : base(discord, id, channel, author) @@ -100,6 +102,20 @@ namespace Discord.Rest } } + if (model.Reactions.IsSpecified) + { + var value = model.Reactions.Value; + if (value.Length > 0) + { + var reactions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + reactions.Add(new RestReaction(value[i])); + _reactions = reactions.ToImmutable(); + } + else + _reactions = ImmutableArray.Create(); + } + if (model.Content.IsSpecified) { var text = model.Content.Value; @@ -116,6 +132,23 @@ namespace Discord.Rest Update(model); } + public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emoji, Discord, options); + public Task AddReactionAsync(string emoji, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emoji, Discord, options); + + public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); + public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); + + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + + public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + + public Task PinAsync(RequestOptions options) => MessageHelper.PinAsync(this, Discord, options); public Task UnpinAsync(RequestOptions options) diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 3fc489ff9..c03dd7ab7 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -30,6 +30,7 @@ namespace Discord.Rpc public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUserIds => MessageHelper.FilterTagsByKey(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; + public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); internal RpcUserMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) : base(discord, id, channel, author) @@ -101,6 +102,22 @@ namespace Discord.Rpc public Task ModifyAsync(Action func, RequestOptions options) => MessageHelper.ModifyAsync(this, Discord, func, options); + public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emoji, Discord, options); + public Task AddReactionAsync(string emoji, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emoji, Discord, options); + + public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); + public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); + + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + + public Task> GetReactionUsersAsync(string emoji, int limit, ulong? afterUserId, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + public Task PinAsync(RequestOptions options) => MessageHelper.PinAsync(this, Discord, options); public Task UnpinAsync(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/API/Gateway/GatewayReaction.cs b/src/Discord.Net.WebSocket/API/Gateway/GatewayReaction.cs new file mode 100644 index 000000000..2b9d6becc --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/GatewayReaction.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GatewayReaction + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs new file mode 100644 index 000000000..944a6e7c9 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class RemoveAllReactionsEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index 529caaa87..a150a6d15 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -71,6 +71,24 @@ namespace Discord.WebSocket remove { _messageUpdatedEvent.Remove(value); } } private readonly AsyncEvent, SocketMessage, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, Task>>(); + public event Func, SocketReaction, Task> ReactionAdded + { + add { _reactionAddedEvent.Add(value); } + remove { _reactionAddedEvent.Remove(value); } + } + private readonly AsyncEvent, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, SocketReaction, Task>>(); + public event Func, SocketReaction, Task> ReactionRemoved + { + add { _reactionRemovedEvent.Add(value); } + remove { _reactionRemovedEvent.Remove(value); } + } + private readonly AsyncEvent, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, SocketReaction, Task>>(); + public event Func, Task> ReactionsCleared + { + add { _reactionsClearedEvent.Add(value); } + remove { _reactionsClearedEvent.Remove(value); } + } + private readonly AsyncEvent, Task>> _reactionsClearedEvent = new AsyncEvent, Task>>(); //Roles public event Func RoleCreated diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3e208ce58..fc3094871 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1308,6 +1308,84 @@ namespace Discord.WebSocket } } break; + case "MESSAGE_REACTION_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; + if (channel != null) + { + SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); + SocketReaction reaction = new SocketReaction(data, channel, Optional.Create(cachedMsg), Optional.Create(user)); + + if (cachedMsg != null) + { + cachedMsg.AddReaction(reaction); + await _reactionAddedEvent.InvokeAsync(data.MessageId, cachedMsg, reaction).ConfigureAwait(false); + return; + } + await _reactionAddedEvent.InvokeAsync(data.MessageId, Optional.Create(), reaction).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_REACTION_ADD referenced an unknown channel.").ConfigureAwait(false); + return; + } + break; + } + case "MESSAGE_REACTION_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; + if (channel != null) + { + SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); + SocketReaction reaction = new SocketReaction(data, channel, Optional.Create(cachedMsg), Optional.Create(user)); + if (cachedMsg != null) + { + cachedMsg.RemoveReaction(reaction); + await _reactionRemovedEvent.InvokeAsync(data.MessageId, cachedMsg, reaction).ConfigureAwait(false); + return; + } + await _reactionRemovedEvent.InvokeAsync(data.MessageId, Optional.Create(), reaction).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE referenced an unknown channel.").ConfigureAwait(false); + return; + } + break; + } + case "MESSAGE_REACTION_REMOVE_ALL": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; + if (channel != null) + { + SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + if (cachedMsg != null) + { + cachedMsg.ClearReactions(); + await _reactionsClearedEvent.InvokeAsync(data.MessageId, cachedMsg).ConfigureAwait(false); + return; + } + await _reactionsClearedEvent.InvokeAsync(data.MessageId, Optional.Create()); + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE_ALL referenced an unknown channel.").ConfigureAwait(false); + return; + } + + break; + } case "MESSAGE_DELETE_BULK": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs new file mode 100644 index 000000000..c2e544a2c --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Gateway.GatewayReaction; + +namespace Discord.WebSocket +{ + public class SocketReaction : IReaction + { + internal SocketReaction(Model model, ISocketMessageChannel channel, Optional message, Optional user) + { + Channel = channel; + Message = message; + MessageId = model.MessageId; + User = user; + UserId = model.UserId; + Emoji = Emoji.FromApi(model.Emoji); + } + + public ulong UserId { get; private set; } + public Optional User { get; private set; } + public ulong MessageId { get; private set; } + public Optional Message { get; private set; } + public ISocketMessageChannel Channel { get; private set; } + public Emoji Emoji { get; private set; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 2261076f7..e8a1fa079 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Threading.Tasks; +using System.Linq; +using Discord.API.Gateway; using Model = Discord.API.Message; namespace Discord.WebSocket @@ -18,6 +20,7 @@ namespace Discord.WebSocket private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; + private List _reactions = new List(); public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; @@ -29,6 +32,7 @@ namespace Discord.WebSocket public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => x.Count()); internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) : base(discord, id, channel, author) @@ -109,10 +113,39 @@ namespace Discord.WebSocket model.Content = text; } } + internal void AddReaction(SocketReaction reaction) + { + _reactions.Add(reaction); + } + internal void RemoveReaction(SocketReaction reaction) + { + if (_reactions.Contains(reaction)) + _reactions.Remove(reaction); + } + internal void ClearReactions() + { + _reactions.Clear(); + } public Task ModifyAsync(Action func, RequestOptions options = null) => MessageHelper.ModifyAsync(this, Discord, func, options); + public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emoji, Discord, options); + public Task AddReactionAsync(string emoji, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emoji, Discord, options); + + public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); + public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); + + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + + public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); public Task UnpinAsync(RequestOptions options = null)