diff --git a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs new file mode 100644 index 0000000000..d92e5b2111 --- /dev/null +++ b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class CacheableEntityExtensions + { + public static IActivityModel ToModel(this RichGame richGame) where TModel : IActivityModel, new() + { + return new TModel() + { + ApplicationId = richGame.ApplicationId, + SmallImage = richGame.SmallAsset?.ImageId, + SmallText = richGame.SmallAsset?.Text, + LargeImage = richGame.LargeAsset?.ImageId, + LargeText = richGame.LargeAsset?.Text, + Details = richGame.Details, + Flags = richGame.Flags, + Name = richGame.Name, + Type = richGame.Type, + JoinSecret = richGame.Secrets?.Join, + SpectateSecret = richGame.Secrets?.Spectate, + MatchSecret = richGame.Secrets?.Match, + State = richGame.State, + PartyId = richGame.Party?.Id, + PartySize = richGame.Party?.Members != null && richGame.Party?.Capacity != null + ? new long[] { richGame.Party.Members, richGame.Party.Capacity } + : null, + TimestampEnd = richGame.Timestamps?.End, + TimestampStart = richGame.Timestamps?.Start + }; + } + + public static IActivityModel ToModel(this SpotifyGame spotify) where TModel : IActivityModel, new() + { + return new TModel() + { + Name = spotify.Name, + SessionId = spotify.SessionId, + SyncId = spotify.TrackId, + LargeText = spotify.AlbumTitle, + Details = spotify.TrackTitle, + State = string.Join(";", spotify.Artists), + TimestampEnd = spotify.EndsAt, + TimestampStart = spotify.StartedAt, + LargeImage = spotify.AlbumArt, + Type = ActivityType.Listening, + Flags = spotify.Flags, + }; + } + + public static IActivityModel ToModel(this CustomStatusGame custom) + where TModel : IActivityModel, new() + where TEmoteModel : IEmojiModel, new() + { + return new TModel + { + Id = "custom", + Type = ActivityType.CustomStatus, + Name = custom.Name, + State = custom.State, + Emoji = custom.Emote.ToModel(), + CreatedAt = custom.CreatedAt + }; + } + + public static IActivityModel ToModel(this StreamingGame stream) where TModel : IActivityModel, new() + { + return new TModel + { + Name = stream.Name, + Url = stream.Url, + Flags = stream.Flags, + Details = stream.Details + }; + } + + public static IEmojiModel ToModel(this IEmote emote, IEmojiModel model) + { + if (emote == null) + return null; + + model.Name = emote.Name; + + if (emote is GuildEmote guildEmote) + { + model.Id = guildEmote.Id; + model.IsAnimated = guildEmote.Animated; + model.IsAvailable = guildEmote.IsAvailable; + model.IsManaged = guildEmote.IsManaged; + model.CreatorId = guildEmote.CreatorId; + model.RequireColons = guildEmote.RequireColons; + model.Roles = guildEmote.RoleIds.ToArray(); + } + + if (emote is Emote e) + { + model.IsAnimated = e.Animated; + model.Id = e.Id; + } + + 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/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs new file mode 100644 index 0000000000..955b170172 --- /dev/null +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal interface ICached : ICached, IDisposable + { + void Update(TType model); + + TType ToModel(); + } + + public interface ICached + { + bool IsFreed { get; } + } +} 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 0000000000..fd9c66add5 --- /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/Emoji/IEmojiModel.cs b/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs new file mode 100644 index 0000000000..4a1270ba7a --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.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 IEmojiModel + { + ulong? Id { get; set; } + string Name { get; set; } + ulong[] Roles { get; set; } + bool RequireColons { get; set; } + bool IsManaged { get; set; } + bool IsAnimated { get; set; } + bool IsAvailable { get; set; } + + ulong? CreatorId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/IEntityModel.cs b/src/Discord.Net.Core/Cache/Models/IEntityModel.cs new file mode 100644 index 0000000000..c48fc05d58 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/IEntityModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IEntityModel where TId : IEquatable + { + TId Id { 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 0000000000..f7e9efe97d --- /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 0000000000..556e0b0c75 --- /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 0000000000..fec9d4e51c --- /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 0000000000..64ec78bf8a --- /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 0000000000..c88f217d80 --- /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 new file mode 100644 index 0000000000..ca718ec660 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IMessageModel : IEntityModel + { + MessageType Type { get; set; } + ulong ChannelId { get; set; } + ulong? GuildId { get; set; } + ulong AuthorId { get; set; } + bool IsWebhookMessage { get; set; } + string Content { 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 0000000000..f4215d3911 --- /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 0000000000..db32d85224 --- /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/Cache/Models/Presense/IActivityModel.cs b/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs new file mode 100644 index 0000000000..64633e6048 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IActivityModel + { + string Id { get; set; } + string Url { get; set; } + string Name { get; set; } + ActivityType Type { get; set; } + string Details { get; set; } + string State { get; set; } + ActivityProperties Flags { get; set; } + DateTimeOffset CreatedAt { get; set; } + IEmojiModel Emoji { get; set; } + ulong? ApplicationId { get; set; } + string SyncId { get; set; } + string SessionId { get; set; } + + + #region Assets + string LargeImage { get; set; } + string LargeText { get; set; } + string SmallImage { get; set; } + string SmallText { get; set; } + #endregion + + #region Party + string PartyId { get; set; } + long[] PartySize { get; set; } + #endregion + + #region Secrets + string JoinSecret { get; set; } + string SpectateSecret { get; set; } + string MatchSecret { get; set; } + #endregion + + #region Timestamps + DateTimeOffset? TimestampStart { get; set; } + DateTimeOffset? TimestampEnd { get; set; } + #endregion + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs new file mode 100644 index 0000000000..b1b45f21e6 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IPresenceModel : IEntityModel + { + ulong UserId { get; set; } + ulong? GuildId { get; set; } + UserStatus Status { get; set; } + ClientType[] ActiveClients { get; set; } + IActivityModel[] Activities { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs b/src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs new file mode 100644 index 0000000000..7d7b289955 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface ICurrentUserModel : IUserModel + { + bool? IsVerified { get; set; } + string Email { get; set; } + bool? IsMfaEnabled { get; set; } + UserProperties Flags { get; set; } + PremiumType PremiumType { get; set; } + string Locale { get; set; } + UserProperties PublicFlags { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs new file mode 100644 index 0000000000..f13920c945 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.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 IMemberModel : IEntityModel + { + //IUserModel User { get; set; } + string Nickname { get; set; } + string GuildAvatar { get; set; } + ulong[] Roles { get; set; } + DateTimeOffset? JoinedAt { get; set; } + DateTimeOffset? PremiumSince { get; set; } + bool IsDeaf { get; set; } + bool IsMute { get; set; } + bool? IsPending { get; set; } + DateTimeOffset? CommunicationsDisabledUntil { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs new file mode 100644 index 0000000000..12a52f5749 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.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 IThreadMemberModel : IEntityModel + { + ulong? ThreadId { get; set; } + DateTimeOffset JoinedAt { get; set; } + } +} diff --git a/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs new file mode 100644 index 0000000000..88b0505200 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.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 IUserModel : IEntityModel + { + string Username { get; set; } + string Discriminator { get; set; } + bool? IsBot { get; set; } + string Avatar { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index 4eab34fa21..abd89808f7 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -107,6 +107,8 @@ public class SpotifyGame : Game /// public string TrackUrl { get; internal set; } + internal string AlbumArt { get; set; } + internal SpotifyGame() { } /// diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 4bd0845c8d..6fbe1e80c5 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -24,6 +24,13 @@ public class GuildEmote : Emote /// public bool RequireColons { get; } /// + /// Gets whether or not the emote is available. + /// + /// + /// An emote can be unavailable if the guild has lost its boost status. + /// + public bool IsAvailable { get; } + /// /// Gets the roles that are allowed to use this emoji. /// /// @@ -39,12 +46,13 @@ public class GuildEmote : Emote /// public ulong? CreatorId { get; } - internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds, ulong? userId) : base(id, name, animated) + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool isAvailable, bool requireColons, IReadOnlyList roleIds, ulong? userId) : base(id, name, animated) { IsManaged = isManaged; RequireColons = requireColons; RoleIds = roleIds; CreatorId = userId; + IsAvailable = isAvailable; } private string DebuggerDisplay => $"{Name} ({Id})"; diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index f5f2ca0076..7992544c2e 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -46,6 +46,11 @@ public interface IMessage : ISnowflakeEntity, IDeletable /// 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 cbbebd932a..fce03e9da8 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 @@ public class MessageInteraction : IMessageInteraction where TUser : IUser /// /// 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 985be92402..8dc95d3520 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -56,5 +56,9 @@ public static class Optional 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 9a7eb80dd7..7213a40e24 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -3,7 +3,7 @@ namespace Discord.API { - internal class ActionRowComponent : IMessageComponent + internal class ActionRowComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -29,5 +29,27 @@ internal ActionRowComponent(Discord.ActionRowComponent c) [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 7970dc9a5c..4493263777 100644 --- a/src/Discord.Net.Rest/API/Common/Attachment.cs +++ b/src/Discord.Net.Rest/API/Common/Attachment.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class Attachment + internal class Attachment : IAttachmentModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -24,5 +24,16 @@ internal class Attachment 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 7f737d7ad4..e81a95fdfa 100644 --- a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class ButtonComponent : IMessageComponent + internal class ButtonComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -59,5 +59,27 @@ public ButtonComponent(Discord.ButtonComponent c) [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/CurrentUser.cs b/src/Discord.Net.Rest/API/Common/CurrentUser.cs new file mode 100644 index 0000000000..626256ae19 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/CurrentUser.cs @@ -0,0 +1,70 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class CurrentUser : User, ICurrentUserModel + { + [JsonProperty("verified")] + public Optional Verified { get; set; } + [JsonProperty("email")] + public Optional Email { get; set; } + [JsonProperty("mfa_enabled")] + public Optional MfaEnabled { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + [JsonProperty("premium_type")] + public Optional PremiumType { get; set; } + [JsonProperty("locale")] + public Optional Locale { get; set; } + [JsonProperty("public_flags")] + public Optional PublicFlags { get; set; } + + // ICurrentUserModel + bool? ICurrentUserModel.IsVerified + { + get => Verified.ToNullable(); + set => throw new NotSupportedException(); + } + + string ICurrentUserModel.Email + { + get => Email.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + bool? ICurrentUserModel.IsMfaEnabled + { + get => MfaEnabled.ToNullable(); + set => throw new NotSupportedException(); + } + + UserProperties ICurrentUserModel.Flags + { + get => Flags.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + PremiumType ICurrentUserModel.PremiumType + { + get => PremiumType.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + string ICurrentUserModel.Locale + { + get => Locale.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + UserProperties ICurrentUserModel.PublicFlags + { + get => PublicFlags.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index 77efa12aa5..15286a7f78 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -4,7 +4,7 @@ namespace Discord.API { - internal class Embed + internal class Embed : IEmbedModel { [JsonProperty("title")] public string Title { get; set; } @@ -32,5 +32,15 @@ internal class Embed 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 d7f3ae68da..66d15e3135 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -2,7 +2,7 @@ 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 6ce810f1a5..d292b2aefc 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 cd08e7e26c..a4c5ed830b 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -2,7 +2,7 @@ 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 6b5db0681c..513df20cd2 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class EmbedImage + internal class EmbedImage : IEmbedMediaModel { [JsonProperty("url")] public string Url { get; set; } @@ -12,5 +12,8 @@ internal class EmbedImage 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 ed0f7c3c89..e9a737149c 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -2,7 +2,7 @@ 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 dd25a1a260..9a9603cb66 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class EmbedThumbnail + internal class EmbedThumbnail : IEmbedMediaModel { [JsonProperty("url")] public string Url { get; set; } @@ -12,5 +12,8 @@ internal class EmbedThumbnail 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 f668217f0a..0b9c1214a6 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -2,13 +2,18 @@ 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/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index ff0baa73e9..2bc381fdb3 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -1,8 +1,9 @@ using Newtonsoft.Json; +using System; namespace Discord.API { - internal class Emoji + internal class Emoji : IEmojiModel { [JsonProperty("id")] public ulong? Id { get; set; } @@ -16,7 +17,57 @@ internal class Emoji public bool RequireColons { get; set; } [JsonProperty("managed")] public bool Managed { get; set; } + [JsonProperty("available")] + public Optional Available { get; set; } [JsonProperty("user")] public Optional User { get; set; } + + ulong? IEmojiModel.Id + { + get => Id; + set => throw new NotSupportedException(); + } + + string IEmojiModel.Name + { + get => Name; + set => throw new NotSupportedException(); + } + + ulong[] IEmojiModel.Roles + { + get => Roles; + set => throw new NotSupportedException(); + } + + bool IEmojiModel.RequireColons + { + get => RequireColons; + set => throw new NotSupportedException(); + } + + bool IEmojiModel.IsManaged + { + get => Managed; + set => throw new NotSupportedException(); + } + + bool IEmojiModel.IsAnimated + { + get => Animated.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + bool IEmojiModel.IsAvailable + { + get => Available.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + ulong? IEmojiModel.CreatorId + { + get => User.GetValueOrDefault()?.Id; + set => throw new NotSupportedException(); + } } } diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 105ce0d736..133943d3e2 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -1,10 +1,11 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using System; using System.Runtime.Serialization; namespace Discord.API { - internal class Game + internal class Game : IActivityModel { [JsonProperty("name")] public string Name { get; set; } @@ -32,7 +33,7 @@ internal class Game public Optional SyncId { get; set; } [JsonProperty("session_id")] public Optional SessionId { get; set; } - [JsonProperty("Flags")] + [JsonProperty("flags")] public Optional Flags { get; set; } [JsonProperty("id")] public Optional Id { get; set; } @@ -40,6 +41,100 @@ internal class Game public Optional Emoji { get; set; } [JsonProperty("created_at")] public Optional CreatedAt { get; set; } + + string IActivityModel.Id { + get => Id.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.Url { + get => StreamUrl.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.State { + get => State.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + IEmojiModel IActivityModel.Emoji { + get => Emoji.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.Name { + get => Name; set => throw new NotSupportedException(); + } + + ActivityType IActivityModel.Type { + get => Type.GetValueOrDefault().GetValueOrDefault(); set => throw new NotSupportedException(); + } + + ActivityProperties IActivityModel.Flags { + get => Flags.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.Details { + get => Details.GetValueOrDefault(); set => throw new NotSupportedException(); + } + DateTimeOffset IActivityModel.CreatedAt { + get => DateTimeOffset.FromUnixTimeMilliseconds(CreatedAt.GetValueOrDefault()); set => throw new NotSupportedException(); + } + + ulong? IActivityModel.ApplicationId { + get => ApplicationId.ToNullable(); set => throw new NotSupportedException(); + } + + string IActivityModel.SyncId { + get => SyncId.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.SessionId { + get => SessionId.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.LargeImage { + get => Assets.GetValueOrDefault()?.LargeImage.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.LargeText { + get => Assets.GetValueOrDefault()?.LargeText.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.SmallImage { + get => Assets.GetValueOrDefault()?.SmallImage.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.SmallText { + get => Assets.GetValueOrDefault()?.SmallText.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IActivityModel.PartyId { + get => Party.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); + } + + long[] IActivityModel.PartySize { + get => Party.GetValueOrDefault()?.Size; set => throw new NotSupportedException(); + } + + string IActivityModel.JoinSecret { + get => Secrets.GetValueOrDefault()?.Join; set => throw new NotSupportedException(); + } + + string IActivityModel.SpectateSecret { + get => Secrets.GetValueOrDefault()?.Spectate; set => throw new NotSupportedException(); + } + + string IActivityModel.MatchSecret { + get => Secrets.GetValueOrDefault()?.Match; set => throw new NotSupportedException(); + } + + DateTimeOffset? IActivityModel.TimestampStart { + get => Timestamps.GetValueOrDefault()?.Start.ToNullable(); set => throw new NotSupportedException(); + } + + DateTimeOffset? IActivityModel.TimestampEnd { + get => Timestamps.GetValueOrDefault()?.End.ToNullable(); set => throw new NotSupportedException(); + } + + + //[JsonProperty("buttons")] //public Optional Buttons { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index cd31012242..e2321a3b1e 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -3,7 +3,7 @@ namespace Discord.API { - internal class GuildMember + internal class GuildMember : IMemberModel { [JsonProperty("user")] public User User { get; set; } @@ -25,5 +25,46 @@ internal class GuildMember public Optional PremiumSince { get; set; } [JsonProperty("communication_disabled_until")] public Optional TimedOutUntil { get; set; } + + // IMemberModel + string IMemberModel.Nickname { + get => Nick.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + string IMemberModel.GuildAvatar { + get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + ulong[] IMemberModel.Roles { + get => Roles.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); + } + + DateTimeOffset? IMemberModel.JoinedAt { + get => JoinedAt.ToNullable(); set => throw new NotSupportedException(); + } + + DateTimeOffset? IMemberModel.PremiumSince { + get => PremiumSince.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + bool IMemberModel.IsDeaf { + get => Deaf.GetValueOrDefault(false); set => throw new NotSupportedException(); + } + + bool IMemberModel.IsMute { + get => Mute.GetValueOrDefault(false); set => throw new NotSupportedException(); + } + + bool? IMemberModel.IsPending { + get => Pending.ToNullable(); set => throw new NotSupportedException(); + } + + DateTimeOffset? IMemberModel.CommunicationsDisabledUntil { + get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + ulong IEntityModel.Id { + get => User.Id; set => throw new NotSupportedException(); + } } } diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index d33a03fe5a..308e25ccfa 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 @@ internal class Message // 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 @@ internal class Message 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 701f6fc030..0dcb522cad 100644 --- a/src/Discord.Net.Rest/API/Common/MessageActivity.cs +++ b/src/Discord.Net.Rest/API/Common/MessageActivity.cs @@ -7,11 +7,14 @@ 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 7302185ad1..08af48c94c 100644 --- a/src/Discord.Net.Rest/API/Common/MessageApplication.cs +++ b/src/Discord.Net.Rest/API/Common/MessageApplication.cs @@ -7,7 +7,7 @@ namespace Discord.API { - public class MessageApplication + internal class MessageApplication : IPartialApplicationModel { /// /// Gets the snowflake ID of the application. @@ -34,5 +34,10 @@ public class MessageApplication /// [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/Presence.cs b/src/Discord.Net.Rest/API/Common/Presence.cs index 23f871ae60..de450172be 100644 --- a/src/Discord.Net.Rest/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -1,10 +1,11 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Linq; namespace Discord.API { - internal class Presence + internal class Presence : IPresenceModel { [JsonProperty("user")] public User User { get; set; } @@ -28,5 +29,28 @@ internal class Presence public List Activities { get; set; } [JsonProperty("premium_since")] public Optional PremiumSince { get; set; } + + ulong IPresenceModel.UserId { + get => User.Id; set => throw new NotSupportedException(); + } + + ulong? IPresenceModel.GuildId { + get => GuildId.ToNullable(); set => throw new NotSupportedException(); + } + + UserStatus IPresenceModel.Status { + get => Status; set => throw new NotSupportedException(); + } + + ClientType[] IPresenceModel.ActiveClients { + get => ClientStatus.IsSpecified ? ClientStatus.Value.Select(x => (ClientType)Enum.Parse(typeof(ClientType), x.Key, true)).ToArray() : Array.Empty(); set => throw new NotSupportedException(); + } + + IActivityModel[] IPresenceModel.Activities { + get => Activities.ToArray(); set => throw new NotSupportedException(); + } + ulong IEntityModel.Id { + get => User.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 4d368ab2d5..f3b3d218d6 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 @@ internal class Reaction 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 25ac476c51..6a43bdca39 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -3,7 +3,7 @@ namespace Discord.API { - internal class SelectMenuComponent : IMessageComponent + internal class SelectMenuComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -28,6 +28,7 @@ internal class SelectMenuComponent : IMessageComponent [JsonProperty("values")] public Optional Values { get; set; } + public SelectMenuComponent() { } public SelectMenuComponent(Discord.SelectMenuComponent component) @@ -40,5 +41,27 @@ public SelectMenuComponent(Discord.SelectMenuComponent component) 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 d0a25a8296..5e8f9a9582 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class SelectMenuOption + internal class SelectMenuOption : IMessageComponentOptionModel { [JsonProperty("label")] public string Label { get; set; } @@ -49,5 +49,13 @@ public SelectMenuOption(Discord.SelectMenuOption option) 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 4b24f711b3..b9a3c8780d 100644 --- a/src/Discord.Net.Rest/API/Common/StickerItem.cs +++ b/src/Discord.Net.Rest/API/Common/StickerItem.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class StickerItem + internal class StickerItem : IStickerItemModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -12,5 +12,10 @@ internal class StickerItem [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 a475345fcf..225e83167c 100644 --- a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs +++ b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class TextInputComponent : IMessageComponent + internal class TextInputComponent : IMessageComponent, IMessageComponentModel { [JsonProperty("type")] public ComponentType Type { get; set; } @@ -45,5 +45,29 @@ public TextInputComponent(Discord.TextInputComponent component) 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/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs index 30249ee44d..10172cc70d 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMember.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -3,10 +3,10 @@ namespace Discord.API { - internal class ThreadMember + internal class ThreadMember : IThreadMemberModel { [JsonProperty("id")] - public Optional Id { get; set; } + public Optional ThreadId { get; set; } [JsonProperty("user_id")] public Optional UserId { get; set; } @@ -14,7 +14,8 @@ internal class ThreadMember [JsonProperty("join_timestamp")] public DateTimeOffset JoinTimestamp { get; set; } - [JsonProperty("flags")] - public int Flags { get; set; } // No enum type (yet?) + ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } + DateTimeOffset IThreadMemberModel.JoinedAt { get => JoinTimestamp; set => throw new NotSupportedException(); } + ulong IEntityModel.Id { get => UserId.GetValueOrDefault(0); set => throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Rest/API/Common/User.cs b/src/Discord.Net.Rest/API/Common/User.cs index 08fe88cb05..5c8a5b240c 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -1,8 +1,9 @@ using Newtonsoft.Json; +using System; namespace Discord.API { - internal class User + internal class User : IUserModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -19,20 +20,33 @@ internal class User [JsonProperty("accent_color")] public Optional AccentColor { get; set; } - //CurrentUser - [JsonProperty("verified")] - public Optional Verified { get; set; } - [JsonProperty("email")] - public Optional Email { get; set; } - [JsonProperty("mfa_enabled")] - public Optional MfaEnabled { get; set; } - [JsonProperty("flags")] - public Optional Flags { get; set; } - [JsonProperty("premium_type")] - public Optional PremiumType { get; set; } - [JsonProperty("locale")] - public Optional Locale { get; set; } - [JsonProperty("public_flags")] - public Optional PublicFlags { get; set; } + + // IUserModel + string IUserModel.Username + { + get => Username.GetValueOrDefault(); + set => throw new NotSupportedException(); + } + + string IUserModel.Discriminator { + get => Discriminator.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + bool? IUserModel.IsBot + { + get => Bot.ToNullable(); + set => throw new NotSupportedException(); + } + + string IUserModel.Avatar + { + get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); + } + + ulong IEntityModel.Id + { + get => Id; + set => throw new NotSupportedException(); + } } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index c6ad6a9fb5..a365cecf34 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -46,6 +46,16 @@ public static async Task> GetGroupChannels .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); @@ -151,6 +161,16 @@ public static async Task GetGuildUserAsync(BaseDiscordClient clie return null; } + public static async Task> GetGuildUsersAsync(BaseDiscordClient client, + ulong guildId, RequestOptions options) + { + var guild = await GetGuildAsync(client, guildId, false, options).ConfigureAwait(false); + if (guild == null) + return null; + + return (await GuildHelper.GetUsersAsync(guild, client, null, null, options).FlattenAsync()).ToImmutableArray(); + } + public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) { var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 3b829ee171..60a95c6e3e 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -2063,10 +2063,10 @@ public async Task GetUserAsync(ulong userId, RequestOptions options = null #endregion #region Current User/DMs - public async Task GetMyUserAsync(RequestOptions options = null) + public async Task GetMyUserAsync(RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); + return await SendAsync("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); } public async Task> GetMyConnectionsAsync(RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index b1948f80a8..023b71f4e9 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -158,6 +158,9 @@ public Task> GetDMChannelsAsync(RequestOption 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); @@ -185,6 +188,8 @@ public Task GetUserAsync(ulong id, RequestOptions options = null) => ClientHelper.GetUserAsync(this, id, options); public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) => ClientHelper.GetGuildUserAsync(this, guildId, id, options); + public Task> GetGuildUsersAsync(ulong guildId, RequestOptions options = null) + => ClientHelper.GetGuildUsersAsync(this, guildId, options); public Task> GetVoiceRegionsAsync(RequestOptions options = null) => ClientHelper.GetVoiceRegionsAsync(this, options); diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index a5b83fb7b9..fbdab727b6 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 @@ internal Attachment(ulong id, string filename, string url, string proxyUrl, int } 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 4d9ef008db..a0600cbc90 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -221,7 +221,7 @@ public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client, 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 @@ bool EnclosedInBlock(Match m) 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 @@ public static ImmutableArray FilterTagsByValue(TagType type, ImmutableArra .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/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 0a4a330997..91d716bf4d 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -94,6 +94,7 @@ internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Mo internal void Update(Model model) { base.Update(model.User); + if (model.JoinedAt.IsSpecified) _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) diff --git a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs index b5ef01c53d..49ef92a647 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -1,7 +1,8 @@ using System; using System.Diagnostics; using System.Threading.Tasks; -using Model = Discord.API.User; +using UserModel = Discord.API.User; +using Model = Discord.API.CurrentUser; namespace Discord.Rest { @@ -28,29 +29,26 @@ internal RestSelfUser(BaseDiscordClient discord, ulong id) : base(discord, id) { } - internal new static RestSelfUser Create(BaseDiscordClient discord, Model model) + internal new static RestSelfUser Create(BaseDiscordClient discord, UserModel model) { var entity = new RestSelfUser(discord, model.Id); entity.Update(model); return entity; } /// - internal override void Update(Model model) + internal override void Update(UserModel model) { base.Update(model); - if (model.Email.IsSpecified) - Email = model.Email.Value; - if (model.Verified.IsSpecified) - IsVerified = model.Verified.Value; - if (model.MfaEnabled.IsSpecified) - IsMfaEnabled = model.MfaEnabled.Value; - if (model.Flags.IsSpecified) - Flags = (UserProperties)model.Flags.Value; - if (model.PremiumType.IsSpecified) - PremiumType = model.PremiumType.Value; - if (model.Locale.IsSpecified) - Locale = model.Locale.Value; + if (model is not Model currentUserModel) + throw new ArgumentException("Got unexpected model type when updating RestSelfUser"); + + Email = currentUserModel.Email.GetValueOrDefault(); + IsVerified = currentUserModel.Verified.GetValueOrDefault(false); + IsMfaEnabled = currentUserModel.MfaEnabled.GetValueOrDefault(false); + Flags = currentUserModel.Flags.GetValueOrDefault(); + PremiumType = currentUserModel.PremiumType.GetValueOrDefault(); + Locale = currentUserModel.Locale.GetValueOrDefault(); } /// diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index dfdb53815b..9074a88e22 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -78,20 +78,16 @@ internal static RestUser Create(BaseDiscordClient discord, IGuild guild, EventUs internal virtual void Update(Model model) { - if (model.Avatar.IsSpecified) - AvatarId = model.Avatar.Value; - if (model.Banner.IsSpecified) - BannerId = model.Banner.Value; - if (model.AccentColor.IsSpecified) - AccentColor = model.AccentColor.Value; - if (model.Discriminator.IsSpecified) + AvatarId = model.Avatar.GetValueOrDefault(); + if(model.Discriminator.IsSpecified) DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); - if (model.Bot.IsSpecified) - IsBot = model.Bot.Value; - if (model.Username.IsSpecified) - Username = model.Username.Value; - if (model.PublicFlags.IsSpecified) - PublicFlags = model.PublicFlags.Value; + IsBot = model.Bot.GetValueOrDefault(false); + Username = model.Username.GetValueOrDefault(); + + if(model is ICurrentUserModel currentUserModel) + { + PublicFlags = currentUserModel.PublicFlags; + } } /// diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 4062cda3db..e688afd376 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; @@ -6,6 +7,23 @@ namespace Discord.Rest { internal static class EntityExtensions { + public static IEmote ToIEmote(this IEmojiModel model) + { + if (model.Id.HasValue) + return model.ToEntity(); + return new Emoji(model.Name); + } + + public static GuildEmote ToEntity(this IEmojiModel model) + => new GuildEmote(model.Id.Value, + model.Name, + model.IsAnimated, + model.IsManaged, + model.IsAvailable, + model.RequireColons, + ImmutableArray.Create(model.Roles), + model.CreatorId); + public static IEmote ToIEmote(this API.Emoji model) { if (model.Id.HasValue) @@ -18,21 +36,30 @@ public static GuildEmote ToEntity(this API.Emoji model) model.Name, model.Animated.GetValueOrDefault(), model.Managed, + model.Available.GetValueOrDefault(), model.RequireColons, 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) { @@ -98,15 +125,11 @@ public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes 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); } @@ -114,48 +137,22 @@ public static API.EmbedField ToModel(this EmbedField entity) { 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 }; @@ -170,48 +167,5 @@ public static Overwrite ToEntity(this API.Overwrite model) { return new Overwrite(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)); } - - public static API.Message ToMessage(this API.InteractionResponse model, IDiscordInteraction interaction) - { - if (model.Data.IsSpecified) - { - var data = model.Data.Value; - var messageModel = new API.Message - { - IsTextToSpeech = data.TTS, - Content = (data.Content.IsSpecified && data.Content.Value == null) ? Optional.Unspecified : data.Content, - Embeds = data.Embeds, - AllowedMentions = data.AllowedMentions, - Components = data.Components, - Flags = data.Flags, - }; - - if(interaction is IApplicationCommandInteraction command) - { - messageModel.Interaction = new API.MessageInteraction - { - Id = command.Id, - Name = command.Data.Name, - Type = InteractionType.ApplicationCommand, - User = new API.User - { - Username = command.User.Username, - Avatar = command.User.AvatarId, - Bot = command.User.IsBot, - Discriminator = command.User.Discriminator, - PublicFlags = command.User.PublicFlags.HasValue ? command.User.PublicFlags.Value : Optional.Unspecified, - Id = command.User.Id, - } - }; - } - - return messageModel; - } - - return new API.Message - { - Id = interaction.Id, - }; - } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs index e0b5fc0b52..10b7adf2e7 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs @@ -17,7 +17,7 @@ public class ReadState [JsonProperty("v")] public int Version { get; set; } [JsonProperty("user")] - public User User { get; set; } + public CurrentUser User { get; set; } [JsonProperty("session_id")] public string SessionId { get; set; } [JsonProperty("read_state")] diff --git a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs new file mode 100644 index 0000000000..f182bd1f60 --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public class DefaultConcurrentCacheProvider : ICacheProvider + { + private readonly ConcurrentDictionary _storeCache = new(); + private readonly ConcurrentDictionary _subStoreCache = new(); + + private class DefaultEntityStore : IEntityStore + where TModel : IEntityModel + where TId : IEquatable + { + private ConcurrentDictionary _cache; + + public DefaultEntityStore(ConcurrentDictionary cache) + { + _cache = cache; + } + + public TModel Get(TId id) + { + if (_cache.TryGetValue(id, out var model)) + return model; + return default; + } + public IEnumerable GetAll() + { + return _cache.Select(x => x.Value); + } + public void AddOrUpdate(TModel model) + { + _cache.AddOrUpdate(model.Id, model, (_, __) => model); + } + public void AddOrUpdateBatch(IEnumerable models) + { + foreach (var model in models) + _cache.AddOrUpdate(model.Id, model, (_, __) => model); + } + public void Remove(TId id) + { + _cache.TryRemove(id, out _); + } + public void PurgeAll() + { + _cache.Clear(); + } + + ValueTask IEntityStore.GetAsync(TId id) => new ValueTask(Get(id)); + IAsyncEnumerable IEntityStore.GetAllAsync() + { + var enumerator = GetAll().GetEnumerator(); + return AsyncEnumerable.Create((cancellationToken) + => AsyncEnumerator.Create( + () => new ValueTask(enumerator.MoveNext()), + () => enumerator.Current, + () => new ValueTask()) + ); + } + ValueTask IEntityStore.AddOrUpdateAsync(TModel model) + { + AddOrUpdate(model); + return default; + } + ValueTask IEntityStore.AddOrUpdateBatchAsync(IEnumerable models) + { + AddOrUpdateBatch(models); + return default; + } + ValueTask IEntityStore.RemoveAsync(TId id) + { + Remove(id); + return default; + } + ValueTask IEntityStore.PurgeAllAsync() + { + PurgeAll(); + return default; + } + } + + public Type GetModel() => null; + + public virtual ValueTask> GetStoreAsync() + where TModel : IEntityModel + where TId : IEquatable + { + var store = _storeCache.GetOrAdd(typeof(TModel), (_) => new DefaultEntityStore(new ConcurrentDictionary())); + return new ValueTask>((IEntityStore)store); + } + + public virtual ValueTask> GetSubStoreAsync(TId parentId) + where TModel : IEntityModel + where TId : IEquatable + { + var store = _subStoreCache.GetOrAdd(parentId, (_) => new DefaultEntityStore(new ConcurrentDictionary())); + return new ValueTask>((IEntityStore)store); + } + } +} diff --git a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs new file mode 100644 index 0000000000..42238a16ed --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface ICacheProvider + { + Type GetModel(); + + ValueTask> GetStoreAsync() + where TModel : IEntityModel + where TId : IEquatable; + + ValueTask> GetSubStoreAsync(TId parentId) + where TModel : IEntityModel + where TId : IEquatable; + } + + public interface IEntityStore + where TModel : IEntityModel + where TId : IEquatable + { + ValueTask GetAsync(TId id); + TModel Get(TId id); + IAsyncEnumerable GetAllAsync(); + IEnumerable GetAll(); + ValueTask AddOrUpdateAsync(TModel model); + void AddOrUpdate(TModel model); + ValueTask AddOrUpdateBatchAsync(IEnumerable models); + void AddOrUpdateBatch(IEnumerable models); + ValueTask RemoveAsync(TId id); + void Remove(TId id); + ValueTask PurgeAllAsync(); + void PurgeAll(); + } +} diff --git a/src/Discord.Net.WebSocket/Cache/LazyCached.cs b/src/Discord.Net.WebSocket/Cache/LazyCached.cs new file mode 100644 index 0000000000..ca568b5ee6 --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/LazyCached.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + /// + /// Represents a lazily-loaded cached value that can be loaded synchronously or asynchronously. + /// + /// The type of the entity. + /// The primary id type of the entity. + public class LazyCached + where TEntity : class, ICached + where TId : IEquatable + { + /// + /// Gets or loads the cached value synchronously. + /// + public TEntity Value + => GetOrLoad(); + + /// + /// Gets whether or not the has been loaded and is still alive. + /// + public bool IsValueCreated + => _loadedValue != null && _loadedValue.IsFreed; + + private TEntity _loadedValue; + private readonly ILookupReferenceStore _store; + private readonly TId _id; + private readonly object _lock = new(); + + internal LazyCached(TEntity value) + { + _loadedValue = value; + } + + internal LazyCached(TId id, ILookupReferenceStore store) + { + _store = store; + _id = id; + } + + private TEntity GetOrLoad() + { + lock (_lock) + { + if(!IsValueCreated) + _loadedValue = _store.Get(_id); + return _loadedValue; + } + } + + /// + /// Gets or loads the value from the cache asynchronously. + /// + /// The loaded or fetched entity. + public async ValueTask GetAsync() + { + if (!IsValueCreated) + _loadedValue = await _store.GetAsync(_id).ConfigureAwait(false); + return _loadedValue; + } + } + + public class LazyCached : LazyCached + where TEntity : class, ICached + { + internal LazyCached(ulong id, ILookupReferenceStore store) + : base(id, store) { } + internal LazyCached(TEntity entity) + : base(entity) { } + } +} diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs new file mode 100644 index 0000000000..53298b476b --- /dev/null +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -0,0 +1,479 @@ +using Discord.Rest; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class CacheReference where TType : class + { + public WeakReference Reference { get; } + + public bool CanRelease + => !Reference.TryGetTarget(out _) || _referenceCount <= 0; + + private int _referenceCount; + + public CacheReference(TType value) + { + Reference = new(value); + _referenceCount = 1; + } + + public bool TryObtainReference(out TType reference) + { + if (Reference.TryGetTarget(out reference)) + { + Interlocked.Increment(ref _referenceCount); + return true; + } + return false; + } + + public void ReleaseReference() + { + Interlocked.Decrement(ref _referenceCount); + } + } + + internal interface ILookupReferenceStore + { + TEntity Get(TId id); + ValueTask GetAsync(TId id); + } + + internal class ReferenceStore : ILookupReferenceStore + where TEntity : class, ICached, TSharedEntity + where TModel : class, IEntityModel + where TId : IEquatable + where TSharedEntity : class + { + private readonly ICacheProvider _cacheProvider; + private readonly ConcurrentDictionary> _references = new(); + private IEntityStore _store; + private Func _entityBuilder; + private Func _modelFactory; + private Func> _restLookup; + private readonly object _lock = new(); + + public ReferenceStore(ICacheProvider cacheProvider, + Func entityBuilder, + Func> restLookup, + Func userDefinedModelFactory) + { + _cacheProvider = cacheProvider; + _entityBuilder = entityBuilder; + _restLookup = restLookup; + _modelFactory = userDefinedModelFactory; + } + + private TModel GetUserDefinedModel(TModel t) + => t.ToSpecifiedModel(_modelFactory()); + + internal bool RemoveReference(TId id) + { + if(_references.TryGetValue(id, out var rf)) + { + rf.ReleaseReference(); + + if (rf.CanRelease) + return _references.TryRemove(id, out _); + } + + return false; + } + + internal void ClearDeadReferences() + { + lock (_lock) + { + var references = _references.Where(x => x.Value.CanRelease).ToArray(); + foreach (var reference in references) + _references.TryRemove(reference.Key, out _); + } + } + + public async ValueTask InitializeAsync() + { + _store ??= await _cacheProvider.GetStoreAsync().ConfigureAwait(false); + } + + public async ValueTask InitializeAsync(TId parentId) + { + _store ??= await _cacheProvider.GetSubStoreAsync(parentId).ConfigureAwait(false); + } + + private bool TryGetReference(TId id, out TEntity entity) + { + entity = null; + return _references.TryGetValue(id, out var reference) && reference.TryObtainReference(out entity); + } + + public TEntity Get(TId id) + { + if(TryGetReference(id, out var entity)) + { + return entity; + } + + var model = _store.Get(id); + + if (model != null) + { + entity = _entityBuilder(model); + _references.TryAdd(id, new CacheReference(entity)); + return entity; + } + + return null; + } + + public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) + { + if (TryGetReference(id, out var entity)) + { + return entity; + } + + var model = await _store.GetAsync(id).ConfigureAwait(false); + + if (model != null) + { + entity = _entityBuilder(model); + _references.TryAdd(id, new CacheReference(entity)); + return entity; + } + + if(mode == CacheMode.AllowDownload) + { + return await _restLookup(id, options).ConfigureAwait(false); + } + + return null; + } + + public IEnumerable GetAll() + { + var models = _store.GetAll(); + return models.Select(x => + { + var entity = _entityBuilder(x); + _references.TryAdd(x.Id, new CacheReference(entity)); + return entity; + }); + } + + public async IAsyncEnumerable GetAllAsync() + { + await foreach(var model in _store.GetAllAsync()) + { + var entity = _entityBuilder(model); + _references.TryAdd(model.Id, new CacheReference(entity)); + yield return entity; + } + } + + public TEntity GetOrAdd(TId id, Func valueFactory) + { + var entity = Get(id); + if (entity != null) + return entity; + + var model = valueFactory(id); + AddOrUpdate(model); + return _entityBuilder(model); + } + + public async ValueTask GetOrAddAsync(TId id, Func valueFactory) + { + var entity = await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); + if (entity != null) + return (TEntity)entity; + + var model = valueFactory(id); + await AddOrUpdateAsync(model).ConfigureAwait(false); + return _entityBuilder(model); + } + + public void AddOrUpdate(TModel model) + { + var userDefinedModel = GetUserDefinedModel(model); + _store.AddOrUpdate(userDefinedModel); + if (TryGetReference(model.Id, out var reference)) + reference.Update(userDefinedModel); + } + + public ValueTask AddOrUpdateAsync(TModel model) + { + var userDefinedModel = GetUserDefinedModel(model); + if (TryGetReference(userDefinedModel.Id, out var reference)) + reference.Update(userDefinedModel); + return _store.AddOrUpdateAsync(userDefinedModel); + } + + public void BulkAddOrUpdate(IEnumerable models) + { + models = models.Select(x => GetUserDefinedModel(x)); + _store.AddOrUpdateBatch(models); + foreach (var model in models) + { + if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) + entity.Update(model); + } + } + + public async ValueTask BulkAddOrUpdateAsync(IEnumerable models) + { + models = models.Select(x => GetUserDefinedModel(x)); + await _store.AddOrUpdateBatchAsync(models).ConfigureAwait(false); + + foreach (var model in models) + { + if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) + entity.Update(model); + } + } + + public void Remove(TId id) + { + _store.Remove(id); + _references.TryRemove(id, out _); + } + + public ValueTask RemoveAsync(TId id) + { + _references.TryRemove(id, out _); + return _store.RemoveAsync(id); + } + + public void Purge() + { + _store.PurgeAll(); + _references.Clear(); + } + + public ValueTask PurgeAsync() + { + _references.Clear(); + 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 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() }, + { typeof(IMemberModel), () => new SocketGuildUser.CacheModel() }, + { typeof(ICurrentUserModel), () => new SocketSelfUser.CacheModel() }, + { typeof(IThreadMemberModel), () => new SocketThreadUser.CacheModel() }, + { typeof(IPresenceModel), () => new SocketPresence.CacheModel() }, + { 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(); + PresenceStore.ClearDeadReferences(); + } + + public async ValueTask InitializeAsync() + { + await UserStore.InitializeAsync(); + 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; + + public bool TryGetMemberStore(ulong guildId, out ReferenceStore store) + => _memberStores.TryGetValue(guildId, out store); + + public async ValueTask> GetMemberStoreAsync(ulong guildId) + { + if (_memberStores.TryGetValue(guildId, out var store)) + return store; + + await _memberStoreLock.WaitAsync().ConfigureAwait(false); + + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketGuildUser.Create(guildId, _client, m), + async (id, options) => await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false), + GetModel); + + await store.InitializeAsync(guildId).ConfigureAwait(false); + + _memberStores.TryAdd(guildId, store); + return store; + } + finally + { + _memberStoreLock.Release(); + } + } + #endregion + + #region Thread Members + public async Task> GetThreadMemberStoreAsync(ulong threadId, ulong guildId) + { + if (_threadMemberStores.TryGetValue(threadId, out var store)) + return store; + + await _threadMemberLock.WaitAsync().ConfigureAwait(false); + + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketThreadUser.Create(_client, guildId, threadId, m), + async (id, options) => await ThreadHelper.GetUserAsync(id, _client.GetChannel(threadId) as SocketThreadChannel, _client, options).ConfigureAwait(false), + GetModel); + + await store.InitializeAsync().ConfigureAwait(false); + + _threadMemberStores.TryAdd(threadId, store); + return store; + } + finally + { + _threadMemberLock.Release(); + } + } + + public ReferenceStore GetThreadMemberStore(ulong threadId) + => _threadMemberStores.TryGetValue(threadId, out var store) ? store : null; + #endregion + + #region Messages + public ReferenceStore GetMessageStore(ulong channelId) + => TryGetMessageStore(channelId, out var store) ? store : null; + + public bool TryGetMessageStore(ulong channelId, out ReferenceStore store) + => _messageStores.TryGetValue(channelId, out store); + + public async ValueTask> GetMessageStoreAsync(ulong channelId) + { + if (_messageStores.TryGetValue(channelId, out var store)) + return store; + + await _messageStoreLock.WaitAsync().ConfigureAwait(false); + + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketMessage.Create(_client, m, channelId), + async (id, options) => await _client.Rest.GetMessageAsync(channelId, id).ConfigureAwait(false), + GetModel); + + await store.InitializeAsync(channelId).ConfigureAwait(false); + + _messageStores.TryAdd(channelId, store); + return store; + } + finally + { + _memberStoreLock.Release(); + } + } + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientStateManager.cs similarity index 91% rename from src/Discord.Net.WebSocket/ClientState.cs rename to src/Discord.Net.WebSocket/ClientStateManager.cs index c40ae3f925..d506387a71 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.cs @@ -5,7 +5,7 @@ namespace Discord.WebSocket { - internal class ClientState + internal partial class ClientStateManager { private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 @@ -30,8 +30,17 @@ internal class ClientState _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); - public ClientState(int guildCount, int dmChannelCount) + internal bool AllowSyncWaits + => _client.AllowSynchronousWaiting; + + private readonly ICacheProvider _cacheProvider; + private readonly DiscordSocketClient _client; + + + public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) { + _client = client; + _cacheProvider = client.CacheProvider; double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; _channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); @@ -40,6 +49,8 @@ public ClientState(int guildCount, int dmChannelCount) _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); _groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); _commands = new ConcurrentDictionary(); + + CreateStores(); } internal SocketChannel GetChannel(ulong id) @@ -121,22 +132,6 @@ internal SocketGuild RemoveGuild(ulong id) return null; } - internal SocketGlobalUser GetUser(ulong id) - { - if (_users.TryGetValue(id, out SocketGlobalUser user)) - return user; - return null; - } - internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) - { - return _users.GetOrAdd(id, userFactory); - } - internal SocketGlobalUser RemoveUser(ulong id) - { - if (_users.TryRemove(id, out SocketGlobalUser user)) - return user; - return null; - } internal void PurgeUsers() { foreach (var guild in _guilds.Values) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 3a14692e0a..25fd2abb8f 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -200,7 +200,7 @@ public DiscordSocketClient GetShard(int id) return _shards[id]; return null; } - private int GetShardIdFor(ulong guildId) + public int GetShardIdFor(ulong guildId) => (int)((guildId >> 22) % (uint)_totalShards); public int GetShardIdFor(IGuild guild) => GetShardIdFor(guild?.Id ?? 0); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 57d58a8b11..a7a0372433 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -70,7 +70,8 @@ public partial class DiscordSocketClient : BaseSocketClient, IDiscordClient internal int TotalShards { get; private set; } internal int MessageCacheSize { get; private set; } internal int LargeThreshold { get; private set; } - internal ClientState State { get; private set; } + internal ICacheProvider CacheProvider { get; private set; } + internal ClientStateManager StateManager { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } @@ -79,9 +80,10 @@ public partial class DiscordSocketClient : BaseSocketClient, IDiscordClient internal bool AlwaysResolveStickers { get; private set; } internal bool LogGatewayIntentWarnings { get; private set; } internal bool SuppressUnknownDispatchWarnings { get; private set; } + internal bool AllowSynchronousWaiting { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// - public override IReadOnlyCollection Guilds => State.Guilds; + public override IReadOnlyCollection Guilds => StateManager.Guilds; /// public override IReadOnlyCollection> DefaultStickerPacks { @@ -94,7 +96,7 @@ public override IReadOnlyCollection> DefaultStickerPa } } /// - public override IReadOnlyCollection PrivateChannels => State.PrivateChannels; + public override IReadOnlyCollection PrivateChannels => StateManager.PrivateChannels; /// /// Gets a collection of direct message channels opened in this session. /// @@ -109,7 +111,7 @@ public override IReadOnlyCollection> DefaultStickerPa /// A collection of DM channels that have been opened in this session. /// public IReadOnlyCollection DMChannels - => State.PrivateChannels.OfType().ToImmutableArray(); + => StateManager.PrivateChannels.OfType().ToImmutableArray(); /// /// Gets a collection of group channels opened in this session. /// @@ -124,7 +126,7 @@ public IReadOnlyCollection DMChannels /// A collection of group channels that have been opened in this session. /// public IReadOnlyCollection GroupChannels - => State.PrivateChannels.OfType().ToImmutableArray(); + => StateManager.PrivateChannels.OfType().ToImmutableArray(); /// /// Initializes a new REST/WebSocket-based Discord client. @@ -141,6 +143,7 @@ internal DiscordSocketClient(DiscordSocketConfig config, DiscordShardedClient sh private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, DiscordShardedClient shardedClient, DiscordSocketClient parentClient) : base(config, client) { + // TODO: config concurrency and size ShardId = config.ShardId ?? 0; TotalShards = config.TotalShards ?? 1; MessageCacheSize = config.MessageCacheSize; @@ -153,7 +156,8 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; - State = new ClientState(0, 0); + CacheProvider = config.CacheProvider ?? new DefaultConcurrentCacheProvider(); + AllowSynchronousWaiting = config.AllowSynchronousWaiting; Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _gatewayIntents = config.GatewayIntents; @@ -200,6 +204,21 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback); + + #region State + + public ValueTask GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) + => StateManager.UserStore.GetAsync(id, cacheMode, options); + + public ValueTask GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) + { + if (StateManager.TryGetMemberStore(guildId, out var store)) + return store.GetAsync(userId, cacheMode, options); + return new ValueTask((IGuildUser)null); + } + + #endregion + /// internal override void Dispose(bool disposing) { @@ -217,7 +236,6 @@ internal override void Dispose(bool disposing) base.Dispose(disposing); } - internal override async ValueTask DisposeAsync(bool disposing) { if (!_isDisposed) @@ -348,7 +366,7 @@ private async Task OnDisconnectingAsync(Exception ex) //Raise virtual GUILD_UNAVAILABLEs await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); - foreach (var guild in State.Guilds) + foreach (var guild in StateManager.Guilds) { if (guild.IsAvailable) await GuildUnavailableAsync(guild).ConfigureAwait(false); @@ -361,11 +379,11 @@ public override async Task GetApplicationInfoAsync(RequestOptio /// public override SocketGuild GetGuild(ulong id) - => State.GetGuild(id); + => StateManager.GetGuild(id); /// public override SocketChannel GetChannel(ulong id) - => State.GetChannel(id); + => StateManager.GetChannel(id); /// /// Gets a generic channel from the cache or does a rest request if unavailable. /// @@ -387,27 +405,9 @@ public override SocketChannel GetChannel(ulong id) public async ValueTask GetChannelAsync(ulong id, RequestOptions options = null) => GetChannel(id) ?? (IChannel)await ClientHelper.GetChannelAsync(this, id, options).ConfigureAwait(false); /// - /// Gets a user from the cache or does a rest request if unavailable. - /// - /// - /// - /// var user = await _client.GetUserAsync(168693960628371456); - /// if (user != null) - /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; - /// - /// - /// The snowflake identifier of the user (e.g. `168693960628371456`). - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the user associated with - /// the snowflake identifier; null if the user is not found. - /// - public async ValueTask GetUserAsync(ulong id, RequestOptions options = null) - => await ClientHelper.GetUserAsync(this, id, options).ConfigureAwait(false); - /// /// Clears all cached channels from the client. /// - public void PurgeChannelCache() => State.PurgeAllChannels(); + public void PurgeChannelCache() => StateManager.PurgeAllChannels(); /// /// Clears cached DM channels from the client. /// @@ -415,10 +415,10 @@ public async ValueTask GetUserAsync(ulong id, RequestOptions options = nu /// public override SocketUser GetUser(ulong id) - => State.GetUser(id); + => StateManager.UserStore.Get(id); /// public override SocketUser GetUser(string username, string discriminator) - => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); + => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); /// /// Gets a global application command. @@ -431,7 +431,7 @@ public override SocketUser GetUser(string username, string discriminator) /// public async ValueTask GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) { - var command = State.GetCommand(id); + var command = StateManager.GetCommand(id); if (command != null) return command; @@ -443,7 +443,7 @@ public async ValueTask GetGlobalApplicationCommandAsyn command = SocketApplicationCommand.Create(this, model); - State.AddCommand(command); + StateManager.AddCommand(command); return command; } @@ -461,7 +461,7 @@ public async Task> GetGlobalApplic foreach(var command in commands) { - State.AddCommand(command); + StateManager.AddCommand(command); } return commands.ToImmutableArray(); @@ -471,7 +471,7 @@ public async Task CreateGlobalApplicationCommandAsync( { var model = await InteractionHelper.CreateGlobalCommandAsync(this, properties, options).ConfigureAwait(false); - var entity = State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(this, model)); + var entity = StateManager.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(this, model)); //Update it in case it was cached entity.Update(model); @@ -486,11 +486,11 @@ public async Task> BulkOverwriteGl var entities = models.Select(x => SocketApplicationCommand.Create(this, x)); //Purge our previous commands - State.PurgeCommands(x => x.IsGlobalCommand); + StateManager.PurgeCommands(x => x.IsGlobalCommand); foreach(var entity in entities) { - State.AddCommand(entity); + StateManager.AddCommand(entity); } return entities.ToImmutableArray(); @@ -499,27 +499,21 @@ public async Task> BulkOverwriteGl /// /// Clears cached users from the client. /// - public void PurgeUserCache() => State.PurgeUsers(); - internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) + public void PurgeUserCache() => StateManager.PurgeUsers(); + internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model) { - return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model)); + return state.UserStore.GetOrAdd(model.Id, x => model); } - internal SocketUser GetOrCreateTemporaryUser(ClientState state, Discord.API.User model) + internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model) { - return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model); + return state.UserStore.Get(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, model); } - internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, Discord.API.User model) + internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model) { - return state.GetOrAddUser(model.Id, x => - { - var user = SocketGlobalUser.Create(this, state, model); - user.GlobalUser.AddRef(); - user.Presence = new SocketPresence(UserStatus.Online, null, null); - return user; - }); + return state.UserStore.GetOrAdd(model.Id, x => model); } internal void RemoveUser(ulong id) - => State.RemoveUser(id); + => StateManager.UserStore.Remove(id); /// public override async Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) @@ -548,7 +542,7 @@ public override async Task GetStickerAsync(ulong id, CacheMode mo if (model.GuildId.IsSpecified) { - var guild = State.GetGuild(model.GuildId.Value); + var guild = StateManager.GetGuild(model.GuildId.Value); //Since the sticker can be from another guild, check if we are in the guild or its in the cache if (guild != null) @@ -696,7 +690,7 @@ private async Task SendStatusAsync() if (CurrentUser == null) return; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - CurrentUser.Presence = new SocketPresence(Status, null, activities); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -820,6 +814,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty int latency = (int)(Environment.TickCount - time); int before = Latency; Latency = latency; + StateManager?.ClearDeadReferences(); await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); } @@ -866,20 +861,26 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); - var currentUser = SocketSelfUser.Create(this, state, data.User); + var state = new ClientStateManager(this, data.Guilds.Length, data.PrivateChannels.Length); + StateManager = state; + await StateManager.InitializeAsync().ConfigureAwait(false); + + var currentUser = SocketSelfUser.Create(this, data.User); Rest.CreateRestSelfUser(data.User); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - currentUser.Presence = new SocketPresence(Status, null, activities); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false); + ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentApplicationId = data.Application.Id; Rest.CurrentUser = RestSelfUser.Create(this, data.User); + int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) { var model = data.Guilds[i]; - var guild = AddGuild(model, state); + var guild = await AddGuildAsync(model).ConfigureAwait(false); if (!guild.IsAvailable) unavailableGuilds++; else @@ -892,7 +893,6 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _unavailableGuildCount = unavailableGuilds; CurrentUser = currentUser; _previousSessionUser = CurrentUser; - State = state; } catch (Exception ex) { @@ -928,7 +928,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _ = _connection.CompleteAsync(); //Notify the client that these guilds are available again - foreach (var guild in State.Guilds) + foreach (var guild in StateManager.Guilds) { if (guild.IsAvailable) await GuildAvailableAsync(guild).ConfigureAwait(false); @@ -953,10 +953,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _lastGuildAvailableTime = Environment.TickCount; await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); - var guild = State.GetGuild(data.Id); + var guild = StateManager.GetGuild(data.Id); if (guild != null) { - guild.Update(State, data); + guild.Update(StateManager, data); + await guild.UpdateCacheAsync(data).ConfigureAwait(false); if (_unavailableGuildCount != 0) _unavailableGuildCount--; @@ -978,7 +979,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); - var guild = AddGuild(data, State); + var guild = await AddGuildAsync(data).ConfigureAwait(false); if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); @@ -997,11 +998,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.Id); + var guild = StateManager.GetGuild(data.Id); if (guild != null) { var before = guild.Clone(); - guild.Update(State, data); + guild.Update(StateManager, data); await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else @@ -1016,11 +1017,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { var before = guild.Clone(); - guild.Update(State, data); + guild.Update(StateManager, data); await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else @@ -1061,7 +1062,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty type = "GUILD_UNAVAILABLE"; await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); - var guild = State.GetGuild(data.Id); + var guild = StateManager.GetGuild(data.Id); if (guild != null) { await GuildUnavailableAsync(guild).ConfigureAwait(false); @@ -1098,7 +1099,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild == null) { @@ -1156,10 +1157,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty SocketChannel channel = null; if (data.GuildId.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild != null) { - channel = guild.AddChannel(State, data); + channel = guild.AddChannel(StateManager, data); if (!guild.IsSynced) { @@ -1175,10 +1176,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } else { - channel = State.GetChannel(data.Id); + channel = StateManager.GetChannel(data.Id); if (channel != null) return; //Discord may send duplicate CHANNEL_CREATEs for DMs - channel = AddPrivateChannel(data, State) as SocketChannel; + channel = AddPrivateChannel(data, StateManager) as SocketChannel; } if (channel != null) @@ -1190,11 +1191,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.Id); + var channel = StateManager.GetChannel(data.Id); if (channel != null) { var before = channel.Clone(); - channel.Update(State, data); + channel.Update(StateManager, data); var guild = (channel as SocketGuildChannel)?.Guild; if (!(guild?.IsSynced ?? true)) @@ -1220,10 +1221,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild != null) { - channel = guild.RemoveChannel(State, data.Id); + channel = guild.RemoveChannel(StateManager, data.Id); if (!guild.IsSynced) { @@ -1257,7 +1258,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { var user = guild.AddOrUpdateUser(data); @@ -1283,7 +1284,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { var user = guild.GetUser(data.User.Id); @@ -1297,13 +1298,13 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (user != null) { var before = user.Clone(); - if (user.GlobalUser.Update(State, data.User)) + if (user.GlobalUser.Value.Update(data.User)) // TODO: update cache only and have lazy like support for events. { //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser.Value, user).ConfigureAwait(false); } - user.Update(State, data); + user.Update(data); var cacheableBefore = new Cacheable(before, user.Id, true, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); @@ -1327,7 +1328,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { SocketUser user = guild.RemoveUser(data.User.Id); @@ -1339,12 +1340,12 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - user ??= State.GetUser(data.User.Id); + user ??= (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user != null) - user.Update(State, data.User); + user.Update(data.User); else - user = State.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, State, data.User)); + user = await StateManager.UserStore.GetOrAddAsync(data.User.Id, _ => data.User).ConfigureAwait(false); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } @@ -1360,7 +1361,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { foreach (var memberModel in data.Members) @@ -1385,7 +1386,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild == null) { @@ -1410,7 +1411,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) + if (StateManager.GetChannel(data.ChannelId) is SocketGroupChannel channel) { var user = channel.GetOrAddUser(data.User); await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); @@ -1427,7 +1428,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) + if (StateManager.GetChannel(data.ChannelId) is SocketGroupChannel channel) { var user = channel.RemoveUser(data.User.Id); if (user != null) @@ -1454,7 +1455,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { var role = guild.AddRole(data.Role); @@ -1478,14 +1479,14 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { var role = guild.GetRole(data.Role.Id); if (role != null) { var before = role.Clone(); - role.Update(State, data.Role); + role.Update(StateManager, data.Role); if (!guild.IsSynced) { @@ -1513,7 +1514,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { var role = guild.RemoveRole(data.RoleId); @@ -1548,7 +1549,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { if (!guild.IsSynced) @@ -1559,7 +1560,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty SocketUser user = guild.GetUser(data.User.Id); if (user == null) - user = SocketUnknownUser.Create(this, State, data.User); + user = SocketUnknownUser.Create(this, data.User); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else @@ -1574,7 +1575,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { if (!guild.IsSynced) @@ -1583,9 +1584,9 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - SocketUser user = State.GetUser(data.User.Id); + SocketUser user = (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user == null) - user = SocketUnknownUser.Create(this, State, data.User); + user = SocketUnknownUser.Create(this, data.User); await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); } else @@ -1616,7 +1617,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty { if (!data.GuildId.IsSpecified) // assume it is a DM { - channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + channel = CreateDMChannel(data.ChannelId, data.Author.Value, StateManager); } else { @@ -1629,7 +1630,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (guild != null) { if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1657,7 +1658,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } } - var msg = SocketMessage.Create(this, State, author, channel, data); + var msg = SocketMessage.Create(this, StateManager, author, channel, data); SocketChannelHelper.AddMessage(channel, this, msg); await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } @@ -1682,7 +1683,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (isCached) { before = cachedMsg.Clone(); - cachedMsg.Update(State, data); + cachedMsg.Update(StateManager, data); after = cachedMsg; } else @@ -1694,7 +1695,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (guild != null) { if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1727,12 +1728,12 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty { if (data.Author.IsSpecified) { - var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, StateManager); channel = dmChannel; author = dmChannel.Recipient; } else - channel = CreateDMChannel(data.ChannelId, author, State); + channel = CreateDMChannel(data.ChannelId, author, StateManager); } else { @@ -1741,7 +1742,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } } - after = SocketMessage.Create(this, State, author, channel, data); + after = SocketMessage.Create(this, StateManager, author, channel, data); } var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); @@ -1941,7 +1942,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.GuildId.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); @@ -1960,12 +1961,12 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty { return; } - user = guild.AddOrUpdateUser(data); + user = guild.AddOrUpdateUser(data.User); } else { - var globalBefore = user.GlobalUser.Clone(); - if (user.GlobalUser.Update(State, data.User)) + var globalBefore = user.GlobalUser.Value.Clone(); + if (user.GlobalUser.Value.Update(data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); @@ -1974,7 +1975,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } else { - user = State.GetUser(data.User.Id); + user = (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user == null) { await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); @@ -1982,10 +1983,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } } - var before = user.Presence?.Clone(); - user.Update(State, data.User); - user.Update(data); - await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); + var before = user.Presence?.Value?.Clone(); + user.Update(data.User); + var after = SocketPresence.Create(this, data); + await StateManager.PresenceStore.AddOrUpdateAsync(data).ConfigureAwait(false); + await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); } break; case "TYPING_START": @@ -2028,7 +2030,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (!data.GuildId.IsSpecified) return; - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild != null) { @@ -2057,7 +2059,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (!data.GuildId.IsSpecified) return; - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild != null) { @@ -2082,7 +2084,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild != null) { @@ -2112,7 +2114,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.Id == CurrentUser.Id) { var before = CurrentUser.Clone(); - CurrentUser.Update(State, data); + CurrentUser.Update(data); await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else @@ -2134,7 +2136,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty SocketVoiceState before, after; if (data.GuildId != null) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); @@ -2149,7 +2151,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.ChannelId != null) { before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + after = await guild.AddOrUpdateVoiceStateAsync(StateManager, data).ConfigureAwait(false); /*if (data.UserId == CurrentUser.Id) { var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); @@ -2181,7 +2183,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.ChannelId != null) { before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = groupChannel.AddOrUpdateVoiceState(State, data); + after = groupChannel.AddOrUpdateVoiceState(StateManager, data); } else { @@ -2198,7 +2200,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) { - SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); + SocketStageChannel stage = guildUser.Guild.Value.GetStageChannel(data.ChannelId.Value); if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null) { @@ -2227,10 +2229,10 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); var isCached = guild != null; var cachedGuild = new Cacheable(guild, data.GuildId, isCached, - () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); + () => Task.FromResult(StateManager.GetGuild(data.GuildId) as IGuild)); var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); @@ -2261,7 +2263,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + if (StateManager.GetChannel(data.ChannelId) is SocketGuildChannel channel) { var guild = channel.Guild; if (!guild.IsSynced) @@ -2275,7 +2277,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty : null; SocketUser target = data.TargetUser.IsSpecified - ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value)) + ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, data.TargetUser.Value)) : null; var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); @@ -2294,7 +2296,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + if (StateManager.GetChannel(data.ChannelId) is SocketGuildChannel channel) { var guild = channel.Guild; if (!guild.IsSynced) @@ -2330,21 +2332,19 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } SocketUser user = data.User.IsSpecified - ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild != null - ? guild.AddOrUpdateUser(data.Member.Value) // null if the bot scope isn't set, so the guild cannot be retrieved. - : State.GetOrAddUser(data.Member.Value.User.Id, (_) => SocketGlobalUser.Create(this, State, data.Member.Value.User)); + ? await StateManager.UserStore.GetOrAddAsync(data.User.Value.Id, (_) => data.User.Value).ConfigureAwait(false) + : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. SocketChannel channel = null; if(data.ChannelId.IsSpecified) { - channel = State.GetChannel(data.ChannelId.Value); + channel = StateManager.GetChannel(data.ChannelId.Value); if (channel == null) { if (!data.GuildId.IsSpecified) // assume it is a DM { - channel = CreateDMChannel(data.ChannelId.Value, user, State); + channel = CreateDMChannel(data.ChannelId.Value, user, StateManager); } else { @@ -2359,7 +2359,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } else if (data.User.IsSpecified) { - channel = State.GetDMChannel(data.User.Value.Id); + channel = StateManager.GetDMChannel(data.User.Value.Id); } var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel, user); @@ -2400,7 +2400,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.GuildId.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); @@ -2410,7 +2410,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var applicationCommand = SocketApplicationCommand.Create(this, data); - State.AddCommand(applicationCommand); + StateManager.AddCommand(applicationCommand); await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); } @@ -2423,7 +2423,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.GuildId.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); @@ -2433,7 +2433,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var applicationCommand = SocketApplicationCommand.Create(this, data); - State.AddCommand(applicationCommand); + StateManager.AddCommand(applicationCommand); await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); } @@ -2446,7 +2446,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.GuildId.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); @@ -2456,7 +2456,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var applicationCommand = SocketApplicationCommand.Create(this, data); - State.RemoveCommand(applicationCommand.Id); + StateManager.RemoveCommand(applicationCommand.Id); await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); } @@ -2470,7 +2470,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { @@ -2482,14 +2482,14 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) { - threadChannel.Update(State, data); + threadChannel.Update(StateManager, data); if(data.ThreadMember.IsSpecified) threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); } else { - threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); + threadChannel = (SocketThreadChannel)guild.AddChannel(StateManager, data); if (data.ThreadMember.IsSpecified) threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); } @@ -2503,7 +2503,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if (guild == null) { await UnknownGuildAsync(type, data.GuildId.Value); @@ -2517,7 +2517,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (threadChannel != null) { - threadChannel.Update(State, data); + threadChannel.Update(StateManager, data); if (data.ThreadMember.IsSpecified) threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); @@ -2525,7 +2525,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty else { //Thread is updated but was not cached, likely meaning the thread was unarchived. - threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); + threadChannel = (SocketThreadChannel)guild.AddChannel(StateManager, data); if (data.ThreadMember.IsSpecified) threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); } @@ -2545,7 +2545,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); + var guild = StateManager.GetGuild(data.GuildId.Value); if(guild == null) { @@ -2566,7 +2566,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if(guild == null) { @@ -2580,16 +2580,16 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if(entity == null) { - entity = (SocketThreadChannel)guild.AddChannel(State, thread); + entity = (SocketThreadChannel)guild.AddChannel(StateManager, thread); } else { - entity.Update(State, thread); + entity.Update(StateManager, thread); } - foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id)) + foreach(var member in data.Members.Where(x => x.ThreadId.Value == entity.Id)) { - var guildMember = guild.GetUser(member.Id.Value); + var guildMember = guild.GetUser(member.ThreadId.Value); entity.AddOrUpdateThreadMember(member, guildMember); } @@ -2602,11 +2602,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); + var thread = (SocketThreadChannel)StateManager.GetChannel(data.ThreadId.Value); if (thread == null) { - await UnknownChannelAsync(type, data.Id.Value); + await UnknownChannelAsync(type, data.ThreadId.Value); return; } @@ -2620,7 +2620,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild == null) { @@ -2693,7 +2693,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if(guild == null) { @@ -2736,7 +2736,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild == null) { @@ -2755,7 +2755,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild == null) { @@ -2786,7 +2786,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if (guild == null) { @@ -2805,7 +2805,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); + var guild = StateManager.GetGuild(data.GuildId); if(guild == null) { @@ -2821,7 +2821,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); + var user = (SocketUser)guild.GetUser(data.UserId) ?? StateManager.UserStore.Get(data.UserId); var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); @@ -2956,48 +2956,42 @@ private async Task SyncGuildsAsync() await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } - internal SocketGuild AddGuild(ExtendedGuild model, ClientState state) + internal async Task AddGuildAsync(ExtendedGuild model) { - var guild = SocketGuild.Create(this, state, model); - state.AddGuild(guild); + await StateManager.GetMemberStoreAsync(model.Id).ConfigureAwait(false); + var guild = SocketGuild.Create(this, StateManager, model); + StateManager.AddGuild(guild); if (model.Large) _largeGuilds.Enqueue(model.Id); return guild; } internal SocketGuild RemoveGuild(ulong id) - => State.RemoveGuild(id); + => StateManager.RemoveGuild(id); /// Unexpected channel type is created. - internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) + internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientStateManager state) { var channel = SocketChannel.CreatePrivate(this, state, model); state.AddChannel(channel as SocketChannel); return channel; } - internal SocketDMChannel CreateDMChannel(ulong channelId, API.User model, ClientState state) + internal SocketDMChannel CreateDMChannel(ulong channelId, API.User model, ClientStateManager state) { return SocketDMChannel.Create(this, state, channelId, model); } - internal SocketDMChannel CreateDMChannel(ulong channelId, SocketUser user, ClientState state) + internal SocketDMChannel CreateDMChannel(ulong channelId, SocketUser user, ClientStateManager state) { return new SocketDMChannel(this, channelId, user); } internal ISocketPrivateChannel RemovePrivateChannel(ulong id) { - var channel = State.RemoveChannel(id) as ISocketPrivateChannel; - if (channel != null) - { - foreach (var recipient in channel.Recipients) - recipient.GlobalUser.RemoveRef(this); - } + var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; return channel; } internal void RemoveDMChannels() { - var channels = State.DMChannels; - State.PurgeDMChannels(); - foreach (var channel in channels) - channel.Recipient.GlobalUser.RemoveRef(this); + var channels = StateManager.DMChannels; + StateManager.PurgeDMChannels(); } internal void EnsureGatewayIntent(GatewayIntents intents) diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 4cd64dbc2d..9804587151 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -25,6 +25,17 @@ namespace Discord.WebSocket /// public class DiscordSocketConfig : DiscordRestConfig { + /// + /// Gets or sets the cache provider to use. + /// + public ICacheProvider CacheProvider { get; set; } + + /// + /// Gets or sets whether or not non-async cache lookups would wait for the task to complete + /// synchronously or to throw. + /// + public bool AllowSynchronousWaiting { get; set; } = false; + /// /// Returns the encoding gateway should use. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 43f23de1a3..fc5b2bb2d1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -37,7 +37,7 @@ internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuil : base(discord, id, guild) { } - internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) + internal new static SocketCategoryChannel Create(SocketGuild guild, ClientStateManager state, Model model) { var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index c30b3d2549..0ee1e9a98a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -29,7 +29,7 @@ internal SocketChannel(DiscordSocketClient discord, ulong id) } /// Unexpected channel type is created. - internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) + internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientStateManager state, Model model) { return model.Type switch { @@ -38,7 +38,7 @@ internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, _ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), }; } - internal abstract void Update(ClientState state, Model model); + internal abstract void Update(ClientStateManager state, Model model); #endregion #region User diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 17ab4ebe39..e3c56b987e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -35,25 +35,25 @@ internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketUser recip { Recipient = recipient; } - internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, Model model) { var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { - Recipient.Update(state, model.Recipients.Value[0]); + Recipient.Update(model.Recipients.Value[0]); } - internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, ulong channelId, API.User recipient) + internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, ulong channelId, API.User recipient) { var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); entity.Update(state, recipient); return entity; } - internal void Update(ClientState state, API.User recipient) + internal void Update(ClientStateManager state, API.User recipient) { - Recipient.Update(state, recipient); + Recipient.Update(recipient); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 4f068cf810..b5885c1d97 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -55,13 +55,13 @@ internal SocketGroupChannel(DiscordSocketClient discord, ulong id) _voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); } - internal static SocketGroupChannel Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketGroupChannel Create(DiscordSocketClient discord, ClientStateManager state, Model model) { var entity = new SocketGroupChannel(discord, model.Id); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { if (model.Name.IsSpecified) Name = model.Name.Value; @@ -73,11 +73,11 @@ internal override void Update(ClientState state, Model model) RTCRegion = model.RTCRegion.GetValueOrDefault(null); } - private void UpdateUsers(ClientState state, UserModel[] models) + private void UpdateUsers(ClientStateManager state, UserModel[] models) { var users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); for (int i = 0; i < models.Length; i++) - users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); + users[models[i].Id] = SocketGroupUser.Create(this, models[i]); _users = users; } @@ -265,8 +265,7 @@ internal SocketGroupUser GetOrAddUser(UserModel model) return user; else { - var privateUser = SocketGroupUser.Create(this, Discord.State, model); - privateUser.GlobalUser.AddRef(); + var privateUser = SocketGroupUser.Create(this, model); _users[privateUser.Id] = privateUser; return privateUser; } @@ -275,7 +274,6 @@ internal SocketGroupUser RemoveUser(ulong id) { if (_users.TryRemove(id, out SocketGroupUser user)) { - user.GlobalUser.RemoveRef(Discord); return user; } return null; @@ -283,7 +281,7 @@ internal SocketGroupUser RemoveUser(ulong id) #endregion #region Voice States - internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + internal SocketVoiceState AddOrUpdateVoiceState(ClientStateManager state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; var voiceState = SocketVoiceState.Create(voiceChannel, model); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 79f02fe1ce..2d6e4c273a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -49,7 +49,7 @@ internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild g { Guild = guild; } - internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) + internal static SocketGuildChannel Create(SocketGuild guild, ClientStateManager state, Model model) { return model.Type switch { @@ -63,7 +63,7 @@ internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, }; } /// - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { Name = model.Name.Value; Position = model.Position.GetValueOrDefault(0); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs index eed8f93740..56d035da69 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -21,7 +21,7 @@ internal SocketNewsChannel(DiscordSocketClient discord, ulong id, SocketGuild gu :base(discord, id, guild) { } - internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) + internal new static SocketNewsChannel Create(SocketGuild guild, ClientStateManager state, Model model) { var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs index 91bca50542..d98a31ff2e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -43,7 +43,7 @@ public IReadOnlyCollection Speakers internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { } - internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model) + internal new static SocketStageChannel Create(SocketGuild guild, ClientStateManager state, Model model) { var entity = new SocketStageChannel(guild.Discord, model.Id, guild); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index e4a299edc0..5aecf11c13 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -63,13 +63,13 @@ internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild gu if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord); } - internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) + internal new static SocketTextChannel Create(SocketGuild guild, ClientStateManager state, Model model) { var entity = new SocketTextChannel(guild.Discord, model.Id, guild); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { base.Update(state, model); CategoryId = model.CategoryId; @@ -117,7 +117,7 @@ public virtual async Task CreateThreadAsync(string name, Th { var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); - var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.State, model); + var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.StateManager, model); if(Discord.AlwaysDownloadUsers && Discord.HasGatewayIntent(GatewayIntents.GuildMembers)) await thread.DownloadUsersAsync(); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 78462b0620..3fbf9d6e42 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -118,7 +118,7 @@ internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulo CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); } - internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) + internal new static SocketThreadChannel Create(SocketGuild guild, ClientStateManager state, Model model) { var parent = guild.GetChannel(model.CategoryId.Value); var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); @@ -126,7 +126,7 @@ internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulo return entity; } - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { base.Update(state, model); @@ -171,7 +171,6 @@ internal SocketThreadUser AddOrUpdateThreadMember(ThreadMember model, SocketGuil else { member = SocketThreadUser.Create(Guild, this, model, guildMember); - member.GlobalUser.AddRef(); _members[member.Id] = member; } return member; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 00003d4ed4..d684ffa9fc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -55,14 +55,14 @@ internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, SocketGuild g : base(discord, id, guild) { } - internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) + internal new static SocketVoiceChannel Create(SocketGuild guild, ClientStateManager state, Model model) { var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); entity.Update(state, model); return entity; } /// - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { base.Update(state, model); CategoryId = model.CategoryId; diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 8b376b3ed2..61a5ff6197 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -14,11 +14,11 @@ using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; using ExtendedModel = Discord.API.Gateway.ExtendedGuild; using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; -using MemberModel = Discord.API.GuildMember; +using MemberModel = Discord.IMemberModel; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; using RoleModel = Discord.API.Role; -using UserModel = Discord.API.User; +using UserModel = Discord.IUserModel; using VoiceStateModel = Discord.API.VoiceState; using StickerModel = Discord.API.Sticker; using EventModel = Discord.API.GuildScheduledEvent; @@ -38,7 +38,7 @@ public class SocketGuild : SocketEntity, IGuild, IDisposable private TaskCompletionSource _syncPromise, _downloaderPromise; private TaskCompletionSource _audioConnectPromise; private ConcurrentDictionary _channels; - private ConcurrentDictionary _members; + //private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; private ConcurrentDictionary _stickers; @@ -305,7 +305,7 @@ public IReadOnlyCollection ThreadChannels /// /// Gets the current logged-in user. /// - public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; + public SocketGuildUser CurrentUser => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(Discord.CurrentUser.Id) : null; /// /// Gets the built-in role containing all users in this guild. /// @@ -324,7 +324,7 @@ public IReadOnlyCollection Channels get { var channels = _channels; - var state = Discord.State; + var state = Discord.StateManager; return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); } } @@ -356,7 +356,7 @@ public IReadOnlyCollection Stickers /// /// A collection of guild users found within this guild. /// - public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); + public IReadOnlyCollection Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray.Empty; /// /// Gets a collection of all roles in this guild. /// @@ -382,13 +382,13 @@ internal SocketGuild(DiscordSocketClient client, ulong id) _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); } - internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) + internal static SocketGuild Create(DiscordSocketClient discord, ClientStateManager state, ExtendedModel model) { var entity = new SocketGuild(discord, model.Id); entity.Update(state, model); return entity; } - internal void Update(ClientState state, ExtendedModel model) + internal void Update(ClientStateManager state, ExtendedModel model) { IsAvailable = !(model.Unavailable ?? false); if (!IsAvailable) @@ -397,8 +397,6 @@ internal void Update(ClientState state, ExtendedModel model) _events = new ConcurrentDictionary(); if (_channels == null) _channels = new ConcurrentDictionary(); - if (_members == null) - _members = new ConcurrentDictionary(); if (_roles == null) _roles = new ConcurrentDictionary(); /*if (Emojis == null) @@ -431,25 +429,6 @@ internal void Update(ClientState state, ExtendedModel model) _channels = channels; - var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); - { - for (int i = 0; i < model.Members.Length; i++) - { - var member = SocketGuildUser.Create(this, state, model.Members[i]); - if (members.TryAdd(member.Id, member)) - member.GlobalUser.AddRef(); - } - DownloadedMemberCount = members.Count; - - for (int i = 0; i < model.Presences.Length; i++) - { - if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) - member.Update(state, model.Presences[i], true); - } - } - _members = members; - MemberCount = model.MemberCount; - var voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); { for (int i = 0; i < model.VoiceStates.Length; i++) @@ -473,6 +452,10 @@ internal void Update(ClientState state, ExtendedModel model) } _events = events; + DownloadedMemberCount = model.Members.Length; + + MemberCount = model.MemberCount; + _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); @@ -480,7 +463,7 @@ internal void Update(ClientState state, ExtendedModel model) /*if (!model.Large) _ = _downloaderPromise.TrySetResultAsync(true);*/ } - internal void Update(ClientState state, Model model) + internal void Update(ClientStateManager state, Model model) { AFKChannelId = model.AFKChannelId; if (model.WidgetChannelId.IsSpecified) @@ -561,31 +544,18 @@ internal void Update(ClientState state, Model model) else _stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 7); } - /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related + + internal async ValueTask UpdateCacheAsync(ExtendedModel model) { - var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); - { - for (int i = 0; i < model.Members.Length; i++) - { - var member = SocketGuildUser.Create(this, state, model.Members[i]); - members.TryAdd(member.Id, member); - } - DownloadedMemberCount = members.Count; + await Discord.StateManager.PresenceStore.BulkAddOrUpdateAsync(model.Presences); - for (int i = 0; i < model.Presences.Length; i++) - { - if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) - member.Update(state, model.Presences[i], true); - } - } - _members = members; + await Discord.StateManager.UserStore.BulkAddOrUpdateAsync(model.Members.Select(x => x.User)); - var _ = _syncPromise.TrySetResultAsync(true); - //if (!model.Large) - // _ = _downloaderPromise.TrySetResultAsync(true); - }*/ + if(Discord.StateManager.TryGetMemberStore(Id, out var store)) + store.BulkAddOrUpdate(model.Members); + } - internal void Update(ClientState state, EmojiUpdateModel model) + internal void Update(ClientStateManager state, EmojiUpdateModel model) { var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) @@ -682,7 +652,7 @@ public Task RemoveBanAsync(ulong userId, RequestOptions options = null) /// public SocketGuildChannel GetChannel(ulong id) { - var channel = Discord.State.GetChannel(id) as SocketGuildChannel; + var channel = Discord.StateManager.GetChannel(id) as SocketGuildChannel; if (channel?.Guild.Id == Id) return channel; return null; @@ -799,7 +769,7 @@ public Task CreateStageChannelAsync(string name, Action CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); - internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) + internal SocketGuildChannel AddChannel(ClientStateManager state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); _channels.TryAdd(model.Id, channel); @@ -807,26 +777,26 @@ internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) return channel; } - internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) + internal SocketGuildChannel AddOrUpdateChannel(ClientStateManager state, ChannelModel model) { if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) - channel.Update(Discord.State, model); + channel.Update(Discord.StateManager, model); else { - channel = SocketGuildChannel.Create(this, Discord.State, model); + channel = SocketGuildChannel.Create(this, Discord.StateManager, model); _channels[channel.Id] = channel; state.AddChannel(channel); } return channel; } - internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) + internal SocketGuildChannel RemoveChannel(ClientStateManager state, ulong id) { if (_channels.TryRemove(id, out var _)) return state.RemoveChannel(id) as SocketGuildChannel; return null; } - internal void PurgeChannelCache(ClientState state) + internal void PurgeChannelCache(ClientStateManager state) { foreach (var channelId in _channels) state.RemoveChannel(channelId.Key); @@ -880,7 +850,7 @@ public async Task> GetApplicationC foreach (var command in commands) { - Discord.State.AddCommand(command); + Discord.StateManager.AddCommand(command); } return commands.ToImmutableArray(); @@ -898,7 +868,7 @@ public async Task> GetApplicationC /// public async ValueTask GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { - var command = Discord.State.GetCommand(id); + var command = Discord.StateManager.GetCommand(id); if (command != null) return command; @@ -913,7 +883,7 @@ public async ValueTask GetApplicationCommandAsync(ulon command = SocketApplicationCommand.Create(Discord, model, Id); - Discord.State.AddCommand(command); + Discord.StateManager.AddCommand(command); return command; } @@ -930,7 +900,7 @@ public async Task CreateApplicationCommandAsync(Applic { var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); - var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); + var entity = Discord.StateManager.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); entity.Update(model); @@ -952,11 +922,11 @@ public async Task> BulkOverwriteAp var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); - Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); + Discord.StateManager.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); foreach(var entity in entities) { - Discord.State.AddCommand(entity); + Discord.StateManager.AddCommand(entity); } return entities.ToImmutableArray(); @@ -1020,7 +990,7 @@ public SocketRole GetRole(ulong id) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); internal SocketRole AddRole(RoleModel model) { - var role = SocketRole.Create(this, Discord.State, model); + var role = SocketRole.Create(this, Discord.StateManager, model); _roles[model.Id] = role; return role; } @@ -1034,7 +1004,7 @@ internal SocketRole RemoveRole(ulong id) internal SocketRole AddOrUpdateRole(RoleModel model) { if (_roles.TryGetValue(model.Id, out SocketRole role)) - _roles[model.Id].Update(Discord.State, model); + _roles[model.Id].Update(Discord.StateManager, model); else role = AddRole(model); @@ -1089,60 +1059,43 @@ public Task AddGuildUserAsync(ulong id, string accessToken, Actio /// A guild user associated with the specified ; if none is found. /// public SocketGuildUser GetUser(ulong id) - { - if (_members.TryGetValue(id, out SocketGuildUser member)) - return member; - return null; - } + => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(id) : null; /// public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); internal SocketGuildUser AddOrUpdateUser(UserModel model) { - if (_members.TryGetValue(model.Id, out SocketGuildUser member)) - member.GlobalUser?.Update(Discord.State, model); + SocketGuildUser member; + if ((member = GetUser(model.Id)) != null) + member.Update(model); else { - member = SocketGuildUser.Create(this, Discord.State, model); - member.GlobalUser.AddRef(); - _members[member.Id] = member; + member = SocketGuildUser.Create(Id, Discord, model); DownloadedMemberCount++; } return member; } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { - if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) - member.Update(Discord.State, model); - else - { - member = SocketGuildUser.Create(this, Discord.State, model); - member.GlobalUser.AddRef(); - _members[member.Id] = member; - DownloadedMemberCount++; - } - return member; - } - internal SocketGuildUser AddOrUpdateUser(PresenceModel model) - { - if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) - member.Update(Discord.State, model, false); + SocketGuildUser member; + if ((member = GetUser(model.Id)) != null) + member.Update(model); else { - member = SocketGuildUser.Create(this, Discord.State, model); - member.GlobalUser.AddRef(); - _members[member.Id] = member; + member = SocketGuildUser.Create(Id, Discord, model); DownloadedMemberCount++; } return member; } internal SocketGuildUser RemoveUser(ulong id) { - if (_members.TryRemove(id, out SocketGuildUser member)) + SocketGuildUser member; + if ((member = GetUser(id)) != null) { DownloadedMemberCount--; - member.GlobalUser.RemoveRef(Discord); + if (Discord.StateManager.TryGetMemberStore(Id, out var store)) + store.Remove(id); return member; } return null; @@ -1158,18 +1111,17 @@ internal SocketGuildUser RemoveUser(ulong id) /// The predicate used to select which users to clear. public void PurgeUserCache(Func predicate) { - var membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); - var membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); + var users = Users.ToArray(); - foreach (var member in membersToPurge) - if(_members.TryRemove(member.Id, out _)) - member.GlobalUser.RemoveRef(Discord); + var membersToPurge = users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); + var membersToKeep = users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); - foreach (var member in membersToKeep) - _members.TryAdd(member.Id, member); + if(Discord.StateManager.TryGetMemberStore(Id, out var store)) + foreach (var member in membersToPurge) + store.Remove(member.Id); _downloaderPromise = new TaskCompletionSource(); - DownloadedMemberCount = _members.Count; + DownloadedMemberCount = membersToKeep.Count(); } /// @@ -1291,7 +1243,6 @@ public Task> GetEventsAsync(RequestOptions o /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. @@ -1537,7 +1488,7 @@ public Task DeleteStickerAsync(SocketCustomSticker sticker, RequestOptions optio #endregion #region Voice States - internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) + internal async Task AddOrUpdateVoiceStateAsync(ClientStateManager state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs index a86aafadf9..bdf89d1abe 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -89,13 +89,13 @@ internal void Update(Model model) if(guildUser != null) { if(model.Creator.IsSpecified) - guildUser.Update(Discord.State, model.Creator.Value); + guildUser.Update(model.Creator.Value); Creator = guildUser; } else if (guildUser == null && model.Creator.IsSpecified) { - guildUser = SocketGuildUser.Create(Guild, Discord.State, model.Creator.Value); + guildUser = SocketGuildUser.Create(Guild.Id, Discord, model.Creator.Value); Creator = guildUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index aeff465bde..d019a87a9b 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -56,18 +56,18 @@ internal override void Update(Model model) if (Channel is SocketGuildChannel channel) { if (model.Message.Value.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(channel.Guild, Discord.State, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); + author = SocketWebhookUser.Create(channel.Guild, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); else if (model.Message.Value.Author.IsSpecified) author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); } else if (model.Message.Value.Author.IsSpecified) author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); - Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value); + Message = SocketUserMessage.Create(Discord, Discord.StateManager, author, Channel, model.Message.Value); } else { - Message.Update(Discord.State, model.Message.Value); + Message.Update(Discord.StateManager, model.Message.Value); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index d722c5a138..e8687dab30 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -29,7 +29,7 @@ internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T mod { foreach (var user in resolved.Users.Value) { - var socketUser = discord.GetOrCreateUser(discord.State, user.Value); + var socketUser = discord.GetOrCreateUser(discord.StateManager, user.Value); Users.Add(ulong.Parse(user.Key), socketUser); } @@ -50,11 +50,11 @@ internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T mod : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); socketChannel = guild != null - ? SocketGuildChannel.Create(guild, discord.State, channelModel) - : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + ? SocketGuildChannel.Create(guild, discord.StateManager, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.StateManager, channelModel); } - discord.State.AddChannel(socketChannel); + discord.StateManager.AddChannel(socketChannel); Channels.Add(ulong.Parse(channel.Key), socketChannel); } } @@ -88,7 +88,7 @@ internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T mod if (guild != null) { if (msg.Value.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, discord.State, msg.Value.Author.Value, msg.Value.WebhookId.Value); + author = SocketWebhookUser.Create(guild, msg.Value.Author.Value, msg.Value.WebhookId.Value); else author = guild.GetUser(msg.Value.Author.Value.Id); } @@ -99,11 +99,11 @@ internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T mod { if (!msg.Value.GuildId.IsSpecified) // assume it is a DM { - channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State); + channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.StateManager); } } - var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value); + var message = SocketMessage.Create(discord, discord.StateManager, author, channel, msg.Value); Messages.Add(message.Id, message); } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 6668426e17..787a106935 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.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 @@ public abstract class SocketMessage : SocketEntity, IMessage 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 @@ public abstract class SocketMessage : SocketEntity, IMessage /// /// 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,7 +139,13 @@ internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChan Author = author; Source = source; } - internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + + //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 || model.Type == MessageType.Reply || @@ -140,55 +156,52 @@ internal static SocketMessage Create(DiscordSocketClient discord, ClientState st else return SocketSystemMessage.Create(discord, state, author, channel, model); } - internal virtual void Update(ClientState 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 @@ internal virtual void Update(ClientState state, Model model) 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, state, 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, state, model.Interaction.Value.User)); - } - - if (model.Flags.IsSpecified) - Flags = model.Flags.Value; + Flags = model.Flags; } /// @@ -309,7 +300,6 @@ public Task DeleteAsync(RequestOptions options = null) /// IReadOnlyCollection IMessage.Stickers => Stickers; - internal void AddReaction(SocketReaction reaction) { _reactions.Add(reaction); @@ -347,5 +337,314 @@ public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options 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 32cac7d8b7..2beceacdeb 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -20,12 +20,19 @@ public class SocketReaction : IReaction /// 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 @@ public class SocketReaction : IReaction /// /// /// - /// 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 @@ public class SocketReaction : IReaction /// 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 @@ internal static SocketReaction Create(Model model, ISocketMessageChannel channel /// 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/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index ec22a7703c..50fbec4b76 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -13,13 +13,13 @@ internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessa : base(discord, id, channel, author, MessageSource.System) { } - internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) { var entity = new SocketSystemMessage(discord, model.Id, channel, author); entity.Update(state, model); return entity; } - internal override void Update(ClientState state, Model model) + internal override void Update(ClientStateManager state, Model model) { base.Update(state, model); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index e5776a089a..617d70838d 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.Diagnostics; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.Message; +using Model = Discord.IMessageModel; namespace Discord.WebSocket { @@ -17,11 +17,12 @@ public class SocketUserMessage : SocketMessage, IUserMessage { 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(); /// @@ -53,33 +54,29 @@ internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessage : base(discord, id, channel, author, source) { } - internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + 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(ClientState 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 @@ internal override void Update(ClientState state, Model model) _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 @@ internal override void Update(ClientState state, Model model) _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, state, 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, state, 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/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 1e90b8f5c4..fdadaa44a3 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -67,13 +67,13 @@ internal SocketRole(SocketGuild guild, ulong id) { Guild = guild; } - internal static SocketRole Create(SocketGuild guild, ClientState state, Model model) + internal static SocketRole Create(SocketGuild guild, ClientStateManager state, Model model) { var entity = new SocketRole(guild, model.Id); entity.Update(state, model); return entity; } - internal void Update(ClientState state, Model model) + internal void Update(ClientStateManager state, Model model) { Name = model.Name; IsHoisted = model.Hoist; diff --git a/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs b/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs index ca7d2d0f1a..b7e1a3cc82 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 @@ internal static SocketUnknownSticker Create(DiscordSocketClient client, Model mo internal void Update(Model model) { Name = model.Name; - Format = model.FormatType; + Format = model.Format; } /// diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 236e7d4326..1187a235a7 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,51 +1,36 @@ using System; using System.Diagnostics; using System.Linq; -using Model = Discord.API.User; +using Model = Discord.IUserModel; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class SocketGlobalUser : SocketUser + internal class SocketGlobalUser : SocketUser, IDisposable { public override bool IsBot { get; internal set; } public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - internal override SocketPresence Presence { get; set; } - public override bool IsWebhook => false; - internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } - - private readonly object _lockObj = new object(); - private ushort _references; private SocketGlobalUser(DiscordSocketClient discord, ulong id) : base(discord, id) { + } - internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model) { var entity = new SocketGlobalUser(discord, model.Id); - entity.Update(state, model); + entity.Update(model); return entity; } - internal void AddRef() - { - checked - { - lock (_lockObj) - _references++; - } - } - internal void RemoveRef(DiscordSocketClient discord) + ~SocketGlobalUser() => Dispose(); + public override void Dispose() { - lock (_lockObj) - { - if (--_references <= 0) - discord.RemoveUser(Id); - } + GC.SuppressFinalize(this); + Discord.StateManager.UserStore.RemoveReference(Id); } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index a40ae59bec..ed13f63145 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -18,38 +18,33 @@ public class SocketGroupUser : SocketUser, IGroupUser /// A representing the channel of which the user belongs to. /// public SocketGroupChannel Channel { get; } - /// - internal override SocketGlobalUser GlobalUser { get; set; } - - /// - public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } - /// - public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } - /// - public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } - /// - public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } - /// - internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } /// public override bool IsWebhook => false; - internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) - : base(channel.Discord, globalUser.Id) + internal SocketGroupUser(SocketGroupChannel channel, ulong userId) + : base(channel.Discord, userId) { Channel = channel; - GlobalUser = globalUser; } - internal static SocketGroupUser Create(SocketGroupChannel channel, ClientState state, Model model) + internal static SocketGroupUser Create(SocketGroupChannel channel, Model model) { - var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); - entity.Update(state, model); + var entity = new SocketGroupUser(channel, model.Id); + entity.Update(model); return entity; } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; + public override void Dispose() + { + GC.SuppressFinalize(this); + + if (GlobalUser.IsValueCreated) + GlobalUser.Value.Dispose(); + } + ~SocketGroupUser() => Dispose(); + #endregion #region IVoiceState diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 051687b785..8757d206a0 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -6,9 +6,9 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using UserModel = Discord.API.User; -using MemberModel = Discord.API.GuildMember; -using PresenceModel = Discord.API.Presence; +using UserModel = Discord.IUserModel; +using MemberModel = Discord.IMemberModel; +using PresenceModel = Discord.IPresenceModel; namespace Discord.WebSocket { @@ -16,19 +16,23 @@ namespace Discord.WebSocket /// Represents a WebSocket-based guild user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketGuildUser : SocketUser, IGuildUser + public class SocketGuildUser : SocketUser, IGuildUser, ICached, IDisposable { #region SocketGuildUser private long? _premiumSinceTicks; private long? _timedOutTicks; private long? _joinedAtTicks; private ImmutableArray _roleIds; + private ulong _guildId; - internal override SocketGlobalUser GlobalUser { get; set; } /// /// Gets the guild the user is in. /// - public SocketGuild Guild { get; } + public Lazy Guild { get; } // TODO: convert to LazyCached once guilds are cached. + /// + /// Gets the ID of the guild that the user is in. + /// + public ulong GuildId => _guildId; /// public string DisplayName => Nickname ?? Username; /// @@ -38,17 +42,16 @@ public class SocketGuildUser : SocketUser, IGuildUser /// public string GuildAvatarId { get; private set; } /// - public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } /// - public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } /// - public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } /// - public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } /// - public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); - internal override SocketPresence Presence { get; set; } + public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this)); /// public override bool IsWebhook => false; @@ -71,14 +74,13 @@ public class SocketGuildUser : SocketUser, IGuildUser /// public bool? IsPending { get; private set; } - /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); /// /// Returns a collection of roles that the user possesses. /// public IReadOnlyCollection Roles - => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); + => _roleIds.Select(id => Guild.Value.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); /// /// Returns the voice channel the user is in, or null if none. /// @@ -92,8 +94,8 @@ public IReadOnlyCollection Roles /// A representing the user's voice status; null if the user is not /// connected to a voice channel. /// - public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); - public AudioInStream AudioStream => Guild.GetAudioStream(Id); + public SocketVoiceState? VoiceState => Guild.Value.GetVoiceState(Id); + public AudioInStream AudioStream => Guild.Value.GetAudioStream(Id); /// public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); /// @@ -119,13 +121,13 @@ public int Hierarchy { get { - if (Guild.OwnerId == Id) + if (Guild.Value.OwnerId == Id) return int.MaxValue; int maxPos = 0; for (int i = 0; i < _roleIds.Length; i++) { - var role = Guild.GetRole(_roleIds[i]); + var role = Guild.Value.GetRole(_roleIds[i]); if (role != null && role.Position > maxPos) maxPos = role.Position; } @@ -133,79 +135,43 @@ public int Hierarchy } } - internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) - : base(guild.Discord, globalUser.Id) + internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client) + : base(client, userId) { - Guild = guild; - GlobalUser = globalUser; + _guildId = guildId; + Guild = new Lazy(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) + internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) { - var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); - entity.Update(state, model); - entity.UpdateRoles(new ulong[0]); + var entity = new SocketGuildUser(guildId, model.Id, client); + if (entity.Update(model)) + client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(entity.ToModel()); + entity.UpdateRoles(Array.Empty()); return entity; } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel model) + internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) { - var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); - entity.Update(state, model); - if (!model.Roles.IsSpecified) - entity.UpdateRoles(new ulong[0]); + var entity = new SocketGuildUser(guildId, model.Id, client); + entity.Update(model); + client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(model); return entity; } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) - { - var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); - entity.Update(state, model, false); - if (!model.Roles.IsSpecified) - entity.UpdateRoles(new ulong[0]); - return entity; - } - internal void Update(ClientState state, MemberModel model) - { - base.Update(state, model.User); - if (model.JoinedAt.IsSpecified) - _joinedAtTicks = model.JoinedAt.Value.UtcTicks; - if (model.Nick.IsSpecified) - Nickname = model.Nick.Value; - if (model.Avatar.IsSpecified) - GuildAvatarId = model.Avatar.Value; - if (model.Roles.IsSpecified) - UpdateRoles(model.Roles.Value); - if (model.PremiumSince.IsSpecified) - _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; - if (model.TimedOutUntil.IsSpecified) - _timedOutTicks = model.TimedOutUntil.Value?.UtcTicks; - if (model.Pending.IsSpecified) - IsPending = model.Pending.Value; - } - internal void Update(ClientState state, PresenceModel model, bool updatePresence) - { - if (updatePresence) - { - Update(model); - } - if (model.Nick.IsSpecified) - Nickname = model.Nick.Value; - if (model.Roles.IsSpecified) - UpdateRoles(model.Roles.Value); - if (model.PremiumSince.IsSpecified) - _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; - } - - internal override void Update(PresenceModel model) + internal void Update(MemberModel model) { - Presence ??= new SocketPresence(); - - Presence.Update(model); - GlobalUser.Update(model); + _joinedAtTicks = model.JoinedAt.HasValue ? model.JoinedAt.Value.UtcTicks : null; + Nickname = model.Nickname; + GuildAvatarId = model.GuildAvatar; + UpdateRoles(model.Roles); + if (model.PremiumSince.HasValue) + _premiumSinceTicks = model.PremiumSince.Value.UtcTicks; + if (model.CommunicationsDisabledUntil.HasValue) + _timedOutTicks = model.CommunicationsDisabledUntil.Value.UtcTicks; + IsPending = model.IsPending.GetValueOrDefault(false); } - private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(Guild.Id); + roles.Add(_guildId); for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); @@ -249,7 +215,7 @@ public Task RemoveTimeOutAsync(RequestOptions options = null) => UserHelper.RemoveTimeOutAsync(this, Discord, options); /// public ChannelPermissions GetPermissions(IGuildChannel channel) - => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + => new ChannelPermissions(Permissions.ResolveChannel(Guild.Value, this, channel, GuildPermissions.RawValue)); /// public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -259,23 +225,30 @@ public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort /// public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); + => CDN.GetGuildUserAvatarUrl(Id, _guildId, GuildAvatarId, size, format); private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; - internal new SocketGuildUser Clone() + internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + + public override void Dispose() { - var clone = MemberwiseClone() as SocketGuildUser; - clone.GlobalUser = GlobalUser.Clone(); - return clone; + if (IsFreed) + return; + + GC.SuppressFinalize(this); + Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); + IsFreed = true; } + ~SocketGuildUser() => Dispose(); + #endregion #region IGuildUser /// - IGuild IGuildUser.Guild => Guild; + IGuild IGuildUser.Guild => Guild.Value; /// - ulong IGuildUser.GuildId => Guild.Id; + ulong IGuildUser.GuildId => _guildId; /// IReadOnlyCollection IGuildUser.RoleIds => _roleIds; @@ -283,5 +256,50 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si /// IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; #endregion + + #region Cache + + internal new class CacheModel : MemberModel + { + public ulong Id { get; set; } + public string Nickname { get; set; } + + public string GuildAvatar { get; set; } + + public ulong[] Roles { get; set; } + + public DateTimeOffset? JoinedAt { get; set; } + + public DateTimeOffset? PremiumSince { get; set; } + + public bool IsDeaf { get; set; } + + public bool IsMute { get; set; } + + public bool? IsPending { get; set; } + + public DateTimeOffset? CommunicationsDisabledUntil { get; set; } + } + internal new MemberModel ToModel() + { + var model = Discord.StateManager.GetModel(); + model.Id = Id; + model.Nickname = Nickname; + model.GuildAvatar = GuildAvatarId; + model.Roles = _roleIds.ToArray(); + model.JoinedAt = JoinedAt; + model.PremiumSince = PremiumSince; + model.IsDeaf = IsDeafened; + model.IsMute = IsMuted; + model.IsPending = IsPending; + model.CommunicationsDisabledUntil = TimedOutUntil; + return model; + } + + MemberModel ICached.ToModel() + => ToModel(); + + void ICached.Update(MemberModel model) => Update(model); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 5250e15ad5..220e853059 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using Model = Discord.API.Presence; +using Model = Discord.IPresenceModel; namespace Discord.WebSocket { @@ -11,8 +11,13 @@ namespace Discord.WebSocket /// Represents the WebSocket user's presence status. This may include their online status and their activity. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketPresence : IPresence + public class SocketPresence : IPresence, ICached { + internal ulong UserId; + internal ulong? GuildId; + internal bool IsFreed; + internal DiscordSocketClient Discord; + /// public UserStatus Status { get; private set; } /// @@ -20,17 +25,24 @@ public class SocketPresence : IPresence /// public IReadOnlyCollection Activities { get; private set; } - internal SocketPresence() { } - internal SocketPresence(UserStatus status, IImmutableSet activeClients, IImmutableList activities) + public static SocketPresence Default + => new SocketPresence(null, UserStatus.Offline, null, null); + + internal SocketPresence(DiscordSocketClient discord) + { + Discord = discord; + } + internal SocketPresence(DiscordSocketClient discord, UserStatus status, IImmutableSet activeClients, IImmutableList activities) + : this(discord) { Status = status; ActiveClients = activeClients ?? ImmutableHashSet.Empty; Activities = activities ?? ImmutableList.Empty; } - internal static SocketPresence Create(Model model) + internal static SocketPresence Create(DiscordSocketClient client, Model model) { - var entity = new SocketPresence(); + var entity = new SocketPresence(client); entity.Update(model); return entity; } @@ -38,8 +50,10 @@ internal static SocketPresence Create(Model model) internal void Update(Model model) { Status = model.Status; - ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray.Empty; + ActiveClients = model.ActiveClients.Length > 0 ? model.ActiveClients.ToImmutableArray() : ImmutableArray.Empty; Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray.Empty; + UserId = model.UserId; + GuildId = model.GuildId; } /// @@ -76,9 +90,9 @@ private static IReadOnlyCollection ConvertClientTypesDict(IDictionar /// /// A list of all that this user currently has available. /// - private static IImmutableList ConvertActivitiesList(IList activities) + private static IImmutableList ConvertActivitiesList(IActivityModel[] activities) { - if (activities == null || activities.Count == 0) + if (activities == null || activities.Length == 0) return ImmutableList.Empty; var list = new List(); foreach (var activity in activities) @@ -96,5 +110,122 @@ private static IImmutableList ConvertActivitiesList(IList a private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; + + ~SocketPresence() => Dispose(); + public void Dispose() + { + if (IsFreed) + return; + + GC.SuppressFinalize(this); + + if(Discord != null) + { + Discord.StateManager.PresenceStore.RemoveReference(UserId); + IsFreed = true; + } + } + + #region Cache + internal class CacheModel : Model + { + public UserStatus Status { get; set; } + + public ClientType[] ActiveClients { get; set; } + + public IActivityModel[] Activities { get; set; } + + public ulong UserId { get; set; } + + public ulong? GuildId { get; set; } + + ulong IEntityModel.Id + { + get => UserId; + set => UserId = value; + } + } + + internal class ActivityCacheModel : IActivityModel + { + public string Id { get; set; } + public string Url { get; set; } + public string Name { get; set; } + public ActivityType Type { get; set; } + public string Details { get; set; } + public string State { get; set; } + public ActivityProperties Flags { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public IEmojiModel Emoji { get; set; } + public ulong? ApplicationId { get; set; } + public string SyncId { get; set; } + public string SessionId { get; set; } + public string LargeImage { get; set; } + public string LargeText { get; set; } + public string SmallImage { get; set; } + public string SmallText { get; set; } + public string PartyId { get; set; } + public long[] PartySize { get; set; } + public string JoinSecret { get; set; } + public string SpectateSecret { get; set; } + public string MatchSecret { get; set; } + public DateTimeOffset? TimestampStart { get; set; } + public DateTimeOffset? TimestampEnd { get; set; } + } + + private class EmojiCacheModel : IEmojiModel + { + public ulong? Id { get; set; } + public string Name { get; set; } + public ulong[] Roles { get; set; } + public bool RequireColons { get; set; } + public bool IsManaged { get; set; } + public bool IsAnimated { get; set; } + public bool IsAvailable { get; set; } + public ulong? CreatorId { get; set; } + } + + internal Model ToModel() + { + var model = Discord.StateManager.GetModel(); + model.Status = Status; + model.ActiveClients = ActiveClients.ToArray(); + model.UserId = UserId; + model.GuildId = GuildId; + model.Activities = Activities.Select(x => + { + switch (x) + { + case Game game: + switch (game) + { + case RichGame richGame: + return richGame.ToModel(); + case SpotifyGame spotify: + return spotify.ToModel(); + case CustomStatusGame custom: + return custom.ToModel(); + case StreamingGame stream: + return stream.ToModel(); + } + break; + } + + return new ActivityCacheModel + { + Name = x.Name, + Details = x.Details, + Flags = x.Flags, + Type = x.Type + }; + }).ToArray(); + return model; + } + + Model ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); + bool ICached.IsFreed => IsFreed; + + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 3bde1beab7..087d4ba297 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -2,7 +2,8 @@ using System; using System.Diagnostics; using System.Threading.Tasks; -using Model = Discord.API.User; +using Model = Discord.ICurrentUserModel; +using UserModel = Discord.IUserModel; namespace Discord.WebSocket { @@ -10,7 +11,7 @@ namespace Discord.WebSocket /// Represents the logged-in WebSocket-based user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketSelfUser : SocketUser, ISelfUser + public class SocketSelfUser : SocketUser, ISelfUser, ICached { /// public string Email { get; private set; } @@ -18,18 +19,6 @@ public class SocketSelfUser : SocketUser, ISelfUser public bool IsVerified { get; private set; } /// public bool IsMfaEnabled { get; private set; } - internal override SocketGlobalUser GlobalUser { get; set; } - - /// - public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } - /// - public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } - /// - public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } - /// - public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } - /// - internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } /// public UserProperties Flags { get; internal set; } /// @@ -40,48 +29,52 @@ public class SocketSelfUser : SocketUser, ISelfUser /// public override bool IsWebhook => false; - internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) - : base(discord, globalUser.Id) + internal SocketSelfUser(DiscordSocketClient discord, ulong userId) + : base(discord, userId) { - GlobalUser = globalUser; + } - internal static SocketSelfUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketSelfUser Create(DiscordSocketClient discord, Model model) { - var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); - entity.Update(state, model); + var entity = new SocketSelfUser(discord, model.Id); + entity.Update(model); return entity; } - internal override bool Update(ClientState state, Model model) + internal override bool Update(UserModel model) { - bool hasGlobalChanges = base.Update(state, model); - if (model.Email.IsSpecified) + bool hasGlobalChanges = base.Update(model); + + if (model is not Model currentUserModel) + throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); + + if(currentUserModel.Email != Email) { - Email = model.Email.Value; + Email = currentUserModel.Email; hasGlobalChanges = true; } - if (model.Verified.IsSpecified) + if (currentUserModel.IsVerified.HasValue) { - IsVerified = model.Verified.Value; + IsVerified = currentUserModel.IsVerified.Value; hasGlobalChanges = true; } - if (model.MfaEnabled.IsSpecified) + if (currentUserModel.IsMfaEnabled.HasValue) { - IsMfaEnabled = model.MfaEnabled.Value; + IsMfaEnabled = currentUserModel.IsMfaEnabled.Value; hasGlobalChanges = true; } - if (model.Flags.IsSpecified && model.Flags.Value != Flags) + if (currentUserModel.Flags != Flags) { - Flags = (UserProperties)model.Flags.Value; + Flags = currentUserModel.Flags; hasGlobalChanges = true; } - if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType) + if (currentUserModel.PremiumType != PremiumType) { - PremiumType = model.PremiumType.Value; + PremiumType = currentUserModel.PremiumType; hasGlobalChanges = true; } - if (model.Locale.IsSpecified && model.Locale.Value != Locale) + if (currentUserModel.Locale != Locale) { - Locale = model.Locale.Value; + Locale = currentUserModel.Locale; hasGlobalChanges = true; } return hasGlobalChanges; @@ -93,5 +86,63 @@ public Task ModifyAsync(Action func, RequestOptions options private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + public override void Dispose() + { + if (IsFreed) + return; + + GC.SuppressFinalize(this); + Discord.StateManager.UserStore.RemoveReference(Id); + IsFreed = true; + } + + #region Cache + internal new class CacheModel : Model + { + public bool? IsVerified { get; set; } + + public string Email { get; set; } + + public bool? IsMfaEnabled { get; set; } + + public UserProperties Flags { get; set; } + + public PremiumType PremiumType { get; set; } + + public string Locale { get; set; } + + public UserProperties PublicFlags { get; set; } + + public string Username { get; set; } + + public string Discriminator { get; set; } + + public bool? IsBot { get; set; } + + public string Avatar { get; set; } + + public ulong Id { get; set; } + } + + internal new Model ToModel() + { + var model = Discord.StateManager.GetModel(); + model.Avatar = AvatarId; + model.Discriminator = Discriminator; + model.Email = Email; + model.Flags = Flags; + model.IsBot = IsBot; + model.IsMfaEnabled = IsMfaEnabled; + model.Locale = Locale; + model.PremiumType = PremiumType; + model.PublicFlags = PublicFlags ?? UserProperties.None; + model.Username = Username; + model.Id = Id; + return model; + } + + Model ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 6eddd876d0..0b614adaec 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.ThreadMember; +using Model = Discord.IThreadMemberModel; using System.Collections.Immutable; namespace Discord.WebSocket @@ -10,12 +10,12 @@ namespace Discord.WebSocket /// /// Represents a thread user received over the gateway. /// - public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser + public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached { /// /// Gets the this user is in. /// - public SocketThreadChannel Thread { get; private set; } + public Lazy Thread { get; private set; } /// public DateTimeOffset ThreadJoinedAt { get; private set; } @@ -23,126 +23,142 @@ public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser /// /// Gets the guild this user is in. /// - public SocketGuild Guild { get; private set; } + public Lazy Guild { get; private set; } /// public DateTimeOffset? JoinedAt - => GuildUser.JoinedAt; + => GuildUser.Value.JoinedAt; /// public string DisplayName - => GuildUser.Nickname ?? GuildUser.Username; + => GuildUser.Value.Nickname ?? GuildUser.Value.Username; /// public string Nickname - => GuildUser.Nickname; + => GuildUser.Value.Nickname; /// public DateTimeOffset? PremiumSince - => GuildUser.PremiumSince; + => GuildUser.Value.PremiumSince; /// public DateTimeOffset? TimedOutUntil - => GuildUser.TimedOutUntil; + => GuildUser.Value.TimedOutUntil; /// public bool? IsPending - => GuildUser.IsPending; + => GuildUser.Value.IsPending; + /// public int Hierarchy - => GuildUser.Hierarchy; + => GuildUser.Value.Hierarchy; /// public override string AvatarId { - get => GuildUser.AvatarId; - internal set => GuildUser.AvatarId = value; + get => GuildUser.Value.AvatarId; + internal set => GuildUser.Value.AvatarId = value; } + /// public string DisplayAvatarId => GuildAvatarId ?? AvatarId; /// public string GuildAvatarId - => GuildUser.GuildAvatarId; + => GuildUser.Value.GuildAvatarId; /// public override ushort DiscriminatorValue { - get => GuildUser.DiscriminatorValue; - internal set => GuildUser.DiscriminatorValue = value; + get => GuildUser.Value.DiscriminatorValue; + internal set => GuildUser.Value.DiscriminatorValue = value; } /// public override bool IsBot { - get => GuildUser.IsBot; - internal set => GuildUser.IsBot = value; + get => GuildUser.Value.IsBot; + internal set => GuildUser.Value.IsBot = value; } /// public override bool IsWebhook - => GuildUser.IsWebhook; + => GuildUser.Value.IsWebhook; /// public override string Username { - get => GuildUser.Username; - internal set => GuildUser.Username = value; + get => GuildUser.Value.Username; + internal set => GuildUser.Value.Username = value; } /// public bool IsDeafened - => GuildUser.IsDeafened; + => GuildUser.Value.IsDeafened; /// public bool IsMuted - => GuildUser.IsMuted; + => GuildUser.Value.IsMuted; /// public bool IsSelfDeafened - => GuildUser.IsSelfDeafened; + => GuildUser.Value.IsSelfDeafened; /// public bool IsSelfMuted - => GuildUser.IsSelfMuted; + => GuildUser.Value.IsSelfMuted; /// public bool IsSuppressed - => GuildUser.IsSuppressed; + => GuildUser.Value.IsSuppressed; /// public IVoiceChannel VoiceChannel - => GuildUser.VoiceChannel; + => GuildUser.Value.VoiceChannel; /// public string VoiceSessionId - => GuildUser.VoiceSessionId; + => GuildUser.Value.VoiceSessionId; /// public bool IsStreaming - => GuildUser.IsStreaming; + => GuildUser.Value.IsStreaming; /// public bool IsVideoing - => GuildUser.IsVideoing; + => GuildUser.Value.IsVideoing; /// public DateTimeOffset? RequestToSpeakTimestamp - => GuildUser.RequestToSpeakTimestamp; + => GuildUser.Value.RequestToSpeakTimestamp; + + private Lazy GuildUser { get; set; } - private SocketGuildUser GuildUser { get; set; } + private ulong _threadId; + private ulong _guildId; - internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId) - : base(guild.Discord, userId) + + internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threadId, ulong userId) + : base(client, userId) { - Thread = thread; - Guild = guild; - GuildUser = member; + _guildId = guildId; + _threadId = threadId; + + GuildUser = new(() => client.StateManager.TryGetMemberStore(guildId, out var store) ? store.Get(userId) : null); + Thread = new(() => client.GetChannel(threadId) as SocketThreadChannel); + Guild = new(() => client.GetGuild(guildId)); } internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) { - var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); + var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.Id); + entity.Update(model); + return entity; + } + + internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model) + { + var entity = new SocketThreadUser(client, guildId, threadId, model.Id); entity.Update(model); return entity; } @@ -150,89 +166,116 @@ internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel t internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) { // this is used for creating the owner of the thread. - var entity = new SocketThreadUser(guild, thread, owner, owner.Id); - entity.Update(new Model - { - JoinTimestamp = thread.CreatedAt, - }); + var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, owner.Id); + entity.ThreadJoinedAt = thread.CreatedAt; return entity; } internal void Update(Model model) { - ThreadJoinedAt = model.JoinTimestamp; + ThreadJoinedAt = model.JoinedAt; } /// - public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); + public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel); /// - public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); + public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.Value.KickAsync(reason, options); /// - public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); + public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.Value.ModifyAsync(func, options); /// - public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(roleId, options); /// - public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); + public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(role, options); /// - public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roleIds, options); /// - public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roles, options); /// - public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(roleId, options); /// - public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(role, options); /// - public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roleIds, options); /// - public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roles, options); /// - public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options); + public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.Value.SetTimeOutAsync(span, options); /// - public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); + public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options); /// - IThreadChannel IThreadUser.Thread => Thread; + IThreadChannel IThreadUser.Thread => Thread.Value; /// - IGuild IThreadUser.Guild => Guild; + IGuild IThreadUser.Guild => Guild.Value; /// - IGuild IGuildUser.Guild => Guild; + IGuild IGuildUser.Guild => Guild.Value; /// - ulong IGuildUser.GuildId => Guild.Id; + ulong IGuildUser.GuildId => Guild.Value.Id; /// - GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; + GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions; /// - IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Value.Roles.Select(x => x.Id).ToImmutableArray(); /// - string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size); + string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size); /// - string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); - internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } + internal override LazyCached Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } + + public override void Dispose() + { + if (IsFreed) + return; + + GC.SuppressFinalize(this); + Discord.StateManager.GetThreadMemberStore(_threadId)?.RemoveReference(Id); + IsFreed = true; + } - internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } /// /// Gets the guild user of this thread user. /// /// - public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; + public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value; + + #region Cache + internal new class CacheModel : Model + { + public ulong Id { get; set; } + public ulong? ThreadId { get; set; } + public DateTimeOffset JoinedAt { get; set; } + } + + internal new Model ToModel() + { + var model = Discord.StateManager.GetModel(); + model.Id = Id; + model.ThreadId = _threadId; + model.JoinedAt = ThreadJoinedAt; + return model; + } + + Model ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 99c47696a8..6da6160fd6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -26,22 +26,22 @@ public class SocketUnknownUser : SocketUser /// public override bool IsWebhook => false; /// - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } - /// - /// This field is not supported for an unknown user. - internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + internal override LazyCached Presence { get { return new(SocketPresence.Default); } set { } } + internal override LazyCached GlobalUser { get => new(null); set { } } internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model) { var entity = new SocketUnknownUser(discord, model.Id); - entity.Update(state, model); + entity.Update(model); return entity; } + public override void Dispose() { } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index d70e617392..20c1951039 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -6,8 +6,8 @@ using System.Linq; using System.Threading.Tasks; using Discord.Rest; -using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; +using Model = Discord.IUserModel; +using PresenceModel = Discord.IPresenceModel; namespace Discord.WebSocket { @@ -15,23 +15,23 @@ namespace Discord.WebSocket /// Represents a WebSocket-based user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class SocketUser : SocketEntity, IUser + public abstract class SocketUser : SocketEntity, IUser, ICached { /// - public abstract bool IsBot { get; internal set; } + public virtual bool IsBot { get; internal set; } /// - public abstract string Username { get; internal set; } + public virtual string Username { get; internal set; } /// - public abstract ushort DiscriminatorValue { get; internal set; } + public virtual ushort DiscriminatorValue { get; internal set; } /// - public abstract string AvatarId { get; internal set; } + public virtual string AvatarId { get; internal set; } /// - public abstract bool IsWebhook { get; } + public virtual bool IsWebhook { get; } /// public UserProperties? PublicFlags { get; private set; } - internal abstract SocketGlobalUser GlobalUser { get; set; } - internal abstract SocketPresence Presence { get; set; } - + internal virtual LazyCached GlobalUser { get; set; } + internal virtual LazyCached Presence { get; set; } + internal bool IsFreed { get; set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// @@ -39,11 +39,11 @@ public abstract class SocketUser : SocketEntity, IUser /// public string Mention => MentionUtils.MentionUser(Id); /// - public UserStatus Status => Presence.Status; + public UserStatus Status => Presence.Value.Status; /// - public IReadOnlyCollection ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; + public IReadOnlyCollection ActiveClients => Presence.Value?.ActiveClients ?? ImmutableHashSet.Empty; /// - public IReadOnlyCollection Activities => Presence.Activities ?? ImmutableList.Empty; + public IReadOnlyCollection Activities => Presence.Value?.Activities ?? ImmutableList.Empty; /// /// Gets mutual guilds shared with this user. /// @@ -56,48 +56,50 @@ public IReadOnlyCollection MutualGuilds internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) { + Presence = new LazyCached(id, discord.StateManager.PresenceStore); + GlobalUser = new LazyCached(id, discord.StateManager.UserStore); } - internal virtual bool Update(ClientState state, Model model) + internal virtual bool Update(Model model) { - Presence ??= new SocketPresence(); bool hasChanges = false; - if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) + if (model.Avatar != AvatarId) { - AvatarId = model.Avatar.Value; + AvatarId = model.Avatar; hasChanges = true; } - if (model.Discriminator.IsSpecified) + if (model.Discriminator != null) { - var newVal = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); + var newVal = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); if (newVal != DiscriminatorValue) { - DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); + DiscriminatorValue = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); hasChanges = true; } } - if (model.Bot.IsSpecified && model.Bot.Value != IsBot) + if (model.IsBot.HasValue && model.IsBot.Value != IsBot) { - IsBot = model.Bot.Value; + IsBot = model.IsBot.Value; hasChanges = true; } - if (model.Username.IsSpecified && model.Username.Value != Username) + if (model.Username != Username) { - Username = model.Username.Value; + Username = model.Username; hasChanges = true; } - if (model.PublicFlags.IsSpecified && model.PublicFlags.Value != PublicFlags) + + if(model is ICurrentUserModel currentUserModel) { - PublicFlags = model.PublicFlags.Value; - hasChanges = true; + if (currentUserModel.PublicFlags != PublicFlags) + { + PublicFlags = currentUserModel.PublicFlags; + hasChanges = true; + } } + return hasChanges; } - internal virtual void Update(PresenceModel model) - { - Presence ??= new SocketPresence(); - Presence.Update(model); - } + public abstract void Dispose(); /// public async Task CreateDMChannelAsync(RequestOptions options = null) @@ -120,5 +122,37 @@ public string GetDefaultAvatarUrl() public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; + + #region Cache + internal class CacheModel : Model + { + public string Username { get; set; } + + public string Discriminator { get; set; } + + public bool? IsBot { get; set; } + + public string Avatar { get; set; } + + public ulong Id { get; set; } + } + + internal Model ToModel() + { + var model = Discord.StateManager.GetModel(); + model.Avatar = AvatarId; + model.Discriminator = Discriminator; + model.Id = Id; + model.IsBot = IsBot; + model.Username = Username; + return model; + } + + Model ICached.ToModel() + => ToModel(); + void ICached.Update(Model model) => Update(model); + bool ICached.IsFreed => IsFreed; + + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 2b2c259c52..d1867ddbf6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -33,8 +33,8 @@ public class SocketWebhookUser : SocketUser, IWebhookUser /// public override bool IsWebhook => true; /// - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } - internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + internal override LazyCached Presence { get { return new(SocketPresence.Default); } set { } } + internal override LazyCached GlobalUser { get => new(null); set { } } internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) : base(guild.Discord, id) @@ -42,16 +42,17 @@ internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) Guild = guild; WebhookId = webhookId; } - internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) + internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId) { var entity = new SocketWebhookUser(guild, model.Id, webhookId); - entity.Update(state, model); + entity.Update(model); return entity; } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; -#endregion + public override void Dispose() { } + #endregion #region IGuildUser /// diff --git a/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs new file mode 100644 index 0000000000..5368426af0 --- /dev/null +++ b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Discord.WebSocket +{ + internal static class CacheModelExtensions + { + 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 source as TDest; + + List sharedInterfaceModels = new(); + + foreach (var intf in sourceType.GetInterfaces()) + { + if (destType.GetInterface(intf.Name) != null && intf.Name.Contains("Model")) + sharedInterfaceModels.Add(intf); + } + + if (sharedInterfaceModels.Count == 0) + throw new NotSupportedException($"cannot find common shared model interface between {sourceType.Name} and {destType.Name}"); + + foreach (var interfaceType in sharedInterfaceModels) + { + var intfName = interfaceType.GenericTypeArguments.Length == 0 ? interfaceType.FullName : + $"{interfaceType.Namespace}.{Regex.Replace(interfaceType.Name, @"`\d+?$", "")}<{string.Join(", ", interfaceType.GenericTypeArguments.Select(x => x.FullName))}>"; + + foreach (var prop in interfaceType.GetProperties()) + { + var sProp = sourceType.GetProperty($"{intfName}.{prop.Name}", BindingFlags.NonPublic | BindingFlags.Instance) ?? sourceType.GetProperty(prop.Name); + var dProp = destType.GetProperty($"{intfName}.{prop.Name}", BindingFlags.NonPublic | BindingFlags.Instance) ?? destType.GetProperty(prop.Name); + + if (sProp == null || dProp == null) + throw new NotSupportedException($"Couldn't find common interface property {prop.Name}"); + + dProp.SetValue(dest, sProp.GetValue(source)); + } + } + + 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 0000000000..58fe881431 --- /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 46f5c1a268..e3927e6fcf 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -7,86 +7,108 @@ namespace Discord.WebSocket { internal static class EntityExtensions { - public static IActivity ToEntity(this API.Game model) + #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 - if (model.Id.IsSpecified && model.Id.Value == "custom") + if (model.Id != null && model.Id == "custom") { return new CustomStatusGame() { Type = ActivityType.CustomStatus, Name = model.Name, - State = model.State.IsSpecified ? model.State.Value : null, - Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, - CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), + State = model.State, + Emote = model.Emoji?.ToIEmote(), + CreatedAt = model.CreatedAt, }; } #endregion #region Spotify Game - if (model.SyncId.IsSpecified) + if (model.SyncId != null) { - var assets = model.Assets.GetValueOrDefault()?.ToEntity(); - string albumText = assets?[1]?.Text; - string albumArtId = assets?[1]?.ImageId?.Replace("spotify:", ""); - var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; + string albumText = model.LargeText; + string albumArtId = model.LargeImage?.Replace("spotify:", ""); return new SpotifyGame { Name = model.Name, - SessionId = model.SessionId.GetValueOrDefault(), - TrackId = model.SyncId.Value, - TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value), + SessionId = model.SessionId, + TrackId = model.SyncId, + TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId), AlbumTitle = albumText, - TrackTitle = model.Details.GetValueOrDefault(), - Artists = model.State.GetValueOrDefault()?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), - StartedAt = timestamps?.Start, - EndsAt = timestamps?.End, - Duration = timestamps?.End - timestamps?.Start, + TrackTitle = model.Details, + Artists = model.State?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), + StartedAt = model.TimestampStart, + EndsAt = model.TimestampEnd, + Duration = model.TimestampEnd - model.TimestampStart, AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, Type = ActivityType.Listening, - Flags = model.Flags.GetValueOrDefault(), + Flags = model.Flags, + AlbumArt = model.LargeImage, }; } #endregion #region Rich Game - if (model.ApplicationId.IsSpecified) + if (model.ApplicationId.HasValue) { ulong appId = model.ApplicationId.Value; - var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); return new RichGame { ApplicationId = appId, Name = model.Name, - Details = model.Details.GetValueOrDefault(), - State = model.State.GetValueOrDefault(), - SmallAsset = assets?[0], - LargeAsset = assets?[1], - Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, - Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, - Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null, - Flags = model.Flags.GetValueOrDefault() + Details = model.Details, + State = model.State, + SmallAsset = new GameAsset + { + Text = model.SmallText, + ImageId = model.SmallImage, + ApplicationId = appId, + }, + LargeAsset = new GameAsset + { + Text = model.LargeText, + ApplicationId = appId, + ImageId = model.LargeImage + }, + Party = model.PartyId != null ? new GameParty + { + Id = model.PartyId, + Capacity = model.PartySize?.Length > 1 ? model.PartySize[1] : 0, + Members = model.PartySize?.Length > 0 ? model.PartySize[0] : 0 + } : null, + Secrets = model.JoinSecret != null || model.SpectateSecret != null || model.MatchSecret != null ? new GameSecrets(model.MatchSecret, model.JoinSecret, model.SpectateSecret) : null, + Timestamps = model.TimestampStart.HasValue || model.TimestampEnd.HasValue ? new GameTimestamps(model.TimestampStart, model.TimestampEnd) : null, + Flags = model.Flags }; } #endregion #region Stream Game - if (model.StreamUrl.IsSpecified) + if (model.Url != null) { return new StreamingGame( model.Name, - model.StreamUrl.Value) + model.Url) { - Flags = model.Flags.GetValueOrDefault(), - Details = model.Details.GetValueOrDefault() + Flags = model.Flags, + Details = model.Details }; } #endregion #region Normal Game - return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing, - model.Flags.IsSpecified ? model.Flags.Value : ActivityProperties.None, - model.Details.GetValueOrDefault()); + return new Game(model.Name, model.Type, model.Flags, model.Details); #endregion } @@ -136,5 +158,6 @@ public static GameTimestamps ToEntity(this API.GameTimestamps model) { return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } + #endregion } } diff --git a/src/Discord.Net.WebSocket/Interactions/ShardedInteractionContext.cs b/src/Discord.Net.WebSocket/Interactions/ShardedInteractionContext.cs index ac05241729..1953498617 100644 --- a/src/Discord.Net.WebSocket/Interactions/ShardedInteractionContext.cs +++ b/src/Discord.Net.WebSocket/Interactions/ShardedInteractionContext.cs @@ -19,13 +19,13 @@ public class ShardedInteractionContext : SocketInteractionContext< /// The underlying client. /// The underlying interaction. public ShardedInteractionContext (DiscordShardedClient client, TInteraction interaction) - : base(client.GetShard(GetShardId(client, ( interaction.User as SocketGuildUser )?.Guild)), interaction) + : base(client.GetShard(GetShardId(client, (interaction.User as SocketGuildUser )?.GuildId)), interaction) { Client = client; } - private static int GetShardId (DiscordShardedClient client, IGuild guild) - => guild == null ? 0 : client.GetShardIdFor(guild); + private static int GetShardId(DiscordShardedClient client, ulong? guildId) + => guildId.HasValue ? client.GetShardIdFor(guildId.Value) : 0; } /// diff --git a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs index a2a1018398..557c47a69c 100644 --- a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs +++ b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs @@ -50,7 +50,7 @@ public SocketInteractionContext(DiscordSocketClient client, TInteraction interac { Client = client; Channel = interaction.Channel; - Guild = (interaction.User as SocketGuildUser)?.Guild; + Guild = (interaction.User as SocketGuildUser)?.Guild.Value; User = interaction.User; Interaction = interaction; }