From 67c50944625fa71e32adc3f637fbf60493137592 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Mon, 23 May 2022 03:44:42 -0300 Subject: [PATCH] more work on cache provider --- .../Cache/CacheableEntityExtensions.cs | 19 +- .../Models/Application/IPartialApplicationModel.cs | 16 + .../Message/Components/IMessageComponentModel.cs | 35 ++ .../Components/IMessageComponentOptionModel.cs | 22 ++ .../Cache/Models/Message/IAttachmentModel.cs | 21 ++ .../Cache/Models/Message/IEmbedModel.cs | 44 +++ .../Cache/Models/Message/IMessageActivityModel.cs | 14 + .../Cache/Models/Message/IMessageModel.cs | 28 +- .../Cache/Models/Message/IReactionModel.cs | 14 + .../Cache/Models/Message/IStickerModel.cs | 15 + src/Discord.Net.Core/Entities/Messages/IMessage.cs | 5 + .../Entities/Messages/MessageInteraction.cs | 28 +- src/Discord.Net.Core/Utils/Optional.cs | 4 + .../API/Common/ActionRowComponent.cs | 24 +- src/Discord.Net.Rest/API/Common/Attachment.cs | 13 +- src/Discord.Net.Rest/API/Common/ButtonComponent.cs | 24 +- src/Discord.Net.Rest/API/Common/Embed.cs | 12 +- src/Discord.Net.Rest/API/Common/EmbedAuthor.cs | 2 +- src/Discord.Net.Rest/API/Common/EmbedField.cs | 4 +- src/Discord.Net.Rest/API/Common/EmbedFooter.cs | 2 +- src/Discord.Net.Rest/API/Common/EmbedImage.cs | 5 +- src/Discord.Net.Rest/API/Common/EmbedProvider.cs | 2 +- src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs | 5 +- src/Discord.Net.Rest/API/Common/EmbedVideo.cs | 7 +- src/Discord.Net.Rest/API/Common/Message.cs | 35 +- src/Discord.Net.Rest/API/Common/MessageActivity.cs | 5 +- .../API/Common/MessageApplication.cs | 7 +- src/Discord.Net.Rest/API/Common/Reaction.cs | 8 +- .../API/Common/SelectMenuComponent.cs | 25 +- .../API/Common/SelectMenuOption.cs | 10 +- src/Discord.Net.Rest/API/Common/StickerItem.cs | 7 +- .../API/Common/TextInputComponent.cs | 26 +- src/Discord.Net.Rest/ClientHelper.cs | 10 + src/Discord.Net.Rest/DiscordRestClient.cs | 3 + .../Entities/Messages/Attachment.cs | 12 +- .../Entities/Messages/MessageHelper.cs | 10 +- .../Extensions/EntityExtensions.cs | 59 +-- .../ClientStateManager.Experiment.cs | 149 ++++++-- .../Entities/Messages/SocketMessage.cs | 413 ++++++++++++++++++--- .../Entities/Messages/SocketReaction.cs | 55 +-- .../Entities/Messages/SocketUserMessage.cs | 72 ++-- .../Entities/Stickers/SocketUnknownSticker.cs | 4 +- .../Extensions/CacheModelExtensions.cs | 22 +- .../Extensions/EntityCacheExtensions.cs | 16 + .../Extensions/EntityExtensions.cs | 12 + 45 files changed, 1066 insertions(+), 259 deletions(-) create mode 100644 src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs create mode 100644 src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs diff --git a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs index 6e6b0945f..d92e5b211 100644 --- a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs +++ b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs @@ -78,17 +78,14 @@ namespace Discord }; } - public static IEmojiModel ToModel(this IEmote emote) where TModel : IEmojiModel, new() + public static IEmojiModel ToModel(this IEmote emote, IEmojiModel model) { if (emote == null) return null; - var model = new TModel() - { - Name = emote.Name - }; + model.Name = emote.Name; - if(emote is GuildEmote guildEmote) + if (emote is GuildEmote guildEmote) { model.Id = guildEmote.Id; model.IsAnimated = guildEmote.Animated; @@ -99,7 +96,7 @@ namespace Discord model.Roles = guildEmote.RoleIds.ToArray(); } - if(emote is Emote e) + if (emote is Emote e) { model.IsAnimated = e.Animated; model.Id = e.Id; @@ -107,5 +104,13 @@ namespace Discord return model; } + + public static IEmojiModel ToModel(this IEmote emote) where TModel : IEmojiModel, new() + { + if (emote == null) + return null; + + return emote.ToModel(new TModel()); + } } } diff --git a/src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs b/src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs new file mode 100644 index 000000000..fd9c66add --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IPartialApplicationModel : IEntityModel + { + string Name { get; set; } + string Icon { get; set; } + string Description { get; set; } + string CoverImage { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs b/src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs new file mode 100644 index 000000000..f7e9efe97 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IMessageComponentModel + { + ComponentType Type { get; set; } + string CustomId { get; set; } + bool? Disabled { get; set; } + ButtonStyle? Style { get; set; } + string Label { get; set; } + + // emoji + ulong? EmojiId { get; set; } + string EmojiName { get; set; } + bool? EmojiAnimated { get; set; } + + string Url { get; set; } + + IMessageComponentOptionModel[] Options { get; set; } + + string Placeholder { get; set; } + int? MinValues { get; set; } + int? MaxValues { get; set; } + IMessageComponentModel[] Components { get; set; } + int? MinLength { get; set; } + int? MaxLength { get; set; } + bool? Required { get; set; } + string Value { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs b/src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs new file mode 100644 index 000000000..556e0b0c7 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IMessageComponentOptionModel + { + string Label { get; set; } + string Value { get; set; } + string Description { get; set; } + + // emoji + ulong? EmojiId { get; set; } + string EmojiName { get; set; } + bool? EmojiAnimated { get; set; } + + bool? Default { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs new file mode 100644 index 000000000..fec9d4e51 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IAttachmentModel : IEntityModel + { + string FileName { get; set; } + string Description { get; set; } + string ContentType { get; set; } + int Size { get; set; } + string Url { get; set; } + string ProxyUrl { get; set; } + int? Height { get; set; } + int? Width { get; set; } + bool Ephemeral { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs new file mode 100644 index 000000000..64ec78bf8 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IEmbedModel + { + string Title { get; set; } + EmbedType Type { get; set; } + string Description { get; set; } + string Url { get; set; } + long? Timestamp { get; set; } + uint? Color { get; set; } + string FooterText { get; set; } + string FooterIconUrl { get; set; } + string FooterProxyUrl { get; set; } + string ProviderName { get; set; } + string ProviderUrl { get; set; } + string AuthorName { get; set; } + string AuthorUrl { get; set; } + string AuthorIconUrl { get; set; } + string AuthorProxyIconUrl { get; set; } + IEmbedMediaModel Image { get; set; } + IEmbedMediaModel Thumbnail { get; set; } + IEmbedMediaModel Video { get; set; } + IEmbedFieldModel[] Fields { get; set; } + } + public interface IEmbedMediaModel + { + string Url { get; set; } + string ProxyUrl { get; set; } + int? Height { get; set; } + int? Width { get; set; } + } + public interface IEmbedFieldModel + { + string Name { get; set; } + string Value { get; set; } + bool Inline { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs new file mode 100644 index 000000000..c88f217d8 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IMessageActivityModel + { + MessageActivityType? Type { get; set; } + string PartyId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs index c1dc6001c..ca718ec66 100644 --- a/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs @@ -14,11 +14,35 @@ namespace Discord ulong AuthorId { get; set; } bool IsWebhookMessage { get; set; } string Content { get; set; } - DateTimeOffset Timestamp { get; set; } - DateTimeOffset? EditedTimestamp { get; set; } + long Timestamp { get; set; } + long? EditedTimestamp { get; set; } bool IsTextToSpeech { get; set; } bool MentionEveryone { get; set; } ulong[] UserMentionIds { get; set; } ulong[] RoleMentionIds { get; set; } + + IAttachmentModel[] Attachments { get; set; } + IEmbedModel[] Embeds { get; set; } + IReactionMetadataModel[] Reactions { get; set; } + bool Pinned { get; set; } + IMessageActivityModel Activity { get; set; } + IPartialApplicationModel Application { get; set; } + ulong? ApplicationId { get; set; } + + // message reference + ulong? ReferenceMessageId { get; set; } + ulong? ReferenceMessageChannelId { get; set; } + ulong? ReferenceMessageGuildId { get; set; } + + MessageFlags Flags { get; set; } + + // interaction + ulong? InteractionId { get; set; } + string InteractionName { get; set; } + InteractionType? InteractionType { get; set; } + ulong? InteractionUserId { get; set; } + + IMessageComponentModel[] Components { get; set; } + IStickerItemModel[] Stickers { get; set; } } } diff --git a/src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs new file mode 100644 index 000000000..f4215d391 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IReactionMetadataModel + { + IEmojiModel Emoji { get; set; } + ulong[] Users { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs new file mode 100644 index 000000000..db32d8522 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IStickerItemModel + { + ulong Id { get; set; } + string Name { get; set; } + StickerFormatType Format { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index f5f2ca007..7992544c2 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -46,6 +46,11 @@ namespace Discord /// bool MentionedEveryone { get; } /// + /// If the message is a or application-owned webhook, + /// this is the id of the application. + /// + ulong? ApplicationId { get; } + /// /// Gets the content for this message. /// /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs b/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs index cbbebd932..fce03e9da 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs @@ -10,7 +10,7 @@ namespace Discord /// Represents a partial within a message. /// /// The type of the user. - public class MessageInteraction : IMessageInteraction where TUser : IUser + public class MessageInteraction : IMessageInteraction where TUser : class, IUser { /// /// Gets the snowflake id of the interaction. @@ -30,14 +30,36 @@ namespace Discord /// /// Gets the who invoked the interaction. /// - public TUser User { get; } + /// + /// When this property is a SocketUser, the get accessor will attempt to preform a + /// synchronous cache lookup. + /// + public TUser User + => _user ?? (_userLookup != null ? _userLookup(UserId) : null); + /// + /// Gets the id of the user who invoked the interaction. + /// + public ulong UserId { get; } + + private readonly TUser _user; + private readonly Func _userLookup; internal MessageInteraction(ulong id, InteractionType type, string name, TUser user) { Id = id; Type = type; Name = name; - User = user; + _user = user; + UserId = user.Id; + } + + internal MessageInteraction(ulong id, InteractionType type, string name, ulong userId, Func lookup) + { + Id = id; + Type = type; + Name = name; + UserId = userId; + _userLookup = lookup; } IUser IMessageInteraction.User => User; diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index 985be9240..8dc95d352 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -56,5 +56,9 @@ namespace Discord public static T? ToNullable(this Optional val) where T : struct => val.IsSpecified ? val.Value : null; + + public static Optional ToOptional(this T? value) + where T : struct + => value.HasValue ? new Optional(value.Value) : new(); } } diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs index 9a7eb80dd..7213a40e2 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Discord.API { - internal class ActionRowComponent : IMessageComponent + internal class ActionRowComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -29,5 +29,27 @@ namespace Discord.API [JsonIgnore] string IMessageComponent.CustomId => null; + + ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } + IMessageComponentModel[] IMessageComponentModel.Components { get => Components.Select(x => x as IMessageComponentModel).ToArray(); set => throw new System.NotSupportedException(); } // cursed hack here + + #region unused + string IMessageComponentModel.CustomId { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Disabled { get => null; set => throw new System.NotSupportedException(); } + ButtonStyle? IMessageComponentModel.Style { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Label { get => null; set => throw new System.NotSupportedException(); } + ulong? IMessageComponentModel.EmojiId { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.EmojiName { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.EmojiAnimated { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Url { get => null; set => throw new System.NotSupportedException(); } + IMessageComponentOptionModel[] IMessageComponentModel.Options { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Placeholder { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinValues { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxValues { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinLength { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxLength { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Required { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Value { get => null; set => throw new System.NotSupportedException(); } + #endregion } } diff --git a/src/Discord.Net.Rest/API/Common/Attachment.cs b/src/Discord.Net.Rest/API/Common/Attachment.cs index 7970dc9a5..449326377 100644 --- a/src/Discord.Net.Rest/API/Common/Attachment.cs +++ b/src/Discord.Net.Rest/API/Common/Attachment.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class Attachment + internal class Attachment : IAttachmentModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -24,5 +24,16 @@ namespace Discord.API public Optional Width { get; set; } [JsonProperty("ephemeral")] public Optional Ephemeral { get; set; } + + string IAttachmentModel.FileName { get => Filename; set => throw new System.NotSupportedException(); } + string IAttachmentModel.Description { get => Description.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + string IAttachmentModel.ContentType { get => ContentType.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + int IAttachmentModel.Size { get => Size; set => throw new System.NotSupportedException(); } + string IAttachmentModel.Url { get => Url; set => throw new System.NotSupportedException(); } + string IAttachmentModel.ProxyUrl { get => ProxyUrl; set => throw new System.NotSupportedException(); } + int? IAttachmentModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } + int? IAttachmentModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } + bool IAttachmentModel.Ephemeral { get => Ephemeral.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + ulong IEntityModel.Id { get => Id; set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs index 7f737d7ad..e81a95fdf 100644 --- a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class ButtonComponent : IMessageComponent + internal class ButtonComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -59,5 +59,27 @@ namespace Discord.API [JsonIgnore] string IMessageComponent.CustomId => CustomId.GetValueOrDefault(); + + ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.CustomId { get => CustomId.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Disabled { get => Disabled.ToNullable(); set => throw new System.NotSupportedException(); } + ButtonStyle? IMessageComponentModel.Style { get => Style; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Label { get => Label.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + ulong? IMessageComponentModel.EmojiId { get => Emote.GetValueOrDefault()?.Id; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.EmojiName { get => Emote.GetValueOrDefault()?.Name; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.EmojiAnimated { get => Emote.GetValueOrDefault()?.Animated; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Url { get => Url.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + + #region unused + IMessageComponentOptionModel[] IMessageComponentModel.Options { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Placeholder { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinValues { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxValues { get => null; set => throw new System.NotSupportedException(); } + IMessageComponentModel[] IMessageComponentModel.Components { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinLength { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxLength { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Required { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Value { get => null; set => throw new System.NotSupportedException(); } + #endregion } } diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index 77efa12aa..15286a7f7 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -4,7 +4,7 @@ using Discord.Net.Converters; namespace Discord.API { - internal class Embed + internal class Embed : IEmbedModel { [JsonProperty("title")] public string Title { get; set; } @@ -32,5 +32,15 @@ namespace Discord.API public Optional Provider { get; set; } [JsonProperty("fields")] public Optional Fields { get; set; } + + EmbedType IEmbedModel.Type { get => Type; set => throw new NotSupportedException(); } + DateTimeOffset? IEmbedModel.Timestamp { get => Timestamp; set => throw new NotSupportedException(); } + IEmbedFooterModel IEmbedModel.Footer { get => Footer.GetValueOrDefault(); set => throw new NotSupportedException(); } + IEmbedMediaModel IEmbedModel.Image { get => Image.GetValueOrDefault(); set => throw new NotSupportedException(); } + IEmbedMediaModel IEmbedModel.Thumbnail { get => Thumbnail.GetValueOrDefault(); set => throw new NotSupportedException(); } + IEmbedMediaModel IEmbedModel.Video { get => Video.GetValueOrDefault(); set => throw new NotSupportedException(); } + IEmbedProviderModel IEmbedModel.Provider { get => Provider.GetValueOrDefault(); set => throw new NotSupportedException(); } + IEmbedAuthorModel IEmbedModel.Author { get => Author.GetValueOrDefault(); set => throw new NotSupportedException(); } + IEmbedFieldModel[] IEmbedModel.Fields { get => Fields.GetValueOrDefault(); set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index d7f3ae68d..66d15e313 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class EmbedAuthor + internal class EmbedAuthor : IEmbedAuthorModel { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/EmbedField.cs b/src/Discord.Net.Rest/API/Common/EmbedField.cs index 6ce810f1a..d292b2aef 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedField.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedField.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { - internal class EmbedField + internal class EmbedField : IEmbedFieldModel { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs index cd08e7e26..a4c5ed830 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class EmbedFooter + internal class EmbedFooter : IEmbedFooterModel { [JsonProperty("text")] public string Text { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs index 6b5db0681..513df20cd 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class EmbedImage + internal class EmbedImage : IEmbedMediaModel { [JsonProperty("url")] public string Url { get; set; } @@ -12,5 +12,8 @@ namespace Discord.API public Optional Height { get; set; } [JsonProperty("width")] public Optional Width { get; set; } + + int? IEmbedMediaModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } + int? IEmbedMediaModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs index ed0f7c3c8..e9a737149 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class EmbedProvider + internal class EmbedProvider : IEmbedProviderModel { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs index dd25a1a26..9a9603cb6 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class EmbedThumbnail + internal class EmbedThumbnail : IEmbedMediaModel { [JsonProperty("url")] public string Url { get; set; } @@ -12,5 +12,8 @@ namespace Discord.API public Optional Height { get; set; } [JsonProperty("width")] public Optional Width { get; set; } + + int? IEmbedMediaModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } + int? IEmbedMediaModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs index f668217f0..0b9c1214a 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -2,13 +2,18 @@ using Newtonsoft.Json; namespace Discord.API { - internal class EmbedVideo + internal class EmbedVideo : IEmbedMediaModel { [JsonProperty("url")] public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] public Optional Width { get; set; } + + int? IEmbedMediaModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } + int? IEmbedMediaModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index d33a03fe5..308e25ccf 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -1,9 +1,10 @@ using Newtonsoft.Json; using System; +using System.Linq; namespace Discord.API { - internal class Message + internal class Message : IMessageModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -49,6 +50,8 @@ namespace Discord.API // sent with Rich Presence-related chat embeds [JsonProperty("application")] public Optional Application { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationId { get; set; } [JsonProperty("message_reference")] public Optional Reference { get; set; } [JsonProperty("flags")] @@ -62,5 +65,35 @@ namespace Discord.API public Optional Interaction { get; set; } [JsonProperty("sticker_items")] public Optional StickerItems { get; set; } + + + MessageType IMessageModel.Type { get => Type; set => throw new NotSupportedException(); } + ulong IMessageModel.ChannelId { get => ChannelId; set => throw new NotSupportedException(); } + ulong? IMessageModel.GuildId { get => GuildId.ToNullable(); set => throw new NotSupportedException(); } + ulong IMessageModel.AuthorId { get => Author.IsSpecified ? Author.Value.Id : Member.IsSpecified ? Member.Value.User.Id : WebhookId.GetValueOrDefault(); set => throw new NotSupportedException(); } + bool IMessageModel.IsWebhookMessage { get => WebhookId.IsSpecified; set => throw new NotSupportedException(); } + string IMessageModel.Content { get => Content.GetValueOrDefault(); set => throw new NotSupportedException(); } + DateTimeOffset IMessageModel.Timestamp { get => Timestamp.Value; set => throw new NotSupportedException(); } // might break? + DateTimeOffset? IMessageModel.EditedTimestamp { get => Timestamp.ToNullable(); set => throw new NotSupportedException(); } + bool IMessageModel.IsTextToSpeech { get => IsTextToSpeech.GetValueOrDefault(); set => throw new NotSupportedException(); } + bool IMessageModel.MentionEveryone { get => MentionEveryone.GetValueOrDefault(); set => throw new NotSupportedException(); } + ulong[] IMessageModel.UserMentionIds { get => UserMentions.IsSpecified ? UserMentions.Value.Select(x => x.Id).ToArray() : Array.Empty(); set => throw new NotSupportedException(); } + IAttachmentModel[] IMessageModel.Attachments { get => Attachments.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); } + IEmbedModel[] IMessageModel.Embeds { get => Embeds.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); } + IReactionMetadataModel[] IMessageModel.Reactions { get => Reactions.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); } + bool IMessageModel.Pinned { get => Pinned.GetValueOrDefault(); set => throw new NotSupportedException(); } + IMessageActivityModel IMessageModel.Activity { get => Activity.GetValueOrDefault(); set => throw new NotSupportedException(); } + IPartialApplicationModel IMessageModel.Application { get => Application.GetValueOrDefault(); set => throw new NotSupportedException(); } + ulong? IMessageModel.ApplicationId { get => ApplicationId.ToNullable(); set => throw new NotSupportedException(); } + ulong? IMessageModel.ReferenceMessageId { get => ReferencedMessage.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); } + ulong? IMessageModel.ReferenceMessageChannelId { get => ReferencedMessage.GetValueOrDefault()?.ChannelId; set => throw new NotSupportedException(); } + MessageFlags IMessageModel.Flags { get => Flags.GetValueOrDefault(); set => throw new NotSupportedException(); } + ulong? IMessageModel.InteractionId { get => Interaction.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); } + string IMessageModel.InteractionName { get => Interaction.GetValueOrDefault()?.Name; set => throw new NotSupportedException(); } + InteractionType? IMessageModel.InteractionType { get => Interaction.GetValueOrDefault()?.Type; set => throw new NotSupportedException(); } + ulong? IMessageModel.InteractionUserId { get => Interaction.GetValueOrDefault()?.User.Id; set => throw new NotSupportedException(); } + IMessageComponentModel[] IMessageModel.Components { get => Components.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); } + IStickerItemModel[] IMessageModel.Stickers { get => StickerItems.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); } + ulong IEntityModel.Id { get => Id; set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageActivity.cs b/src/Discord.Net.Rest/API/Common/MessageActivity.cs index 701f6fc03..0dcb522ca 100644 --- a/src/Discord.Net.Rest/API/Common/MessageActivity.cs +++ b/src/Discord.Net.Rest/API/Common/MessageActivity.cs @@ -7,11 +7,14 @@ using System.Threading.Tasks; namespace Discord.API { - public class MessageActivity + public class MessageActivity : IMessageActivityModel { [JsonProperty("type")] public Optional Type { get; set; } [JsonProperty("party_id")] public Optional PartyId { get; set; } + + MessageActivityType? IMessageActivityModel.Type { get => Type.ToNullable(); set => throw new NotSupportedException(); } + string IMessageActivityModel.PartyId { get => PartyId.GetValueOrDefault(); set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageApplication.cs b/src/Discord.Net.Rest/API/Common/MessageApplication.cs index 7302185ad..08af48c94 100644 --- a/src/Discord.Net.Rest/API/Common/MessageApplication.cs +++ b/src/Discord.Net.Rest/API/Common/MessageApplication.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Discord.API { - public class MessageApplication + internal class MessageApplication : IPartialApplicationModel { /// /// Gets the snowflake ID of the application. @@ -34,5 +34,10 @@ namespace Discord.API /// [JsonProperty("name")] public string Name { get; set; } + + string IPartialApplicationModel.CoverImage { get => CoverImage; set => throw new NotSupportedException(); } + string IPartialApplicationModel.Icon { get => Icon; set => throw new NotSupportedException(); } + string IPartialApplicationModel.Name { get => Name; set => throw new NotSupportedException(); } + ulong IEntityModel.Id { get => Id; set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/Reaction.cs b/src/Discord.Net.Rest/API/Common/Reaction.cs index 4d368ab2d..f3b3d218d 100644 --- a/src/Discord.Net.Rest/API/Common/Reaction.cs +++ b/src/Discord.Net.Rest/API/Common/Reaction.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { - internal class Reaction + internal class Reaction : IReactionMetadataModel { [JsonProperty("count")] public int Count { get; set; } @@ -10,5 +10,9 @@ namespace Discord.API public bool Me { get; set; } [JsonProperty("emoji")] public Emoji Emoji { get; set; } + + int IReactionMetadataModel.Count { get => Count; set => throw new System.NotSupportedException(); } + bool IReactionMetadataModel.Me { get => Me; set => throw new System.NotSupportedException(); } + IEmojiModel IReactionMetadataModel.Emoji { get => Emoji; set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs index 25ac476c5..6a43bdca3 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -3,7 +3,7 @@ using System.Linq; namespace Discord.API { - internal class SelectMenuComponent : IMessageComponent + internal class SelectMenuComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -28,6 +28,7 @@ namespace Discord.API [JsonProperty("values")] public Optional Values { get; set; } + public SelectMenuComponent() { } public SelectMenuComponent(Discord.SelectMenuComponent component) @@ -40,5 +41,27 @@ namespace Discord.API MaxValues = component.MaxValues; Disabled = component.IsDisabled; } + + ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.CustomId { get => CustomId; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Disabled { get => Disabled; set => throw new System.NotSupportedException(); } + IMessageComponentOptionModel[] IMessageComponentModel.Options { get => Options; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Placeholder { get => Placeholder.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinValues { get => MinValues; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxValues { get => MaxValues; set => throw new System.NotSupportedException(); } + + #region unused + ButtonStyle? IMessageComponentModel.Style { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Label { get => null; set => throw new System.NotSupportedException(); } + ulong? IMessageComponentModel.EmojiId { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.EmojiName { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.EmojiAnimated { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Url { get => null; set => throw new System.NotSupportedException(); } + IMessageComponentModel[] IMessageComponentModel.Components { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinLength { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxLength { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Required { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Value { get => null; set => throw new System.NotSupportedException(); } + #endregion } } diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs index d0a25a829..5e8f9a958 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class SelectMenuOption + internal class SelectMenuOption : IMessageComponentOptionModel { [JsonProperty("label")] public string Label { get; set; } @@ -49,5 +49,13 @@ namespace Discord.API Default = option.IsDefault ?? Optional.Unspecified; } + + string IMessageComponentOptionModel.Label { get => Label; set => throw new System.NotSupportedException(); } + string IMessageComponentOptionModel.Value { get => Value; set => throw new System.NotSupportedException(); } + string IMessageComponentOptionModel.Description { get => Description.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + ulong? IMessageComponentOptionModel.EmojiId { get => Emoji.GetValueOrDefault()?.Id; set => throw new System.NotSupportedException(); } + string IMessageComponentOptionModel.EmojiName { get => Emoji.GetValueOrDefault()?.Name; set => throw new System.NotSupportedException(); } + bool? IMessageComponentOptionModel.EmojiAnimated { get => Emoji.GetValueOrDefault()?.Animated; set => throw new System.NotSupportedException(); } + bool? IMessageComponentOptionModel.Default { get => Default.ToNullable(); set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/StickerItem.cs b/src/Discord.Net.Rest/API/Common/StickerItem.cs index 4b24f711b..b9a3c8780 100644 --- a/src/Discord.Net.Rest/API/Common/StickerItem.cs +++ b/src/Discord.Net.Rest/API/Common/StickerItem.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class StickerItem + internal class StickerItem : IStickerItemModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -12,5 +12,10 @@ namespace Discord.API [JsonProperty("format_type")] public StickerFormatType FormatType { get; set; } + + + ulong IStickerItemModel.Id { get => Id; set => throw new System.NotSupportedException(); } + string IStickerItemModel.Name { get => Name; set => throw new System.NotSupportedException(); } + StickerFormatType IStickerItemModel.Format { get => FormatType; set => throw new System.NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs index a475345fc..225e83167 100644 --- a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs +++ b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - internal class TextInputComponent : IMessageComponent + internal class TextInputComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -45,5 +45,29 @@ namespace Discord.API Required = component.Required ?? Optional.Unspecified; Value = component.Value ?? Optional.Unspecified; } + + ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.CustomId { get => CustomId; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinLength { get => MinLength.ToNullable(); set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxLength { get => MaxLength.ToNullable(); set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.Required { get => Required.ToNullable(); set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Value { get => Value.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Label { get => Label; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Placeholder { get => Placeholder.GetValueOrDefault(); set => throw new System.NotSupportedException(); } + + #region unused + + bool? IMessageComponentModel.Disabled { get => null; set => throw new System.NotSupportedException(); } + ButtonStyle? IMessageComponentModel.Style { get => null; set => throw new System.NotSupportedException(); } + ulong? IMessageComponentModel.EmojiId { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.EmojiName { get => null; set => throw new System.NotSupportedException(); } + bool? IMessageComponentModel.EmojiAnimated { get => null; set => throw new System.NotSupportedException(); } + string IMessageComponentModel.Url { get => null; set => throw new System.NotSupportedException(); } + IMessageComponentOptionModel[] IMessageComponentModel.Options { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MinValues { get => null; set => throw new System.NotSupportedException(); } + int? IMessageComponentModel.MaxValues { get => null; set => throw new System.NotSupportedException(); } + IMessageComponentModel[] IMessageComponentModel.Components { get => null; set => throw new System.NotSupportedException(); } + + #endregion } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index ab0238fee..a365cecf3 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -46,6 +46,16 @@ namespace Discord.Rest .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); } + public static async Task GetMessageAsync(BaseDiscordClient client, ulong channelId, ulong messageId, RequestOptions options) + { + var channel = await GetChannelAsync(client, channelId, options).ConfigureAwait(false); + + if (channel is not IRestMessageChannel msgChannel) + return null; + + return await msgChannel.GetMessageAsync(messageId, options).ConfigureAwait(false); + } + public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 6ae8bc0b9..023b71f4e 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -158,6 +158,9 @@ namespace Discord.Rest public Task> GetGroupChannelsAsync(RequestOptions options = null) => ClientHelper.GetGroupChannelsAsync(this, options); + public Task GetMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) + => ClientHelper.GetMessageAsync(this, channelId, messageId, options); + public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options); diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index a5b83fb7b..fbdab727b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Model = Discord.API.Attachment; +using Model = Discord.IAttachmentModel; namespace Discord { @@ -44,11 +44,11 @@ namespace Discord } internal static Attachment Create(Model model) { - return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size, - model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null, - model.Ephemeral.ToNullable(), model.Description.GetValueOrDefault(), - model.ContentType.GetValueOrDefault()); + return new Attachment(model.Id, model.FileName, model.Url, model.ProxyUrl, model.Size, + model.Height, + model.Width, + model.Ephemeral, model.Description, + model.ContentType); } /// diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 4d9ef008d..a0600cbc9 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -221,7 +221,7 @@ namespace Discord.Rest await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } - public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection userMentions) + public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, ulong[] userMentions) { var tags = ImmutableArray.CreateBuilder(); int index = 0; @@ -278,11 +278,9 @@ namespace Discord.Rest IUser mentionedUser = null; foreach (var mention in userMentions) { - if (mention.Id == id) + if (mention == id) { mentionedUser = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); - if (mentionedUser == null) - mentionedUser = mention; break; } } @@ -372,11 +370,11 @@ namespace Discord.Rest .ToImmutableArray(); } - public static MessageSource GetSource(Model msg) + public static MessageSource GetSource(IMessageModel msg) { if (msg.Type != MessageType.Default && msg.Type != MessageType.Reply) return MessageSource.System; - else if (msg.WebhookId.IsSpecified) + else if (msg.IsWebhookMessage) return MessageSource.Webhook; else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) return MessageSource.Bot; diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 5de5b7b5d..e688afd37 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -40,17 +41,25 @@ namespace Discord.Rest ImmutableArray.Create(model.Roles), model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); - public static Embed ToEntity(this API.Embed model) + public static Embed ToEntity(this IEmbedModel model) { - return new Embed(model.Type, model.Title, model.Description, model.Url, model.Timestamp, + return new Embed(model.Type, model.Title, model.Description, model.Url, + model.Timestamp.HasValue ? new DateTimeOffset(model.Timestamp.Value, TimeSpan.Zero) : null, model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, - model.Image.IsSpecified ? model.Image.Value.ToEntity() : (EmbedImage?)null, - model.Video.IsSpecified ? model.Video.Value.ToEntity() : (EmbedVideo?)null, - model.Author.IsSpecified ? model.Author.Value.ToEntity() : (EmbedAuthor?)null, - model.Footer.IsSpecified ? model.Footer.Value.ToEntity() : (EmbedFooter?)null, - model.Provider.IsSpecified ? model.Provider.Value.ToEntity() : (EmbedProvider?)null, - model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, - model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); + model.Image != null + ? new EmbedImage(model.Image.Url, model.Image.ProxyUrl, model.Image.Height, model.Image.Width) : (EmbedImage?)null, + model.Video != null + ? new EmbedVideo(model.Video.Url, model.Video.Height, model.Video.Width) : (EmbedVideo?)null, + model.AuthorIconUrl != null || model.AuthorName != null || model.AuthorProxyIconUrl != null || model.AuthorUrl != null + ? new EmbedAuthor(model.AuthorName, model.AuthorUrl, model.AuthorIconUrl, model.AuthorProxyIconUrl) : (EmbedAuthor?)null, + model.FooterIconUrl != null || model.FooterProxyUrl != null || model.FooterText != null + ? new EmbedFooter(model.FooterText, model.FooterIconUrl, model.FooterProxyUrl) : (EmbedFooter?)null, + model.ProviderUrl != null || model.ProviderName != null + ? new EmbedProvider(model.ProviderName, model.ProviderUrl) : (EmbedProvider?)null, + model.Thumbnail != null + ? new EmbedThumbnail(model.Thumbnail.Url, model.Thumbnail.ProxyUrl, model.Thumbnail.Height, model.Thumbnail.Width) : (EmbedThumbnail?)null, + model.Fields != null + ? model.Fields.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); } public static RoleTags ToEntity(this API.RoleTags model) { @@ -116,15 +125,11 @@ namespace Discord.Rest if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) yield return "users"; } - public static EmbedAuthor ToEntity(this API.EmbedAuthor model) - { - return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); - } public static API.EmbedAuthor ToModel(this EmbedAuthor entity) { return new API.EmbedAuthor { Name = entity.Name, Url = entity.Url, IconUrl = entity.IconUrl }; } - public static EmbedField ToEntity(this API.EmbedField model) + public static EmbedField ToEntity(this IEmbedFieldModel model) { return new EmbedField(model.Name, model.Value, model.Inline); } @@ -132,48 +137,22 @@ namespace Discord.Rest { return new API.EmbedField { Name = entity.Name, Value = entity.Value, Inline = entity.Inline }; } - public static EmbedFooter ToEntity(this API.EmbedFooter model) - { - return new EmbedFooter(model.Text, model.IconUrl, model.ProxyIconUrl); - } public static API.EmbedFooter ToModel(this EmbedFooter entity) { return new API.EmbedFooter { Text = entity.Text, IconUrl = entity.IconUrl }; } - public static EmbedImage ToEntity(this API.EmbedImage model) - { - return new EmbedImage(model.Url, model.ProxyUrl, - model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null); - } public static API.EmbedImage ToModel(this EmbedImage entity) { return new API.EmbedImage { Url = entity.Url }; } - public static EmbedProvider ToEntity(this API.EmbedProvider model) - { - return new EmbedProvider(model.Name, model.Url); - } public static API.EmbedProvider ToModel(this EmbedProvider entity) { return new API.EmbedProvider { Name = entity.Name, Url = entity.Url }; } - public static EmbedThumbnail ToEntity(this API.EmbedThumbnail model) - { - return new EmbedThumbnail(model.Url, model.ProxyUrl, - model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null); - } public static API.EmbedThumbnail ToModel(this EmbedThumbnail entity) { return new API.EmbedThumbnail { Url = entity.Url }; } - public static EmbedVideo ToEntity(this API.EmbedVideo model) - { - return new EmbedVideo(model.Url, - model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null); - } public static API.EmbedVideo ToModel(this EmbedVideo entity) { return new API.EmbedVideo { Url = entity.Url }; diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index b4b91ff69..53298b476 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -49,7 +49,7 @@ namespace Discord.WebSocket internal class ReferenceStore : ILookupReferenceStore where TEntity : class, ICached, TSharedEntity - where TModel : IEntityModel + where TModel : class, IEntityModel where TId : IEquatable where TSharedEntity : class { @@ -263,20 +263,40 @@ namespace Discord.WebSocket return _store.PurgeAllAsync(); } + public IEnumerable GetEnumerable(IEnumerable ids) + { + foreach (var id in ids) + { + yield return Get(id); + } + } + + public async IAsyncEnumerable GetEnumerableAsync(IEnumerable ids) + { + foreach (var id in ids) + { + yield return (TEntity)await GetAsync(id, CacheMode.CacheOnly); + } + } + TEntity ILookupReferenceStore.Get(TId id) => Get(id); async ValueTask ILookupReferenceStore.GetAsync(TId id) => (TEntity)await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); } internal partial class ClientStateManager { - public ReferenceStore UserStore; + public ReferenceStore UserStore; public ReferenceStore PresenceStore; + private ConcurrentDictionary> _memberStores; private ConcurrentDictionary> _threadMemberStores; + private ConcurrentDictionary> _messageStores; private SemaphoreSlim _memberStoreLock; + private SemaphoreSlim _messageStoreLock; private SemaphoreSlim _threadMemberLock; + #region Models private readonly Dictionary> _defaultModelFactory = new() { { typeof(IUserModel), () => new SocketUser.CacheModel() }, @@ -284,10 +304,45 @@ namespace Discord.WebSocket { typeof(ICurrentUserModel), () => new SocketSelfUser.CacheModel() }, { typeof(IThreadMemberModel), () => new SocketThreadUser.CacheModel() }, { typeof(IPresenceModel), () => new SocketPresence.CacheModel() }, - { typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() } + { typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() }, + { typeof(IMessageModel), () => new SocketMessage.CacheModel() }, + { typeof(IMessageActivityModel), () => new SocketMessage.CacheModel.MessageActivityModel() }, + { typeof(IMessageComponentModel), () => new SocketMessage.CacheModel.MessageComponentModel() }, + { typeof(IMessageComponentOptionModel), () => new SocketMessage.CacheModel.MessageComponentModel.MessageComponentOptionModel() }, + { typeof(IPartialApplicationModel), () => new SocketMessage.CacheModel.PartialApplicationModel() }, + { typeof(IStickerItemModel), () => new SocketMessage.CacheModel.StickerItemModel() }, + { typeof(IReactionMetadataModel), () => new SocketMessage.CacheModel.ReactionModel() }, + { typeof(IEmbedModel), () => new SocketMessage.CacheModel.EmbedModel() }, + { typeof(IEmbedFieldModel), () => new SocketMessage.CacheModel.EmbedModel.EmbedFieldModel() }, + { typeof(IEmbedMediaModel), () => new SocketMessage.CacheModel.EmbedModel.EmbedMediaModel()} + }; + public TModel GetModel() + where TFallback : class, TModel, new() + where TModel : class + { + return GetModel() ?? new TFallback(); + } + + public TModel GetModel() + where TModel : class + { + var type = _cacheProvider.GetModel(); + if (type != null) + { + if (!type.GetInterfaces().Contains(typeof(TModel))) + throw new InvalidOperationException($"Cannot use {type.Name} as a model for {typeof(TModel).Name}"); + + return (TModel)Activator.CreateInstance(type); + } + else + return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; + } + #endregion + + #region References & Initialization public void ClearDeadReferences() { UserStore.ClearDeadReferences(); @@ -300,6 +355,29 @@ namespace Discord.WebSocket await PresenceStore.InitializeAsync(); } + private void CreateStores() + { + UserStore = new ReferenceStore( + _cacheProvider, + m => SocketGlobalUser.Create(_client, m), + async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), + GetModel); + + PresenceStore = new ReferenceStore( + _cacheProvider, + m => SocketPresence.Create(_client, m), + (id, options) => Task.FromResult(null), + GetModel); + + _memberStores = new(); + _threadMemberStores = new(); + + _threadMemberLock = new(1, 1); + _memberStoreLock = new(1, 1); + } + #endregion + + #region Members public ReferenceStore GetMemberStore(ulong guildId) => TryGetMemberStore(guildId, out var store) ? store : null; @@ -331,7 +409,9 @@ namespace Discord.WebSocket _memberStoreLock.Release(); } } + #endregion + #region Thread Members public async Task> GetThreadMemberStoreAsync(ulong threadId, ulong guildId) { if (_threadMemberStores.TryGetValue(threadId, out var store)) @@ -360,49 +440,40 @@ namespace Discord.WebSocket public ReferenceStore GetThreadMemberStore(ulong threadId) => _threadMemberStores.TryGetValue(threadId, out var store) ? store : null; + #endregion - public TModel GetModel() - where TFallback : class, TModel, new() - where TModel : class - { - return GetModel() ?? new TFallback(); - } + #region Messages + public ReferenceStore GetMessageStore(ulong channelId) + => TryGetMessageStore(channelId, out var store) ? store : null; - public TModel GetModel() - where TModel : class - { - var type = _cacheProvider.GetModel(); - - if (type != null) - { - if (!type.GetInterfaces().Contains(typeof(TModel))) - throw new InvalidOperationException($"Cannot use {type.Name} as a model for {typeof(TModel).Name}"); - - return (TModel)Activator.CreateInstance(type); - } - else - return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; - } + public bool TryGetMessageStore(ulong channelId, out ReferenceStore store) + => _messageStores.TryGetValue(channelId, out store); - private void CreateStores() + public async ValueTask> GetMessageStoreAsync(ulong channelId) { - UserStore = new ReferenceStore( - _cacheProvider, - m => SocketGlobalUser.Create(_client, m), - async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), - GetModel); + if (_messageStores.TryGetValue(channelId, out var store)) + return store; - PresenceStore = new ReferenceStore( - _cacheProvider, - m => SocketPresence.Create(_client, m), - (id, options) => Task.FromResult(null), - GetModel); + await _messageStoreLock.WaitAsync().ConfigureAwait(false); - _memberStores = new(); - _threadMemberStores = new(); + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketMessage.Create(_client, m, channelId), + async (id, options) => await _client.Rest.GetMessageAsync(channelId, id).ConfigureAwait(false), + GetModel); - _threadMemberLock = new(1, 1); - _memberStoreLock = new(1,1); + await store.InitializeAsync(channelId).ConfigureAwait(false); + + _messageStores.TryAdd(channelId, store); + return store; + } + finally + { + _memberStoreLock.Release(); + } } + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index c7bc5e873..787a10693 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -5,19 +5,25 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.Message; +using Model = Discord.IMessageModel; namespace Discord.WebSocket { /// /// Represents a WebSocket-based message. /// - public abstract class SocketMessage : SocketEntity, IMessage + public abstract class SocketMessage : SocketEntity, IMessage, ICached { #region SocketMessage + internal bool IsFreed { get; set; } private long _timestampTicks; private readonly List _reactions = new List(); - private ImmutableArray _userMentions = ImmutableArray.Create(); + private ulong[] _userMentionIds; + private ulong _channelId; + private ulong _guildId; + private ulong _authorId; + private bool _isWebhook; + //private ImmutableArray _userMentions = ImmutableArray.Create(); /// /// Gets the author of this message. @@ -54,6 +60,7 @@ namespace Discord.WebSocket public virtual DateTimeOffset? EditedTimestamp => null; /// public virtual bool MentionedEveryone => false; + public virtual ulong? ApplicationId { get; private set; } /// public MessageActivity Activity { get; private set; } @@ -115,10 +122,13 @@ namespace Discord.WebSocket /// /// Returns the users mentioned in this message. /// + /// + /// The returned enumerable will preform cache lookups per enumeration. + /// /// /// Collection of WebSocket-based users. /// - public IReadOnlyCollection MentionedUsers => _userMentions; + public IEnumerable MentionedUsers => Discord.StateManager.UserStore.GetEnumerable(_userMentionIds); // TODO: async counterpart? /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); @@ -129,6 +139,12 @@ namespace Discord.WebSocket Author = author; Source = source; } + + //internal static SocketMessage Create(DiscordSocketClient discord, Model model, ulong channelId) + //{ + + //} + internal static SocketMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) { if (model.Type == MessageType.Default || @@ -140,55 +156,52 @@ namespace Discord.WebSocket else return SocketSystemMessage.Create(discord, state, author, channel, model); } - internal virtual void Update(ClientStateManager state, Model model) + internal virtual void Update(Model model) { Type = model.Type; - if (model.Timestamp.IsSpecified) - _timestampTicks = model.Timestamp.Value.UtcTicks; - - if (model.Content.IsSpecified) - { - Content = model.Content.Value; - } + _timestampTicks = model.Timestamp; + ApplicationId = model.ApplicationId; + Content = model.Content; + _userMentionIds = model.UserMentionIds; - if (model.Application.IsSpecified) + if (model.Application != null) { // create a new Application from the API model Application = new MessageApplication() { - Id = model.Application.Value.Id, - CoverImage = model.Application.Value.CoverImage, - Description = model.Application.Value.Description, - Icon = model.Application.Value.Icon, - Name = model.Application.Value.Name + Id = model.Application.Id, + CoverImage = model.Application.CoverImage, + Description = model.Application.Description, + Icon = model.Application.Icon, + Name = model.Application.Name }; } - if (model.Activity.IsSpecified) + if (model.Activity != null) { // create a new Activity from the API model Activity = new MessageActivity() { - Type = model.Activity.Value.Type.Value, - PartyId = model.Activity.Value.PartyId.GetValueOrDefault() + Type = model.Activity.Type.Value, + PartyId = model.Activity.PartyId }; } - if (model.Reference.IsSpecified) + if (model.ReferenceMessageId.HasValue) { // Creates a new Reference from the API model Reference = new MessageReference { - GuildId = model.Reference.Value.GuildId, - InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + GuildId = model.ReferenceMessageGuildId.ToOptional(), + InternalChannelId = model.ReferenceMessageChannelId.ToOptional(), + MessageId = model.ReferenceMessageId.ToOptional() }; } - if (model.Components.IsSpecified) + if (model.Components != null && model.Components.Length > 0) { - Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select(y => + Components = model.Components.Select(x => new ActionRowComponent(x.Components.Select(y => { switch (y.Type) { @@ -236,38 +249,16 @@ namespace Discord.WebSocket else Components = new List(); - if (model.UserMentions.IsSpecified) + if (model.InteractionId.HasValue) { - var value = model.UserMentions.Value; - if (value.Length > 0) - { - var newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - { - var val = value[i]; - if (val != null) - { - var user = Channel.GetUserAsync(val.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; - if (user != null) - newMentions.Add(user); - else - newMentions.Add(SocketUnknownUser.Create(Discord, val)); - } - } - _userMentions = newMentions.ToImmutable(); - } + Interaction = new MessageInteraction(model.InteractionId.Value, + model.InteractionType.Value, + model.InteractionName, + model.InteractionUserId.Value, + Discord.StateManager.UserStore.Get); } - if (model.Interaction.IsSpecified) - { - Interaction = new MessageInteraction(model.Interaction.Value.Id, - model.Interaction.Value.Type, - model.Interaction.Value.Name, - SocketGlobalUser.Create(Discord, model.Interaction.Value.User)); - } - - if (model.Flags.IsSpecified) - Flags = model.Flags.Value; + Flags = model.Flags; } /// @@ -309,7 +300,6 @@ namespace Discord.WebSocket /// IReadOnlyCollection IMessage.Stickers => Stickers; - internal void AddReaction(SocketReaction reaction) { _reactions.Add(reaction); @@ -347,5 +337,314 @@ namespace Discord.WebSocket public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); #endregion + + #region Cache + + internal class CacheModel : Model + { + public MessageType Type { get; set; } + public ulong ChannelId { get; set; } + public ulong? GuildId { get; set; } + public ulong AuthorId { get; set; } + public bool IsWebhookMessage { get; set; } + public string Content { get; set; } + public long Timestamp { get; set; } + public long? EditedTimestamp { get; set; } + public bool IsTextToSpeech { get; set; } + public bool MentionEveryone { get; set; } + public ulong[] UserMentionIds { get; set; } + public AttachmentModel[] Attachments { get; set; } + public EmbedModel[] Embeds { get; set; } + public ReactionModel[] Reactions { get; set; } // TODO: seperate store? + public bool Pinned { get; set; } + public MessageActivityModel Activity { get; set; } + public PartialApplicationModel Application { get; set; } + public ulong? ApplicationId { get; set; } + public ulong? ReferenceMessageId { get; set; } + public ulong? ReferenceMessageChannelId { get; set; } + public ulong? ReferenceMessageGuildId { get; set; } + public MessageFlags Flags { get; set; } + public ulong? InteractionId { get; set; } + public string InteractionName { get; set; } + public InteractionType? InteractionType { get; set; } + public ulong? InteractionUserId { get; set; } + public MessageComponentModel[] Components { get; set; } + public StickerItemModel[] Stickers { get; set; } + public ulong Id { get; set; } + + internal class AttachmentModel : IAttachmentModel + { + public string FileName { get; set; } + public string Description { get; set; } + public string ContentType { get; set; } + public int Size { get; set; } + public string Url { get; set; } + public string ProxyUrl { get; set; } + public int? Height { get; set; } + public int? Width { get; set; } + public bool Ephemeral { get; set; } + public ulong Id { get; set; } + } + internal class EmbedModel : IEmbedModel + { + public string Title { get; set; } + public EmbedType Type { get; set; } + public string Description { get; set; } + public string Url { get; set; } + public long? Timestamp { get; set; } + public uint? Color { get; set; } + public string FooterText { get; set; } + public string FooterIconUrl { get; set; } + public string FooterProxyUrl { get; set; } + public string ProviderName { get; set; } + public string ProviderUrl { get; set; } + public string AuthorName { get; set; } + public string AuthorUrl { get; set; } + public string AuthorIconUrl { get; set; } + public string AuthorProxyIconUrl { get; set; } + public EmbedMediaModel Image { get; set; } + public EmbedMediaModel Thumbnail { get; set; } + public EmbedMediaModel Video { get; set; } + public EmbedFieldModel[] Fields { get; set; } + + IEmbedMediaModel IEmbedModel.Image { get => Image; set => Image = value.InterfaceCopy(); } + IEmbedMediaModel IEmbedModel.Thumbnail { get => Thumbnail; set => Thumbnail = value.InterfaceCopy(); } + IEmbedMediaModel IEmbedModel.Video { get => Video; set => Video = value.InterfaceCopy(); } + IEmbedFieldModel[] IEmbedModel.Fields { get => Fields; set => value?.Select(x => x.InterfaceCopy()); } + + internal class EmbedMediaModel : IEmbedMediaModel + { + public string Url { get; set; } + public string ProxyUrl { get; set; } + public int? Height { get; set; } + public int? Width { get; set; } + } + internal class EmbedFieldModel : IEmbedFieldModel + { + public string Name { get; set; } + public string Value { get; set; } + public bool Inline { get; set; } + } + } + internal class ReactionModel : IReactionMetadataModel + { + public IEmojiModel Emoji { get; set; } + public ulong[] Users { get; set; } + } + internal class MessageActivityModel : IMessageActivityModel + { + public MessageActivityType? Type { get; set; } + public string PartyId { get; set; } + } + internal class PartialApplicationModel : IPartialApplicationModel + { + public string Name { get; set; } + public string Icon { get; set; } + public string Description { get; set; } + public string CoverImage { get; set; } + public ulong Id { get; set; } + } + internal class MessageComponentModel : IMessageComponentModel + { + public ComponentType Type { get; set; } + public string CustomId { get; set; } + public bool? Disabled { get; set; } + public ButtonStyle? Style { get; set; } + public string Label { get; set; } + public ulong? EmojiId { get; set; } + public string EmojiName { get; set; } + public bool? EmojiAnimated { get; set; } + public string Url { get; set; } + public MessageComponentOptionModel[] Options { get; set; } + public string Placeholder { get; set; } + public int? MinValues { get; set; } + public int? MaxValues { get; set; } + public MessageComponentModel[] Components { get; set; } + public int? MinLength { get; set; } + public int? MaxLength { get; set; } + public bool? Required { get; set; } + public string Value { get; set; } + + internal class MessageComponentOptionModel : IMessageComponentOptionModel + { + public string Label { get; set; } + public string Value { get; set; } + public string Description { get; set; } + public ulong? EmojiId { get; set; } + public string EmojiName { get; set; } + public bool? EmojiAnimated { get; set; } + public bool? Default { get; set; } + } + + IMessageComponentOptionModel[] IMessageComponentModel.Options { get => Options; set => Options = value.Select(x => x.InterfaceCopy(new MessageComponentOptionModel())).ToArray(); } + IMessageComponentModel[] IMessageComponentModel.Components { get => Components; set => Components = value.Select(x => x.InterfaceCopy(new MessageComponentModel())).ToArray(); } + } + internal class StickerItemModel : IStickerItemModel + { + public ulong Id { get; set; } + public string Name { get; set; } + public StickerFormatType Format { get; set; } + } + + IAttachmentModel[] Model.Attachments { get => Attachments; set => Attachments = value.Select(x => x.InterfaceCopy()).ToArray(); } + IEmbedModel[] Model.Embeds { get => Embeds; set => Embeds = value.Select(x => x.InterfaceCopy()).ToArray(); } + IReactionMetadataModel[] Model.Reactions { get => Reactions; set => Reactions = value.Select(x => x.InterfaceCopy()).ToArray(); } + IMessageActivityModel Model.Activity { get => Activity; set => Activity = value.InterfaceCopy(); } + IPartialApplicationModel Model.Application { get => Application; set => Application = value.InterfaceCopy(); } + IMessageComponentModel[] Model.Components { get => Components; set => Components = value.Select(x => x.InterfaceCopy()).ToArray(); } + IStickerItemModel[] Model.Stickers { get => Stickers; set => Stickers = value.Select(x => x.InterfaceCopy()).ToArray(); } + } + + internal virtual Model ToModel() + { + var model = Discord.StateManager.GetModel(); + model.Content = Content; + model.Type = Type; + model.ChannelId = _channelId; + model.GuildId = _guildId; + model.AuthorId = _authorId; + model.IsWebhookMessage = _isWebhook; + model.Timestamp = _timestampTicks; + model.IsTextToSpeech = IsTTS; + model.MentionEveryone = MentionedEveryone; + model.UserMentionIds = _userMentionIds; + model.ApplicationId = ApplicationId; + model.Flags = Flags ?? MessageFlags.None; + model.Id = Id; + + if(Interaction != null) + { + model.InteractionName = Interaction.Name; + model.InteractionId = Interaction.Id; + model.InteractionType = Interaction.Type; + model.InteractionUserId = Interaction.UserId; + } + + if(Reference != null) + { + model.ReferenceMessageId = Reference.MessageId.ToNullable(); + model.ReferenceMessageGuildId = Reference.GuildId.ToNullable(); + model.ReferenceMessageChannelId = Reference.ChannelId; + } + + model.Attachments = Attachments.Select(x => + { + var attachmentModel = Discord.StateManager.GetModel(); + attachmentModel.Width = x.Width; + attachmentModel.Height = x.Height; + attachmentModel.Size = x.Size; + attachmentModel.Description = x.Description; + attachmentModel.Ephemeral = x.Ephemeral; + attachmentModel.FileName = x.Filename; + attachmentModel.Url = x.Url; + attachmentModel.ContentType = x.ContentType; + attachmentModel.Id = x.Id; + attachmentModel.ProxyUrl = x.ProxyUrl; + + return attachmentModel; + }).ToArray(); + + model.Embeds = Embeds.Select(x => + { + var embedModel = Discord.StateManager.GetModel(); + + embedModel.AuthorName = x.Author?.Name; + embedModel.AuthorProxyIconUrl = x.Author?.ProxyIconUrl; + embedModel.AuthorIconUrl = x.Author?.IconUrl; + embedModel.AuthorUrl = x.Author?.Url; + + embedModel.Color = x.Color; + embedModel.Description = x.Description; + embedModel.Title = x.Title; + embedModel.Timestamp = x.Timestamp?.UtcTicks; + embedModel.Type = x.Type; + embedModel.Url = x.Url; + + embedModel.ProviderName = x.Provider?.Name; + embedModel.ProviderUrl = x.Provider?.Url; + + embedModel.FooterIconUrl = x.Footer?.IconUrl; + embedModel.FooterProxyUrl = x.Footer?.ProxyUrl; + embedModel.FooterText = x.Footer?.Text; + + var image = Discord.StateManager.GetModel(); + image.Width = x.Image?.Width; + image.Height = x.Image?.Height; + image.Url = x.Image?.Url; + image.ProxyUrl = x.Image?.ProxyUrl; + + embedModel.Image = image; + + var thumbnail = Discord.StateManager.GetModel(); + thumbnail.Width = x.Thumbnail?.Width; + thumbnail.Height = x.Thumbnail?.Height; + thumbnail.Url = x.Thumbnail?.Url; + thumbnail.ProxyUrl = x.Thumbnail?.ProxyUrl; + + embedModel.Thumbnail = thumbnail; + + var video = Discord.StateManager.GetModel(); + video.Width = x.Video?.Width; + video.Height = x.Video?.Height; + video.Url = x.Video?.Url; + + embedModel.Video = video; + + embedModel.Fields = x.Fields.Select(x => + { + var fieldModel = Discord.StateManager.GetModel(); + fieldModel.Name = x.Name; + fieldModel.Value = x.Value; + fieldModel.Inline = x.Inline; + return fieldModel; + }).ToArray(); + + return embedModel; + }).ToArray(); + + model.Reactions = _reactions.GroupBy(x => x.Emote).Select(x => + { + var reactionMetadataModel = Discord.StateManager.GetModel(); + reactionMetadataModel.Emoji = x.Key.ToModel(Discord.StateManager.GetModel()); + reactionMetadataModel.Users = x.Select(x => x.UserId).ToArray(); + return reactionMetadataModel; + }).ToArray(); + + var activityModel = Discord.StateManager.GetModel(); + activityModel.PartyId = Activity?.PartyId; + activityModel.Type = Activity?.Type; + model.Activity = activityModel; + + var applicationModel = Discord.StateManager.GetModel(); + applicationModel.Description = Application.Description; + applicationModel.Name = Application.Name; + applicationModel.CoverImage = Application.CoverImage; + applicationModel.Id = Application.Id; + applicationModel.Icon = Application.Icon; + model.Application = applicationModel; + + return model; + } + + ~SocketMessage() => Dispose(); + public void Dispose() + { + if (IsFreed) + return; + + IsFreed = true; + + GC.SuppressFinalize(this); + + if (Discord.StateManager.TryGetMessageStore(Channel.Id, out var store)) + store.RemoveReference(Id); + } + + + void ICached.Update(Model model) => Update(model); + Model ICached.ToModel() => ToModel(); + + bool ICached.IsFreed => IsFreed; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 32cac7d8b..2beceacde 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -20,12 +20,19 @@ namespace Discord.WebSocket /// public ulong UserId { get; } /// + /// Gets the ID of the message that has been reacted to. + /// + /// + /// A message snowflake identifier associated with the message. + /// + public ulong MessageId { get; } + /// /// Gets the user who added the reaction if possible. /// /// /// /// 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 + /// the client. In other words, when the user is not in the cache, this property may not /// contain a value, leaving the only identifiable information to be /// . /// @@ -35,25 +42,16 @@ namespace Discord.WebSocket /// /// /// - /// A user object where possible; a value is not always returned. + /// A lazily-cached user object. /// - /// - public Optional User { get; } - /// - /// Gets the ID of the message that has been reacted to. - /// - /// - /// A message snowflake identifier associated with the message. - /// - public ulong MessageId { get; } + public LazyCached User { get; } /// /// Gets the message that has been reacted to if possible. /// /// - /// A WebSocket-based message where possible; a value is not always returned. + /// A lazily-cached message. /// - /// - public Optional Message { get; } + public LazyCached Message { get; } /// /// Gets the channel where the reaction takes place in. /// @@ -64,16 +62,26 @@ namespace Discord.WebSocket /// public IEmote Emote { get; } - internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) + internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) { - Channel = channel; + var client = ((SocketChannel)channel).Discord; + MessageId = messageId; - Message = message; UserId = userId; - User = user; + + Channel = channel; + + Message = message.IsSpecified + ? new LazyCached(message.Value) + : new LazyCached(messageId, client.StateManager.GetMessageStore(channel.Id)); + + User = user.IsSpecified + ? new LazyCached(user.Value) + : new LazyCached(userId, client.StateManager.UserStore); + Emote = emoji; } - internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional message, Optional user) + internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional message, Optional user) { IEmote emote; if (model.Emoji.Id.HasValue) @@ -86,11 +94,14 @@ namespace Discord.WebSocket /// public override bool Equals(object other) { - if (other == null) return false; - if (other == this) return true; + if (other == null) + return false; + if (other == this) + return true; var otherReaction = other as SocketReaction; - if (otherReaction == null) return false; + if (otherReaction == null) + return false; return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 4c8e94432..617d70838 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -5,7 +5,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.Message; +using Model = Discord.IMessageModel; namespace Discord.WebSocket { @@ -17,11 +17,12 @@ namespace Discord.WebSocket { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private IUserMessage _referencedMessage; private ImmutableArray _attachments = ImmutableArray.Create(); private ImmutableArray _embeds = ImmutableArray.Create(); private ImmutableArray _tags = ImmutableArray.Create(); - private ImmutableArray _roleMentions = ImmutableArray.Create(); + private ulong[] _roleMentions; + private ulong? _referencedMessageId; + //private ImmutableArray _roleMentions = ImmutableArray.Create(); private ImmutableArray _stickers = ImmutableArray.Create(); /// @@ -56,30 +57,26 @@ namespace Discord.WebSocket internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) { var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); - entity.Update(state, model); + entity.Update(model); return entity; } - internal override void Update(ClientStateManager state, Model model) + internal override void Update(Model model) { - base.Update(state, model); + base.Update(model); SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; - if (model.IsTextToSpeech.IsSpecified) - _isTTS = model.IsTextToSpeech.Value; - if (model.Pinned.IsSpecified) - _isPinned = model.Pinned.Value; - if (model.EditedTimestamp.IsSpecified) - _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; - if (model.MentionEveryone.IsSpecified) - _isMentioningEveryone = model.MentionEveryone.Value; - if (model.RoleMentions.IsSpecified) - _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); - - if (model.Attachments.IsSpecified) + _isTTS = model.IsTextToSpeech; + _isPinned = model.Pinned; + _editedTimestampTicks = model.EditedTimestamp; + _isMentioningEveryone = model.MentionEveryone; + _roleMentions = model.RoleMentionIds; + _referencedMessageId = model.ReferenceMessageId; + + if (model.Attachments != null && model.Attachments.Length > 0) { - var value = model.Attachments.Value; + var value = model.Attachments; if (value.Length > 0) { var attachments = ImmutableArray.CreateBuilder(value.Length); @@ -91,9 +88,9 @@ namespace Discord.WebSocket _attachments = ImmutableArray.Create(); } - if (model.Embeds.IsSpecified) + if (model.Embeds != null && model.Embeds.Length > 0) { - var value = model.Embeds.Value; + var value = model.Embeds; if (value.Length > 0) { var embeds = ImmutableArray.CreateBuilder(value.Length); @@ -105,41 +102,16 @@ namespace Discord.WebSocket _embeds = ImmutableArray.Create(); } - if (model.Content.IsSpecified) + if (model.Content != null) { - var text = model.Content.Value; + var text = model.Content; _tags = MessageHelper.ParseTags(text, Channel, guild, MentionedUsers); model.Content = text; } - if (model.ReferencedMessage.IsSpecified && model.ReferencedMessage.Value != null) - { - var refMsg = model.ReferencedMessage.Value; - ulong? webhookId = refMsg.WebhookId.ToNullable(); - SocketUser refMsgAuthor = null; - if (refMsg.Author.IsSpecified) - { - if (guild != null) - { - if (webhookId != null) - refMsgAuthor = SocketWebhookUser.Create(guild, refMsg.Author.Value, webhookId.Value); - else - refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); - } - else - refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); - if (refMsgAuthor == null) - refMsgAuthor = SocketUnknownUser.Create(Discord, refMsg.Author.Value); - } - else - // Message author wasn't specified in the payload, so create a completely anonymous unknown user - refMsgAuthor = new SocketUnknownUser(Discord, id: 0); - _referencedMessage = SocketUserMessage.Create(Discord, state, refMsgAuthor, Channel, refMsg); - } - - if (model.StickerItems.IsSpecified) + if (model.Stickers != null && model.Stickers.Length > 0) { - var value = model.StickerItems.Value; + var value = model.Stickers; if (value.Length > 0) { var stickers = ImmutableArray.CreateBuilder(value.Length); diff --git a/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs b/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs index ca7d2d0f1..b7e1a3cc8 100644 --- a/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs +++ b/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; -using Model = Discord.API.StickerItem; +using Model = Discord.IStickerItemModel; namespace Discord.WebSocket { @@ -47,7 +47,7 @@ namespace Discord.WebSocket internal void Update(Model model) { Name = model.Name; - Format = model.FormatType; + Format = model.Format; } /// diff --git a/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs index 5b1da074c..5368426af 100644 --- a/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs @@ -8,19 +8,26 @@ namespace Discord.WebSocket { internal static class CacheModelExtensions { - public static TDest ToSpecifiedModel(this IEntityModel source, TDest dest) - where TId : IEquatable - where TDest : IEntityModel + public static TDest InterfaceCopy(this object source) + where TDest : class, new() + => source.InterfaceCopy(new TDest()); + + public static TDest InterfaceCopy(this TSource source, TDest dest) + where TSource : class + where TDest : class { if (source == null || dest == null) throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); + if (source == null || dest == null) + throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); + // get the shared model interface var sourceType = source.GetType(); var destType = dest.GetType(); if (sourceType == destType) - return (TDest)source; + return source as TDest; List sharedInterfaceModels = new(); @@ -52,5 +59,12 @@ namespace Discord.WebSocket return dest; } + + public static TDest ToSpecifiedModel(this IEntityModel source, TDest dest) + where TId : IEquatable + where TDest : class, IEntityModel + { + return source.InterfaceCopy(dest); + } } } diff --git a/src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs new file mode 100644 index 000000000..58fe88143 --- /dev/null +++ b/src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs @@ -0,0 +1,16 @@ +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public static class EntityCacheExtensions + { + public static ValueTask GetUserAsync(this MessageInteraction interaction, DiscordSocketClient client, + CacheMode mode, RequestOptions options = null) + => client.StateManager.UserStore.GetAsync(interaction.UserId, mode, options); + } +} diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 6cde93d87..e3927e6fc 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -7,6 +7,17 @@ namespace Discord.WebSocket { internal static class EntityExtensions { + #region Emotes + public static IEmote ToEntity(this IEmojiModel model) + { + if (model.Id.HasValue) + return new Emote(model.Id.Value, model.Name, model.IsAnimated); + else + return new Emoji(model.Name); + } + #endregion + + #region Activity public static IActivity ToEntity(this IActivityModel model) { #region Custom Status Game @@ -147,5 +158,6 @@ namespace Discord.WebSocket { return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } + #endregion } }