From 3e36fbb854baf990968f67ed23e3ff53f5e47089 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Sat, 16 Apr 2022 06:40:31 -0300 Subject: [PATCH 01/11] initial implementation Cachable models so far - Users - Presence --- .../Cache/CacheableEntityExtensions.cs | 107 +++++++ src/Discord.Net.Core/Cache/ICached.cs | 13 + .../Cache/Models/Emoji/IEmojiModel.cs | 34 ++ .../Cache/Models/Presense/IActivityModel.cs | 88 ++++++ .../Cache/Models/Presense/IPresenceModel.cs | 17 + .../Cache/Models/Users/ICurrentUserModel.cs | 19 ++ .../Cache/Models/Users/IMemberModel.cs | 23 ++ .../Cache/Models/Users/IUserModel.cs | 16 + .../Entities/Activities/SpotifyGame.cs | 2 + .../Entities/Emotes/GuildEmote.cs | 10 +- .../API/Common/CurrentUser.cs | 42 +++ src/Discord.Net.Rest/API/Common/Emoji.cs | 20 +- src/Discord.Net.Rest/API/Common/Game.cs | 53 +++- .../API/Common/GuildMember.cs | 23 +- src/Discord.Net.Rest/API/Common/Presence.cs | 15 +- src/Discord.Net.Rest/API/Common/User.cs | 28 +- src/Discord.Net.Rest/ClientHelper.cs | 10 + src/Discord.Net.Rest/DiscordRestApiClient.cs | 4 +- src/Discord.Net.Rest/DiscordRestClient.cs | 2 + .../Entities/Users/RestGuildUser.cs | 1 + .../Entities/Users/RestSelfUser.cs | 28 +- .../Entities/Users/RestUser.cs | 22 +- .../Extensions/EntityExtensions.cs | 61 ++-- .../API/Gateway/ReadyEvent.cs | 2 +- .../Cache/CacheRunMode.cs | 21 ++ .../Cache/DefaultConcurrentCacheProvider.cs | 82 +++++ .../Cache/ICacheProvider.cs | 37 +++ .../ClientStateManager.Experiment.cs | 160 ++++++++++ .../{ClientState.cs => ClientStateManager.cs} | 23 +- .../DiscordShardedClient.cs | 2 +- .../DiscordSocketClient.cs | 298 +++++++++--------- .../DiscordSocketConfig.cs | 11 + .../Channels/SocketCategoryChannel.cs | 2 +- .../Entities/Channels/SocketChannel.cs | 4 +- .../Entities/Channels/SocketDMChannel.cs | 8 +- .../Entities/Channels/SocketGroupChannel.cs | 10 +- .../Entities/Channels/SocketGuildChannel.cs | 4 +- .../Entities/Channels/SocketNewsChannel.cs | 2 +- .../Entities/Channels/SocketStageChannel.cs | 2 +- .../Entities/Channels/SocketTextChannel.cs | 6 +- .../Entities/Channels/SocketThreadChannel.cs | 4 +- .../Entities/Channels/SocketVoiceChannel.cs | 4 +- .../Entities/Guilds/SocketGuild.cs | 141 ++++----- .../Entities/Guilds/SocketGuildEvent.cs | 4 +- .../SocketMessageComponent.cs | 6 +- .../SocketBaseCommand/SocketResolvableData.cs | 14 +- .../Entities/Messages/SocketMessage.cs | 4 +- .../Entities/Messages/SocketSystemMessage.cs | 4 +- .../Entities/Messages/SocketUserMessage.cs | 4 +- .../Entities/Roles/SocketRole.cs | 4 +- .../Entities/Users/SocketGlobalUser.cs | 11 +- .../Entities/Users/SocketGroupUser.cs | 4 +- .../Entities/Users/SocketGuildUser.cs | 168 +++++----- .../Entities/Users/SocketPresence.cs | 71 ++++- .../Entities/Users/SocketSelfUser.cs | 89 +++++- .../Entities/Users/SocketThreadUser.cs | 2 +- .../Entities/Users/SocketUnknownUser.cs | 4 +- .../Entities/Users/SocketUser.cs | 84 +++-- .../Entities/Users/SocketWebhookUser.cs | 4 +- .../Extensions/EntityExtensions.cs | 83 ++--- .../Extensions/StateExtensions.cs | 21 ++ .../Interactions/ShardedInteractionContext.cs | 6 +- .../Interactions/SocketInteractionContext.cs | 2 +- .../State/DefaultStateProvider.cs | 256 +++++++++++++++ .../State/IStateProvider.cs | 25 ++ .../State/StateBehavior.cs | 53 ++++ 66 files changed, 1818 insertions(+), 566 deletions(-) create mode 100644 src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs create mode 100644 src/Discord.Net.Core/Cache/ICached.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs create mode 100644 src/Discord.Net.Rest/API/Common/CurrentUser.cs create mode 100644 src/Discord.Net.WebSocket/Cache/CacheRunMode.cs create mode 100644 src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs create mode 100644 src/Discord.Net.WebSocket/Cache/ICacheProvider.cs create mode 100644 src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs rename src/Discord.Net.WebSocket/{ClientState.cs => ClientStateManager.cs} (91%) create mode 100644 src/Discord.Net.WebSocket/Extensions/StateExtensions.cs create mode 100644 src/Discord.Net.WebSocket/State/DefaultStateProvider.cs create mode 100644 src/Discord.Net.WebSocket/State/IStateProvider.cs create mode 100644 src/Discord.Net.WebSocket/State/StateBehavior.cs diff --git a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs new file mode 100644 index 0000000000..fb265f94af --- /dev/null +++ b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs @@ -0,0 +1,107 @@ +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 : WritableActivityModel, 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 : WritableActivityModel, 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 : WritableActivityModel, new() + where TEmoteModel : WritableEmojiModel, new() + { + return new TModel + { + 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 : WritableActivityModel, new() + { + return new TModel + { + Name = stream.Name, + Url = stream.Url, + Flags = stream.Flags, + Details = stream.Details + }; + } + + public static IEmojiModel ToModel(this IEmote emote) where TModel : WritableEmojiModel, new() + { + var model = new TModel() + { + 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; + } + } +} diff --git a/src/Discord.Net.Core/Cache/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs new file mode 100644 index 0000000000..3146741bbc --- /dev/null +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal interface ICached + { + TType ToModel(); + } +} 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..bc5b43e2a6 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IEmojiModel + { + ulong? Id { get; } + string Name { get; } + ulong[] Roles { get; } + bool RequireColons { get; } + bool IsManaged { get; } + bool IsAnimated { get; } + bool IsAvailable { get; } + + ulong? CreatorId { get; } + } + + internal class WritableEmojiModel : 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; } + } +} 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..a66e887548 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IActivityModel + { + string Id { get; } + string Url { get; } + string Name { get; } + ActivityType Type { get; } + string Details { get; } + string State { get; } + ActivityProperties Flags { get; } + DateTimeOffset CreatedAt { get; } + IEmojiModel Emoji { get; } + ulong? ApplicationId { get; } + string SyncId { get; } + string SessionId { get; } + + + #region Assets + string LargeImage { get; } + string LargeText { get; } + string SmallImage { get; } + string SmallText { get; } + #endregion + + #region Party + string PartyId { get; } + long[] PartySize { get; } + #endregion + + #region Secrets + string JoinSecret { get; } + string SpectateSecret { get; } + string MatchSecret { get; } + #endregion + + #region Timestamps + DateTimeOffset? TimestampStart { get; } + DateTimeOffset? TimestampEnd { get; } + #endregion + } + + internal class WritableActivityModel : 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; } + + + #region Assets + public string LargeImage { get; set; } + public string LargeText { get; set; } + public string SmallImage { get; set; } + public string SmallText { get; set; } + #endregion + + #region Party + public string PartyId { get; set; } + public long[] PartySize { get; set; } + #endregion + + #region Secrets + public string JoinSecret { get; set; } + public string SpectateSecret { get; set; } + public string MatchSecret { get; set; } + #endregion + + #region Timestamps + public DateTimeOffset? TimestampStart { get; set; } + public 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..c58a1fad50 --- /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 + { + ulong UserId { get; } + ulong? GuildId { get; } + UserStatus Status { get; } + ClientType[] ActiveClients { get; } + IActivityModel[] Activities { get; } + } +} 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..80b832cf88 --- /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; } + string Email { get; } + bool? IsMfaEnabled { get; } + UserProperties Flags { get; } + PremiumType PremiumType { get; } + string Locale { get; } + UserProperties PublicFlags { get; } + } +} 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..3c3a67d5e6 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IMemberModel + { + IUserModel User { get; } + + string Nickname { get; } + string GuildAvatar { get; } + ulong[] Roles { get; } + DateTimeOffset JoinedAt { get; } + DateTimeOffset? PremiumSince { get; } + bool IsDeaf { get; } + bool IsMute { get; } + bool? IsPending { get; } + DateTimeOffset? CommunicationsDisabledUntil { get; } + } +} 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..24d05d0fcb --- /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 : IEntity + { + string Username { get; } + string Discriminator { get; } + bool? IsBot { get; } + string Avatar { get; } + } +} 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.Rest/API/Common/CurrentUser.cs b/src/Discord.Net.Rest/API/Common/CurrentUser.cs new file mode 100644 index 0000000000..4a19056b2b --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/CurrentUser.cs @@ -0,0 +1,42 @@ +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 => Verified.ToNullable(); + + string ICurrentUserModel.Email => Email.GetValueOrDefault(); + + bool? ICurrentUserModel.IsMfaEnabled => MfaEnabled.ToNullable(); + + UserProperties ICurrentUserModel.Flags => Flags.GetValueOrDefault(); + + PremiumType ICurrentUserModel.PremiumType => PremiumType.GetValueOrDefault(); + + string ICurrentUserModel.Locale => Locale.GetValueOrDefault(); + + UserProperties ICurrentUserModel.PublicFlags => PublicFlags.GetValueOrDefault(); + } +} diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index ff0baa73e9..af1a255321 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class Emoji + internal class Emoji : IEmojiModel { [JsonProperty("id")] public ulong? Id { get; set; } @@ -16,7 +16,25 @@ 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 => Id; + + string IEmojiModel.Name => Name; + + ulong[] IEmojiModel.Roles => Roles; + + bool IEmojiModel.RequireColons => RequireColons; + + bool IEmojiModel.IsManaged => Managed; + + bool IEmojiModel.IsAnimated => Animated.GetValueOrDefault(); + + bool IEmojiModel.IsAvailable => Available.GetValueOrDefault(); + + ulong? IEmojiModel.CreatorId => User.GetValueOrDefault()?.Id; } } diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 105ce0d736..32e0d5c513 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,54 @@ internal class Game public Optional Emoji { get; set; } [JsonProperty("created_at")] public Optional CreatedAt { get; set; } + + string IActivityModel.Id => Id.GetValueOrDefault(); + + string IActivityModel.Url => StreamUrl.GetValueOrDefault(); + + string IActivityModel.State => State.GetValueOrDefault(); + + IEmojiModel IActivityModel.Emoji => Emoji.GetValueOrDefault(); + + string IActivityModel.Name => Name; + + ActivityType IActivityModel.Type => Type.GetValueOrDefault().GetValueOrDefault(); + + ActivityProperties IActivityModel.Flags => Flags.GetValueOrDefault(); + + string IActivityModel.Details => Details.GetValueOrDefault(); + DateTimeOffset IActivityModel.CreatedAt => DateTimeOffset.FromUnixTimeMilliseconds(CreatedAt.GetValueOrDefault()); + + ulong? IActivityModel.ApplicationId => ApplicationId.ToNullable(); + + string IActivityModel.SyncId => SyncId.GetValueOrDefault(); + + string IActivityModel.SessionId => SessionId.GetValueOrDefault(); + + string IActivityModel.LargeImage => Assets.GetValueOrDefault()?.LargeImage.GetValueOrDefault(); + + string IActivityModel.LargeText => Assets.GetValueOrDefault()?.LargeText.GetValueOrDefault(); + + string IActivityModel.SmallImage => Assets.GetValueOrDefault()?.SmallImage.GetValueOrDefault(); + + string IActivityModel.SmallText => Assets.GetValueOrDefault()?.SmallText.GetValueOrDefault(); + + string IActivityModel.PartyId => Party.GetValueOrDefault()?.Id; + + long[] IActivityModel.PartySize => Party.GetValueOrDefault()?.Size; + + string IActivityModel.JoinSecret => Secrets.GetValueOrDefault()?.Join; + + string IActivityModel.SpectateSecret => Secrets.GetValueOrDefault()?.Spectate; + + string IActivityModel.MatchSecret => Secrets.GetValueOrDefault()?.Match; + + DateTimeOffset? IActivityModel.TimestampStart => Timestamps.GetValueOrDefault()?.Start.ToNullable(); + + DateTimeOffset? IActivityModel.TimestampEnd => Timestamps.GetValueOrDefault()?.End.ToNullable(); + + + //[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..cfe4e652e8 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,26 @@ internal class GuildMember public Optional PremiumSince { get; set; } [JsonProperty("communication_disabled_until")] public Optional TimedOutUntil { get; set; } + + // IMemberModel + string IMemberModel.Nickname => Nick.GetValueOrDefault(); + + string IMemberModel.GuildAvatar => Avatar.GetValueOrDefault(); + + ulong[] IMemberModel.Roles => Roles.GetValueOrDefault(Array.Empty()); + + DateTimeOffset IMemberModel.JoinedAt => JoinedAt.GetValueOrDefault(); + + DateTimeOffset? IMemberModel.PremiumSince => PremiumSince.GetValueOrDefault(); + + bool IMemberModel.IsDeaf => Deaf.GetValueOrDefault(false); + + bool IMemberModel.IsMute => Mute.GetValueOrDefault(false); + + bool? IMemberModel.IsPending => Pending.ToNullable(); + + DateTimeOffset? IMemberModel.CommunicationsDisabledUntil => TimedOutUntil.GetValueOrDefault(); + + IUserModel IMemberModel.User => User; } } diff --git a/src/Discord.Net.Rest/API/Common/Presence.cs b/src/Discord.Net.Rest/API/Common/Presence.cs index 23f871ae60..1734602424 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,17 @@ internal class Presence public List Activities { get; set; } [JsonProperty("premium_since")] public Optional PremiumSince { get; set; } + + ulong IPresenceModel.UserId => User.Id; + + ulong? IPresenceModel.GuildId => GuildId.ToNullable(); + + UserStatus IPresenceModel.Status => Status; + + ClientType[] IPresenceModel.ActiveClients => ClientStatus.IsSpecified + ? ClientStatus.Value.Select(x => (ClientType)Enum.Parse(typeof(ClientType), x.Key, true)).ToArray() + : Array.Empty(); + + IActivityModel[] IPresenceModel.Activities => Activities.ToArray(); } } diff --git a/src/Discord.Net.Rest/API/Common/User.cs b/src/Discord.Net.Rest/API/Common/User.cs index 08fe88cb05..26e5deafdc 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal class User + internal class User : IUserModel { [JsonProperty("id")] public ulong Id { get; set; } @@ -19,20 +19,16 @@ 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 => Username.GetValueOrDefault(); + + string IUserModel.Discriminator => Discriminator.GetValueOrDefault(); + + bool? IUserModel.IsBot => Bot.ToNullable(); + + string IUserModel.Avatar => Avatar.GetValueOrDefault(); + + ulong IEntity.Id => Id; } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index c6ad6a9fb5..ab0238fee0 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -151,6 +151,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..6ae8bc0b96 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -185,6 +185,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/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..5de5b7b5d7 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -6,6 +6,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,6 +35,7 @@ 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); @@ -170,48 +188,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/CacheRunMode.cs b/src/Discord.Net.WebSocket/Cache/CacheRunMode.cs new file mode 100644 index 0000000000..f53719e06c --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/CacheRunMode.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public enum CacheRunMode + { + /// + /// The cache should preform a synchronous cache lookup. + /// + Sync, + + /// + /// The cache should preform either a or asynchronous cache lookup. + /// + Async + } +} diff --git a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs new file mode 100644 index 0000000000..f0131759dc --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs @@ -0,0 +1,82 @@ +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 _users; + private readonly ConcurrentDictionary> _members; + private readonly ConcurrentDictionary _presense; + + private ValueTask CompletedValueTask => new ValueTask(Task.CompletedTask).Preserve(); + + public DefaultConcurrentCacheProvider(int defaultConcurrency, int defaultCapacity) + { + _users = new(defaultConcurrency, defaultCapacity); + _members = new(defaultConcurrency, defaultCapacity); + _presense = new(defaultConcurrency, defaultCapacity); + } + + public ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode mode) + { + _users.AddOrUpdate(model.Id, model, (_, __) => model); + return CompletedValueTask; + } + public ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode mode) + { + var guildMemberCache = _members.GetOrAdd(guildId, (_) => new ConcurrentDictionary()); + guildMemberCache.AddOrUpdate(model.User.Id, model, (_, __) => model); + return CompletedValueTask; + } + public ValueTask GetMemberAsync(ulong id, ulong guildId, CacheRunMode mode) + => new ValueTask(_members.FirstOrDefault(x => x.Key == guildId).Value?.FirstOrDefault(x => x.Key == id).Value); + + public ValueTask> GetMembersAsync(ulong guildId, CacheRunMode mode) + { + if(_members.TryGetValue(guildId, out var inner)) + return new ValueTask>(inner.ToArray().Select(x => x.Value)); // ToArray here is important before .Select due to concurrency + return new ValueTask>(Array.Empty()); + } + public ValueTask GetUserAsync(ulong id, CacheRunMode mode) + { + if (_users.TryGetValue(id, out var result)) + return new ValueTask(result); + return new ValueTask((IUserModel)null); + } + public ValueTask> GetUsersAsync(CacheRunMode mode) + => new ValueTask>(_users.ToArray().Select(x => x.Value)); + public ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode mode) + { + if (_members.TryGetValue(guildId, out var inner)) + inner.TryRemove(id, out var _); + return CompletedValueTask; + } + public ValueTask RemoveUserAsync(ulong id, CacheRunMode mode) + { + _members.TryRemove(id, out var _); + return CompletedValueTask; + } + + public ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode) + { + if (_presense.TryGetValue(userId, out var presense)) + return new ValueTask(presense); + return new ValueTask((IPresenceModel)null); + } + public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode) + { + _presense.AddOrUpdate(userId, presense, (_, __) => presense); + return CompletedValueTask; + } + public ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode) + { + _presense.TryRemove(userId, out var _); + return CompletedValueTask; + } + } +} diff --git a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs new file mode 100644 index 0000000000..2655801930 --- /dev/null +++ b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface ICacheProvider + { + #region Users + + ValueTask GetUserAsync(ulong id, CacheRunMode runmode); + ValueTask> GetUsersAsync(CacheRunMode runmode); + ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode runmode); + ValueTask RemoveUserAsync(ulong id, CacheRunMode runmode); + + #endregion + + #region Members + + ValueTask GetMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); + ValueTask> GetMembersAsync(ulong guildId, CacheRunMode runmode); + ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode runmode); + ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); + + #endregion + + #region Presence + + ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode); + ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode); + ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode); + + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs new file mode 100644 index 0000000000..cc59c57aa8 --- /dev/null +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class CacheWeakReference : WeakReference + { + public new T Target { get => (T)base.Target; set => base.Target = value; } + public CacheWeakReference(T target) + : base(target, false) + { + + } + + public bool TryGetTarget(out T target) + { + target = Target; + return IsAlive; + } + } + + internal partial class ClientStateManager + { + private readonly ConcurrentDictionary> _userReferences = new(); + private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), CacheWeakReference> _memberReferences = new(); + + + #region Helpers + + private void EnsureSync(ValueTask vt) + { + if (!vt.IsCompleted) + throw new NotSupportedException($"Cannot use async context for value task lookup"); + } + + #endregion + + #region Global users + internal void RemoveReferencedGlobalUser(ulong id) + => _userReferences.TryRemove(id, out _); + + private void TrackGlobalUser(ulong id, SocketGlobalUser user) + { + if (user != null) + { + _userReferences.TryAdd(id, new CacheWeakReference(user)); + } + } + + internal ValueTask GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + => _state.GetUserAsync(id, mode.ToBehavior(), options); + + internal SocketGlobalUser GetUser(ulong id) + { + if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) + return user; + + user = (SocketGlobalUser)_state.GetUserAsync(id, StateBehavior.SyncOnly).Result; + + if(user != null) + TrackGlobalUser(id, user); + + return user; + } + + internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) + { + if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) + return user; + + user = GetUser(id); + + if (user == null) + { + user ??= userFactory(id); + _state.AddOrUpdateUserAsync(user); + TrackGlobalUser(id, user); + } + + return user; + } + + internal void RemoveUser(ulong id) + { + _state.RemoveUserAsync(id); + } + #endregion + + #region GuildUsers + private void TrackMember(ulong userId, ulong guildId, SocketGuildUser user) + { + if(user != null) + { + _memberReferences.TryAdd((guildId, userId), new CacheWeakReference(user)); + } + } + internal void RemovedReferencedMember(ulong userId, ulong guildId) + => _memberReferences.TryRemove((guildId, userId), out _); + + internal ValueTask GetMemberAsync(ulong userId, ulong guildId, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + => _state.GetMemberAsync(guildId, userId, mode.ToBehavior(), options); + + internal SocketGuildUser GetMember(ulong userId, ulong guildId) + { + if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) + return member; + member = (SocketGuildUser)_state.GetMemberAsync(guildId, userId, StateBehavior.SyncOnly).Result; + if(member != null) + TrackMember(userId, guildId, member); + return member; + } + + internal SocketGuildUser GetOrAddMember(ulong userId, ulong guildId, Func memberFactory) + { + if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) + return member; + + member = GetMember(userId, guildId); + + if (member == null) + { + member ??= memberFactory(userId, guildId); + TrackMember(userId, guildId, member); + Task.Run(async () => await _state.AddOrUpdateMemberAsync(guildId, member)); // can run async, think of this as fire and forget. + } + + return member; + } + + internal IEnumerable GetMembers(ulong guildId) + => _state.GetMembersAsync(guildId, StateBehavior.SyncOnly).Result; + + internal void AddOrUpdateMember(ulong guildId, SocketGuildUser user) + => EnsureSync(_state.AddOrUpdateMemberAsync(guildId, user)); + + internal void RemoveMember(ulong userId, ulong guildId) + => EnsureSync(_state.RemoveMemberAsync(guildId, userId)); + + #endregion + + #region Presence + internal void AddOrUpdatePresence(SocketPresence presence) + { + EnsureSync(_state.AddOrUpdatePresenseAsync(presence.UserId, presence, StateBehavior.SyncOnly)); + } + + internal SocketPresence GetPresence(ulong userId) + { + if (_state.GetPresenceAsync(userId, StateBehavior.SyncOnly).Result is not SocketPresence socketPresence) + throw new NotSupportedException("Cannot use non-socket entity for presence"); + + return socketPresence; + } + #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..1416e9cf92 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,11 @@ internal class ClientState _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); - public ClientState(int guildCount, int dmChannelCount) + private readonly IStateProvider _state; + + public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCount) { + _state = state; double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; _channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); @@ -121,22 +124,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 aaef4656a7..355dec0061 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -70,9 +70,10 @@ 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 ClientStateManager StateManager { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } + internal IStateProvider StateProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } internal bool AlwaysDownloadDefaultStickers { get; private set; } @@ -81,7 +82,7 @@ public partial class DiscordSocketClient : BaseSocketClient, IDiscordClient internal bool SuppressUnknownDispatchWarnings { 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 +95,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 +110,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 +125,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 +142,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 +155,6 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; - State = new ClientState(0, 0); Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _gatewayIntents = config.GatewayIntents; @@ -165,6 +166,7 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); + StateProvider = config.StateProvider ?? new DefaultStateProvider(_gatewayLogger, config.CacheProvider ?? new DefaultConcurrentCacheProvider(5, 50), this, config.DefaultStateBehavior); _nextAudioId = 1; _shardedClient = shardedClient; @@ -200,6 +202,17 @@ 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.GetUserAsync(id, cacheMode, options); + + public ValueTask GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) + => StateManager.GetMemberAsync(userId, guildId, cacheMode, options); + + #endregion + /// internal override void Dispose(bool disposing) { @@ -217,7 +230,6 @@ internal override void Dispose(bool disposing) base.Dispose(disposing); } - internal override async ValueTask DisposeAsync(bool disposing) { if (!_isDisposed) @@ -348,7 +360,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 +373,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 +399,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 +409,10 @@ public async ValueTask GetUserAsync(ulong id, RequestOptions options = nu /// public override SocketUser GetUser(ulong id) - => State.GetUser(id); + => StateManager.GetUser(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 +425,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 +437,7 @@ public async ValueTask GetGlobalApplicationCommandAsyn command = SocketApplicationCommand.Create(this, model); - State.AddCommand(command); + StateManager.AddCommand(command); return command; } @@ -461,7 +455,7 @@ public async Task> GetGlobalApplic foreach(var command in commands) { - State.AddCommand(command); + StateManager.AddCommand(command); } return commands.ToImmutableArray(); @@ -471,7 +465,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 +480,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 +493,26 @@ 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)); } - 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); } - 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; }); } internal void RemoveUser(ulong id) - => State.RemoveUser(id); + => StateManager.RemoveUser(id); /// public override async Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) @@ -548,7 +541,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 +689,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); + StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -866,12 +859,13 @@ 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 state = new ClientStateManager(StateProvider, data.Guilds.Length, data.PrivateChannels.Length); + StateManager = state; var currentUser = SocketSelfUser.Create(this, state, data.User); Rest.CreateRestSelfUser(data.User); var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - currentUser.Presence = new SocketPresence(Status, null, activities); + StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentApplicationId = data.Application.Id; Rest.CurrentUser = RestSelfUser.Create(this, data.User); @@ -892,7 +886,6 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _unavailableGuildCount = unavailableGuilds; CurrentUser = currentUser; _previousSessionUser = CurrentUser; - State = state; } catch (Exception ex) { @@ -928,7 +921,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 +946,10 @@ 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); if (_unavailableGuildCount != 0) _unavailableGuildCount--; @@ -978,7 +971,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 = AddGuild(data, StateManager); if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); @@ -997,11 +990,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 +1009,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 +1054,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 +1091,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 +1149,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 +1168,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 +1183,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 +1213,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 +1250,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 +1276,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 +1290,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.Update(StateManager, data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); } - user.Update(State, data); + user.Update(StateManager, data); var cacheableBefore = new Cacheable(before, user.Id, true, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); @@ -1327,7 +1320,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 +1332,12 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - user ??= State.GetUser(data.User.Id); + user ??= StateManager.GetUser(data.User.Id); if (user != null) - user.Update(State, data.User); + user.Update(StateManager, data.User); else - user = State.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, State, data.User)); + user = StateManager.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, StateManager, data.User)); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } @@ -1360,7 +1353,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 +1378,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 +1403,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 +1420,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 +1447,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 +1471,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 +1506,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 +1541,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 +1552,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, StateManager, data.User); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else @@ -1574,7 +1567,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 +1576,9 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - SocketUser user = State.GetUser(data.User.Id); + SocketUser user = StateManager.GetUser(data.User.Id); if (user == null) - user = SocketUnknownUser.Create(this, State, data.User); + user = SocketUnknownUser.Create(this, StateManager, data.User); await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); } else @@ -1616,7 +1609,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 +1622,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, StateManager, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1657,7 +1650,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 +1675,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 +1687,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, StateManager, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1727,12 +1720,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 +1734,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 +1934,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 +1953,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)) + if (user.GlobalUser.Update(StateManager, data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); @@ -1974,7 +1967,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } else { - user = State.GetUser(data.User.Id); + user = StateManager.GetUser(data.User.Id); if (user == null) { await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); @@ -1982,10 +1975,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(StateManager, data.User); + var after = SocketPresence.Create(data); + StateManager.AddOrUpdatePresence(after); + await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); } break; case "TYPING_START": @@ -2028,7 +2022,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 +2051,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 +2076,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 +2106,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(StateManager, data); await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else @@ -2134,7 +2128,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 +2143,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 +2175,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 +2192,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 +2221,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 +2255,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 +2269,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, StateManager, data.TargetUser.Value)) : null; var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); @@ -2294,7 +2288,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,19 +2324,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)) + ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, StateManager, data.User.Value)) : 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 { @@ -2357,7 +2351,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); @@ -2398,7 +2392,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); @@ -2408,7 +2402,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); } @@ -2421,7 +2415,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); @@ -2431,7 +2425,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); } @@ -2444,7 +2438,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); @@ -2454,7 +2448,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); } @@ -2468,7 +2462,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) { @@ -2480,14 +2474,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); } @@ -2501,7 +2495,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); @@ -2515,7 +2509,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); @@ -2523,7 +2517,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); } @@ -2543,7 +2537,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) { @@ -2564,7 +2558,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) { @@ -2578,11 +2572,11 @@ 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)) @@ -2600,7 +2594,7 @@ 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.Id.Value); if (thread == null) { @@ -2618,7 +2612,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) { @@ -2691,7 +2685,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) { @@ -2734,7 +2728,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) { @@ -2753,7 +2747,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) { @@ -2784,7 +2778,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) { @@ -2803,7 +2797,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) { @@ -2819,7 +2813,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.GetUser(data.UserId); var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); @@ -2954,7 +2948,7 @@ private async Task SyncGuildsAsync() await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } - internal SocketGuild AddGuild(ExtendedGuild model, ClientState state) + internal SocketGuild AddGuild(ExtendedGuild model, ClientStateManager state) { var guild = SocketGuild.Create(this, state, model); state.AddGuild(guild); @@ -2963,26 +2957,26 @@ internal SocketGuild AddGuild(ExtendedGuild model, ClientState state) 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; + var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; if (channel != null) { foreach (var recipient in channel.Recipients) @@ -2992,8 +2986,8 @@ internal ISocketPrivateChannel RemovePrivateChannel(ulong id) } internal void RemoveDMChannels() { - var channels = State.DMChannels; - State.PurgeDMChannels(); + var channels = StateManager.DMChannels; + StateManager.PurgeDMChannels(); foreach (var channel in channels) channel.Recipient.GlobalUser.RemoveRef(this); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 4cd64dbc2d..21d84ba3b9 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -25,6 +25,12 @@ namespace Discord.WebSocket /// public class DiscordSocketConfig : DiscordRestConfig { + /// + /// Gets or sets the cache provider to use + /// + public ICacheProvider CacheProvider { get; set; } + public IStateProvider StateProvider { get; set; } + /// /// Returns the encoding gateway should use. /// @@ -193,6 +199,11 @@ public int MaxWaitBetweenGuildAvailablesBeforeReady /// public bool SuppressUnknownDispatchWarnings { get; set; } = true; + /// + /// Gets or sets the default state behavior clients will use. + /// + public StateBehavior DefaultStateBehavior { get; set; } = StateBehavior.Default; + /// /// Initializes a new instance of the class with the default configuration. /// 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..755fa7ab34 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -35,23 +35,23 @@ 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]); } - 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); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 4f068cf810..f6736245d5 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,7 +73,7 @@ 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++) @@ -265,7 +265,7 @@ internal SocketGroupUser GetOrAddUser(UserModel model) return user; else { - var privateUser = SocketGroupUser.Create(this, Discord.State, model); + var privateUser = SocketGroupUser.Create(this, Discord.StateManager, model); privateUser.GlobalUser.AddRef(); _users[privateUser.Id] = privateUser; return privateUser; @@ -283,7 +283,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..4ff39e5e54 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); 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 49d2cd3bdf..1a16e8a253 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.GetMember(Discord.CurrentUser.Id, Id); /// /// 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.GetMembers(Id).Cast().ToImmutableArray(); /// /// 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,19 @@ internal void Update(ClientState state, ExtendedModel model) } _events = events; + for (int i = 0; i < model.Members.Length; i++) + { + Discord.StateManager.AddOrUpdateMember(Id, SocketGuildUser.Create(Id, Discord, model.Members[i])); + } + DownloadedMemberCount = model.Members.Length; + + for (int i = 0; i < model.Presences.Length; i++) + { + Discord.StateManager.AddOrUpdatePresence(SocketPresence.Create(model.Presences[i])); + } + + MemberCount = model.MemberCount; + _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); @@ -480,7 +472,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,7 +553,7 @@ 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 void Update(ClientStateManager state, GuildSyncModel model) //TODO remove? userbot related { var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); { @@ -585,7 +577,7 @@ internal void Update(ClientState state, Model model) // _ = _downloaderPromise.TrySetResultAsync(true); }*/ - 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 +674,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 +791,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 +799,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 +872,7 @@ public async Task> GetApplicationC foreach (var command in commands) { - Discord.State.AddCommand(command); + Discord.StateManager.AddCommand(command); } return commands.ToImmutableArray(); @@ -898,7 +890,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 +905,7 @@ public async ValueTask GetApplicationCommandAsync(ulon command = SocketApplicationCommand.Create(Discord, model, Id); - Discord.State.AddCommand(command); + Discord.StateManager.AddCommand(command); return command; } @@ -930,7 +922,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 +944,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 +1012,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 +1026,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 +1081,45 @@ 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.GetMember(id, Id); /// 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.GlobalUser?.Update(Discord.StateManager, model); else { - member = SocketGuildUser.Create(this, Discord.State, model); + member = SocketGuildUser.Create(Id, Discord, model); member.GlobalUser.AddRef(); - _members[member.Id] = member; DownloadedMemberCount++; } return member; } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { - if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) - member.Update(Discord.State, model); + SocketGuildUser member; + if ((member = GetUser(model.User.Id)) != null) + member.Update(Discord.StateManager, model); else { - member = SocketGuildUser.Create(this, Discord.State, model); + member = SocketGuildUser.Create(Id, Discord, 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); - else - { - member = SocketGuildUser.Create(this, Discord.State, model); - member.GlobalUser.AddRef(); - _members[member.Id] = member; 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); + Discord.StateManager.RemoveMember(id, Id); return member; } return null; @@ -1158,18 +1135,16 @@ 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); + foreach (var member in membersToPurge) + Discord.StateManager.RemoveMember(member.Id, Id); _downloaderPromise = new TaskCompletionSource(); - DownloadedMemberCount = _members.Count; + DownloadedMemberCount = membersToKeep.Count(); } /// @@ -1537,7 +1512,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..9f019cdb1e 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(Discord.StateManager, 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..28a922e650 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, Discord.StateManager, 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..d369607497 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, discord.StateManager, 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..51a691b6f3 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -129,7 +129,7 @@ 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, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) { if (model.Type == MessageType.Default || model.Type == MessageType.Reply || @@ -140,7 +140,7 @@ 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(ClientStateManager state, Model model) { Type = model.Type; 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..94c081d75b 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -53,14 +53,14 @@ 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); 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/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/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 236e7d4326..41eadcc4c8 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,18 +1,17 @@ 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(); } @@ -23,8 +22,9 @@ internal class SocketGlobalUser : SocketUser 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, ClientStateManager state, Model model) { var entity = new SocketGlobalUser(discord, model.Id); entity.Update(state, model); @@ -48,6 +48,9 @@ internal void RemoveRef(DiscordSocketClient discord) } } + ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); + public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id); + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index a40ae59bec..9d5fb0ef80 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -30,7 +30,7 @@ public class SocketGroupUser : SocketUser, IGroupUser /// 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; } } + internal override Lazy Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } /// public override bool IsWebhook => false; @@ -41,7 +41,7 @@ internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser Channel = channel; GlobalUser = globalUser; } - internal static SocketGroupUser Create(SocketGroupChannel channel, ClientState state, Model model) + internal static SocketGroupUser Create(SocketGroupChannel channel, ClientStateManager state, Model model) { var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 051687b785..2930139388 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,24 @@ 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; } + /// + /// Gets the guilds id that the user is in. + /// + public ulong GuildId => _guildId; /// public string DisplayName => Nickname ?? Username; /// @@ -47,8 +52,7 @@ public class SocketGuildUser : SocketUser, IGuildUser public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.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; @@ -78,7 +82,7 @@ public class SocketGuildUser : SocketUser, IGuildUser /// 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 +96,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 +123,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 +137,46 @@ public int Hierarchy } } - internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) - : base(guild.Discord, globalUser.Id) + internal SocketGuildUser(ulong guildId, SocketGlobalUser globalUser, DiscordSocketClient client) + : base(client, globalUser.Id) { - Guild = guild; + _guildId = guildId; + Guild = new Lazy(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); GlobalUser = globalUser; } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) - { - var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); - entity.Update(state, model); - entity.UpdateRoles(new ulong[0]); - return entity; - } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel model) + internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel 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, client.GetOrCreateUser(client.StateManager, (Discord.API.User)model), client); + if (entity.Update(client.StateManager, model)) + client.StateManager.AddOrUpdateMember(guildId, entity); + entity.UpdateRoles(Array.Empty()); return entity; } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel 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, false); - if (!model.Roles.IsSpecified) - entity.UpdateRoles(new ulong[0]); + var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, model.User), client); + entity.Update(client.StateManager, model); + client.StateManager.AddOrUpdateMember(guildId, entity); return entity; } - internal void Update(ClientState state, MemberModel model) + internal void Update(ClientStateManager 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) - { - Presence ??= new SocketPresence(); - Presence.Update(model); - GlobalUser.Update(model); + _joinedAtTicks = model.JoinedAt.UtcTicks; + 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 +220,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,7 +230,7 @@ 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)"; @@ -269,13 +240,14 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si clone.GlobalUser = GlobalUser.Clone(); return clone; } + #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 +255,55 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si /// IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; #endregion + + #region Cache + + private struct CacheModel : MemberModel + { + public UserModel User { 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; } + } + + MemberModel ICached.ToModel() + => ToMemberModel(); + + internal MemberModel ToMemberModel() + { + return new CacheModel + { + User = ((ICached)this).ToModel(), + CommunicationsDisabledUntil = TimedOutUntil, + GuildAvatar = GuildAvatarId, + IsDeaf = IsDeafened, + IsMute = IsMuted, + IsPending = IsPending, + JoinedAt = JoinedAt ?? DateTimeOffset.UtcNow, // review: nullable joined at here? should our model reflect this? + Nickname = Nickname, + PremiumSince = PremiumSince, + Roles = _roleIds.ToArray() + }; + } + + public void Dispose() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); + ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); + + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 5250e15ad5..e6cd61bcc1 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,11 @@ 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; + /// public UserStatus Status { get; private set; } /// @@ -38,8 +41,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 +81,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 +101,61 @@ private static IImmutableList ConvertActivitiesList(IList a private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; + + #region Cache + private struct 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; } + } + + internal Model ToModel() + { + return new CacheModel + { + Status = Status, + ActiveClients = ActiveClients.ToArray(), + UserId = UserId, + GuildId = GuildId, + 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 WritableActivityModel + { + Name = x.Name, + Details = x.Details, + Flags = x.Flags, + Type = x.Type + }; + }).ToArray(), + }; + } + + Model ICached.ToModel() => ToModel(); + + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 3bde1beab7..45b3ebc4f0 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; } @@ -29,7 +30,7 @@ public class SocketSelfUser : SocketUser, ISelfUser /// 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; } } + internal override Lazy Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } /// public UserProperties Flags { get; internal set; } /// @@ -45,43 +46,47 @@ internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser { GlobalUser = globalUser; } - internal static SocketSelfUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketSelfUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) { var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); entity.Update(state, model); return entity; } - internal override bool Update(ClientState state, Model model) + internal override bool Update(ClientStateManager state, UserModel model) { bool hasGlobalChanges = base.Update(state, model); - if (model.Email.IsSpecified) + + 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 +98,55 @@ public Task ModifyAsync(Action func, RequestOptions options private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + + #region Cache + + private struct 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; } + } + + Model ICached.ToModel() + { + return new CacheModel + { + Avatar = AvatarId, + Discriminator = Discriminator, + Email = Email, + Flags = Flags, + Id = Id, + IsBot = IsBot, + IsMfaEnabled = IsMfaEnabled, + IsVerified = IsVerified, + Locale = Locale, + PremiumType = this.PremiumType, + PublicFlags = PublicFlags ?? UserProperties.None, + Username = Username + }; + } + + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 6eddd876d0..e42805e4ee 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -227,7 +227,7 @@ internal void Update(Model model) internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } - internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } + internal override Lazy Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } /// /// Gets the guild user of this thread user. diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 99c47696a8..5d2ddef327 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -26,7 +26,7 @@ public class SocketUnknownUser : SocketUser /// public override bool IsWebhook => false; /// - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + internal override Lazy Presence { get { return new Lazy(() => 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(); } @@ -35,7 +35,7 @@ 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, ClientStateManager state, Model model) { var entity = new SocketUnknownUser(discord, model.Id); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index d70e617392..fca36184b7 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,7 +15,7 @@ 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; } @@ -30,7 +30,7 @@ public abstract class SocketUser : SocketEntity, IUser /// public UserProperties? PublicFlags { get; private set; } internal abstract SocketGlobalUser GlobalUser { get; set; } - internal abstract SocketPresence Presence { get; set; } + internal virtual Lazy Presence { 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. /// @@ -57,46 +57,45 @@ internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal virtual bool Update(ClientState state, Model model) + internal virtual bool Update(ClientStateManager state, Model model) { - Presence ??= new SocketPresence(); + Presence ??= new Lazy(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); 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); + return hasChanges; } /// @@ -120,5 +119,36 @@ 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 + private struct 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; } + } + + Model ICached.ToModel() + => ToModel(); + + internal Model ToModel() + { + return new CacheModel + { + Avatar = AvatarId, + Discriminator = Discriminator, + Id = Id, + IsBot = IsBot, + Username = Username + }; + } + + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 2b2c259c52..06f9a8ab5b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -33,7 +33,7 @@ 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 Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) @@ -42,7 +42,7 @@ 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, ClientStateManager state, Model model, ulong webhookId) { var entity = new SocketWebhookUser(guild, model.Id, webhookId); entity.Update(state, model); diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 46f5c1a268..6cde93d871 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -7,86 +7,97 @@ namespace Discord.WebSocket { internal static class EntityExtensions { - public static IActivity ToEntity(this API.Game model) + 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 } diff --git a/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs b/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs new file mode 100644 index 0000000000..7719b26c11 --- /dev/null +++ b/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal static class StateExtensions + { + public static StateBehavior ToBehavior(this CacheMode mode) + { + return mode switch + { + CacheMode.AllowDownload => StateBehavior.AllowDownload, + CacheMode.CacheOnly => StateBehavior.CacheOnly, + _ => StateBehavior.AllowDownload + }; + } + } +} 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 4cd9ef2648..d61068e3ec 100644 --- a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs +++ b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs @@ -45,7 +45,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; } diff --git a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs b/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs new file mode 100644 index 0000000000..1604fce0c8 --- /dev/null +++ b/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs @@ -0,0 +1,256 @@ +using Discord.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class DefaultStateProvider : IStateProvider + { + private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 + private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 + private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth + + private readonly ICacheProvider _cache; + private readonly StateBehavior _defaultBehavior; + private readonly DiscordSocketClient _client; + private readonly Logger _logger; + public DefaultStateProvider(Logger logger, ICacheProvider cacheProvider, DiscordSocketClient client, StateBehavior stateBehavior) + { + _cache = cacheProvider; + _client = client; + _logger = logger; + + if (stateBehavior == StateBehavior.Default) + throw new ArgumentException("Cannot use \"default\" as the default state behavior"); + + _defaultBehavior = stateBehavior; + } + + private void RunAsyncWithLogs(ValueTask task) + { + _ = Task.Run(async () => + { + try + { + await task.ConfigureAwait(false); + } + catch (Exception x) + { + await _logger.ErrorAsync("Cache provider failed", x).ConfigureAwait(false); + } + }); + } + + private TResult WaitSynchronouslyForTask(Task t) + { + var sw = new SpinWait(); + while (!t.IsCompleted) + sw.SpinOnce(); + return t.GetAwaiter().GetResult(); + } + + private TType ValidateAsSocketEntity(ISnowflakeEntity entity) where TType : SocketEntity + { + if(entity is not TType val) + throw new NotSupportedException("Cannot cache non-socket entities"); + return val; + } + + private StateBehavior ResolveBehavior(StateBehavior behavior) + => behavior == StateBehavior.Default ? _defaultBehavior : behavior; + + + public ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user) + { + var socketGuildUser = ValidateAsSocketEntity(user); + var model = socketGuildUser.ToMemberModel(); + RunAsyncWithLogs(_cache.AddOrUpdateMemberAsync(model, guildId, CacheRunMode.Async)); + return default; + } + public ValueTask AddOrUpdateUserAsync(IUser user) + { + var socketUser = ValidateAsSocketEntity(user); + var model = socketUser.ToModel(); + RunAsyncWithLogs(_cache.AddOrUpdateUserAsync(model, CacheRunMode.Async)); + return default; + } + public ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null) + { + var behavior = ResolveBehavior(stateBehavior); + + var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; + + if(behavior != StateBehavior.DownloadOnly) + { + var memberLookupTask = _cache.GetMemberAsync(id, guildId, cacheMode); + + if (memberLookupTask.IsCompleted) + { + var model = memberLookupTask.Result; + if(model != null) + return new ValueTask(SocketGuildUser.Create(guildId, _client, model)); + } + else + { + return new ValueTask(Task.Run(async () => + { + var result = await memberLookupTask; + + if (result != null) + return (IGuildUser)SocketGuildUser.Create(guildId, _client, result); + else if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) + return await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false); + return null; + })); + } + } + + if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) + return new ValueTask(_client.Rest.GetGuildUserAsync(guildId, id, options).ContinueWith(x => (IGuildUser)x.Result)); + + return default; + } + + public ValueTask> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null) + { + var behavior = ResolveBehavior(stateBehavior); + + var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; + + if(behavior != StateBehavior.DownloadOnly) + { + var memberLookupTask = _cache.GetMembersAsync(guildId, cacheMode); + + if (memberLookupTask.IsCompleted) + return new ValueTask>(memberLookupTask.Result?.Select(x => SocketGuildUser.Create(guildId, _client, x))); + else + { + return new ValueTask>(Task.Run(async () => + { + var result = await memberLookupTask; + + if (result != null && result.Any()) + return result.Select(x => (IGuildUser)SocketGuildUser.Create(guildId, _client, x)); + + if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) + return await _client.Rest.GetGuildUsersAsync(guildId, options); + + return null; + })); + } + } + + return default; + } + + public ValueTask GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null) + { + var behavior = ResolveBehavior(stateBehavior); + + var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; + + if (behavior != StateBehavior.DownloadOnly) + { + var userLookupTask = _cache.GetUserAsync(id, cacheMode); + + if (userLookupTask.IsCompleted) + { + var model = userLookupTask.Result; + if(model != null) + return new ValueTask(SocketGlobalUser.Create(_client, null, model)); + } + else + { + return new ValueTask(Task.Run(async () => + { + var result = await userLookupTask; + + if (result != null) + return SocketGlobalUser.Create(_client, null, result); + + if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) + return await _client.Rest.GetUserAsync(id, options); + + return null; + })); + } + } + + if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) + return new ValueTask(_client.Rest.GetUserAsync(id, options).ContinueWith(x => (IUser)x.Result)); + + return default; + } + + public ValueTask> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null) + { + var behavior = ResolveBehavior(stateBehavior); + + var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; + + if(behavior != StateBehavior.DownloadOnly) + { + var usersTask = _cache.GetUsersAsync(cacheMode); + + if (usersTask.IsCompleted) + return new ValueTask>(usersTask.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x))); + else + { + return new ValueTask>(usersTask.AsTask().ContinueWith(x => x.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x)))); + } + } + + // no download path + return default; + } + + public ValueTask RemoveMemberAsync(ulong id, ulong guildId) + => _cache.RemoveMemberAsync(id, guildId, CacheRunMode.Async); + public ValueTask RemoveUserAsync(ulong id) + => _cache.RemoveUserAsync(id, CacheRunMode.Async); + + public ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBehavior) + { + var behavior = ResolveBehavior(stateBehavior); + + var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; + + if(stateBehavior != StateBehavior.DownloadOnly) + { + var fetchTask = _cache.GetPresenceAsync(userId, cacheMode); + + if (fetchTask.IsCompleted) + return new ValueTask(SocketPresence.Create(fetchTask.Result)); + else + { + return new ValueTask(fetchTask.AsTask().ContinueWith(x => + { + if (x.Result != null) + return (IPresence)SocketPresence.Create(x.Result); + return null; + })); + } + } + + // theres no rest call to download presence so return null + return new ValueTask((IPresence)null); + } + + public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior) + { + if (presense is not SocketPresence socketPresense) + throw new ArgumentException($"Expected socket entity but got {presense?.GetType()}"); + + var model = socketPresense.ToModel(); + + RunAsyncWithLogs(_cache.AddOrUpdatePresenseAsync(userId, model, CacheRunMode.Async)); + return default; + } + public ValueTask RemovePresenseAsync(ulong userId) + => _cache.RemovePresenseAsync(userId, CacheRunMode.Async); + } +} diff --git a/src/Discord.Net.WebSocket/State/IStateProvider.cs b/src/Discord.Net.WebSocket/State/IStateProvider.cs new file mode 100644 index 0000000000..c944d9f199 --- /dev/null +++ b/src/Discord.Net.WebSocket/State/IStateProvider.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface IStateProvider + { + ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBehavior); + ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior); + ValueTask RemovePresenseAsync(ulong userId); + + ValueTask GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null); + ValueTask> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null); + ValueTask AddOrUpdateUserAsync(IUser user); + ValueTask RemoveUserAsync(ulong id); + + ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null); + ValueTask> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null); + ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user); + ValueTask RemoveMemberAsync(ulong guildId, ulong id); + } +} diff --git a/src/Discord.Net.WebSocket/State/StateBehavior.cs b/src/Discord.Net.WebSocket/State/StateBehavior.cs new file mode 100644 index 0000000000..4a387d5a9e --- /dev/null +++ b/src/Discord.Net.WebSocket/State/StateBehavior.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public enum StateBehavior + { + /// + /// Use the default Cache Behavior of the client. + /// + /// + Default = 0, + /// + /// The entity will only be retrieved via a synchronous cache lookup. + /// + /// For the default , this is equivalent to using + /// + /// + /// This flag is used to indicate that the retrieval of this entity should not leave the + /// synchronous path of the . When true, + /// the calling method *should* not ever leave the calling task, and never generate an async + /// state machine. + /// + /// Bear in mind that the true behavior of this flag depends entirely on the to + /// abide by design implications of this flag. Once Discord.Net has called out to the state provider with this + /// flag, it is out of our control whether or not an async method is evaluated. + /// + SyncOnly = 1, + /// + /// The entity will only be retrieved via a cache lookup - the Discord API will not be contacted to retrieve the entity. + /// + /// + /// When using an alternative , usage of this flag implies that it is + /// okay for the state provider to make an external call if the local cache missed the entity. + /// + /// Note that when designing an , this flag does not imply that the state + /// provider itself should contact Discord for the entity; rather that if using a dual-layer caching system, + /// it would be okay to contact an external layer, e.g. Redis, for the entity. + /// + CacheOnly = 2, + /// + /// The entity will be downloaded from the Discord REST API if the on hand cannot locate it. + /// + AllowDownload = 3, + /// + /// The entity will be downloaded from the Discord REST API. The local will not be contacted to find the entity. + /// + DownloadOnly = 4 + } +} From 627f88795b42276ef9b83f2f45bc24fc021448cb Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Mon, 18 Apr 2022 08:56:45 -0300 Subject: [PATCH 02/11] Change up model flows --- .../Cache/CacheableEntityExtensions.cs | 16 ++-- .../Cache/Models/Emoji/IEmojiModel.cs | 29 ++---- .../Cache/Models/Presense/IActivityModel.cs | 86 +++++------------ .../Cache/Models/Presense/IPresenceModel.cs | 10 +- .../Cache/Models/Users/ICurrentUserModel.cs | 14 +-- .../Cache/Models/Users/IMemberModel.cs | 20 ++-- .../Cache/Models/Users/IUserModel.cs | 11 ++- .../API/Common/CurrentUser.cs | 42 +++++++-- src/Discord.Net.Rest/API/Common/Emoji.cs | 49 ++++++++-- src/Discord.Net.Rest/API/Common/Game.cs | 92 ++++++++++++++----- .../API/Common/GuildMember.cs | 40 ++++++-- src/Discord.Net.Rest/API/Common/Presence.cs | 22 +++-- src/Discord.Net.Rest/API/Common/User.cs | 28 +++++- .../Cache/ICacheProvider.cs | 2 +- .../ClientStateManager.Experiment.cs | 5 +- .../Entities/Users/SocketPresence.cs | 49 +++++++++- .../Entities/Users/SocketUser.cs | 10 +- 17 files changed, 337 insertions(+), 188 deletions(-) diff --git a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs index fb265f94af..6e6b0945fa 100644 --- a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs +++ b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs @@ -8,7 +8,7 @@ namespace Discord { internal static class CacheableEntityExtensions { - public static IActivityModel ToModel(this RichGame richGame) where TModel : WritableActivityModel, new() + public static IActivityModel ToModel(this RichGame richGame) where TModel : IActivityModel, new() { return new TModel() { @@ -34,7 +34,7 @@ internal static class CacheableEntityExtensions }; } - public static IActivityModel ToModel(this SpotifyGame spotify) where TModel : WritableActivityModel, new() + public static IActivityModel ToModel(this SpotifyGame spotify) where TModel : IActivityModel, new() { return new TModel() { @@ -53,11 +53,12 @@ internal static class CacheableEntityExtensions } public static IActivityModel ToModel(this CustomStatusGame custom) - where TModel : WritableActivityModel, new() - where TEmoteModel : WritableEmojiModel, new() + where TModel : IActivityModel, new() + where TEmoteModel : IEmojiModel, new() { return new TModel { + Id = "custom", Type = ActivityType.CustomStatus, Name = custom.Name, State = custom.State, @@ -66,7 +67,7 @@ public static IActivityModel ToModel(this CustomStatusGame }; } - public static IActivityModel ToModel(this StreamingGame stream) where TModel : WritableActivityModel, new() + public static IActivityModel ToModel(this StreamingGame stream) where TModel : IActivityModel, new() { return new TModel { @@ -77,8 +78,11 @@ public static IActivityModel ToModel(this CustomStatusGame }; } - public static IEmojiModel ToModel(this IEmote emote) where TModel : WritableEmojiModel, new() + public static IEmojiModel ToModel(this IEmote emote) where TModel : IEmojiModel, new() { + if (emote == null) + return null; + var model = new TModel() { Name = emote.Name diff --git a/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs b/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs index bc5b43e2a6..4a1270ba7a 100644 --- a/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs @@ -8,27 +8,14 @@ namespace Discord { public interface IEmojiModel { - ulong? Id { get; } - string Name { get; } - ulong[] Roles { get; } - bool RequireColons { get; } - bool IsManaged { get; } - bool IsAnimated { get; } - bool IsAvailable { get; } + 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; } - } - - internal class WritableEmojiModel : 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; } + ulong? CreatorId { get; set; } } } diff --git a/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs b/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs index a66e887548..64633e6048 100644 --- a/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs @@ -8,81 +8,41 @@ namespace Discord { public interface IActivityModel { - string Id { get; } - string Url { get; } - string Name { get; } - ActivityType Type { get; } - string Details { get; } - string State { get; } - ActivityProperties Flags { get; } - DateTimeOffset CreatedAt { get; } - IEmojiModel Emoji { get; } - ulong? ApplicationId { get; } - string SyncId { get; } - string SessionId { get; } + 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; } - string LargeText { get; } - string SmallImage { get; } - string SmallText { get; } + string LargeImage { get; set; } + string LargeText { get; set; } + string SmallImage { get; set; } + string SmallText { get; set; } #endregion #region Party - string PartyId { get; } - long[] PartySize { get; } + string PartyId { get; set; } + long[] PartySize { get; set; } #endregion #region Secrets - string JoinSecret { get; } - string SpectateSecret { get; } - string MatchSecret { get; } + string JoinSecret { get; set; } + string SpectateSecret { get; set; } + string MatchSecret { get; set; } #endregion #region Timestamps - DateTimeOffset? TimestampStart { get; } - DateTimeOffset? TimestampEnd { get; } - #endregion - } - - internal class WritableActivityModel : 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; } - - - #region Assets - public string LargeImage { get; set; } - public string LargeText { get; set; } - public string SmallImage { get; set; } - public string SmallText { get; set; } - #endregion - - #region Party - public string PartyId { get; set; } - public long[] PartySize { get; set; } - #endregion - - #region Secrets - public string JoinSecret { get; set; } - public string SpectateSecret { get; set; } - public string MatchSecret { get; set; } - #endregion - - #region Timestamps - public DateTimeOffset? TimestampStart { get; set; } - public DateTimeOffset? TimestampEnd { get; set; } + 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 index c58a1fad50..887aa858be 100644 --- a/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs @@ -8,10 +8,10 @@ namespace Discord { public interface IPresenceModel { - ulong UserId { get; } - ulong? GuildId { get; } - UserStatus Status { get; } - ClientType[] ActiveClients { get; } - IActivityModel[] Activities { get; } + 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 index 80b832cf88..7d7b289955 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs @@ -8,12 +8,12 @@ namespace Discord { public interface ICurrentUserModel : IUserModel { - bool? IsVerified { get; } - string Email { get; } - bool? IsMfaEnabled { get; } - UserProperties Flags { get; } - PremiumType PremiumType { get; } - string Locale { get; } - UserProperties PublicFlags { get; } + 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 index 3c3a67d5e6..972fa7354f 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs @@ -8,16 +8,16 @@ namespace Discord { public interface IMemberModel { - IUserModel User { get; } + IUserModel User { get; set; } - string Nickname { get; } - string GuildAvatar { get; } - ulong[] Roles { get; } - DateTimeOffset JoinedAt { get; } - DateTimeOffset? PremiumSince { get; } - bool IsDeaf { get; } - bool IsMute { get; } - bool? IsPending { get; } - DateTimeOffset? CommunicationsDisabledUntil { get; } + 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/IUserModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs index 24d05d0fcb..6dcf1d9a7a 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs @@ -6,11 +6,12 @@ namespace Discord { - public interface IUserModel : IEntity + public interface IUserModel { - string Username { get; } - string Discriminator { get; } - bool? IsBot { get; } - string Avatar { get; } + ulong Id { get; set; } + string Username { get; set; } + string Discriminator { get; set; } + bool? IsBot { get; set; } + string Avatar { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/CurrentUser.cs b/src/Discord.Net.Rest/API/Common/CurrentUser.cs index 4a19056b2b..626256ae19 100644 --- a/src/Discord.Net.Rest/API/Common/CurrentUser.cs +++ b/src/Discord.Net.Rest/API/Common/CurrentUser.cs @@ -25,18 +25,46 @@ internal class CurrentUser : User, ICurrentUserModel public Optional PublicFlags { get; set; } // ICurrentUserModel - bool? ICurrentUserModel.IsVerified => Verified.ToNullable(); + bool? ICurrentUserModel.IsVerified + { + get => Verified.ToNullable(); + set => throw new NotSupportedException(); + } - string ICurrentUserModel.Email => Email.GetValueOrDefault(); + string ICurrentUserModel.Email + { + get => Email.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - bool? ICurrentUserModel.IsMfaEnabled => MfaEnabled.ToNullable(); + bool? ICurrentUserModel.IsMfaEnabled + { + get => MfaEnabled.ToNullable(); + set => throw new NotSupportedException(); + } - UserProperties ICurrentUserModel.Flags => Flags.GetValueOrDefault(); + UserProperties ICurrentUserModel.Flags + { + get => Flags.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - PremiumType ICurrentUserModel.PremiumType => PremiumType.GetValueOrDefault(); + PremiumType ICurrentUserModel.PremiumType + { + get => PremiumType.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - string ICurrentUserModel.Locale => Locale.GetValueOrDefault(); + string ICurrentUserModel.Locale + { + get => Locale.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - UserProperties ICurrentUserModel.PublicFlags => PublicFlags.GetValueOrDefault(); + UserProperties ICurrentUserModel.PublicFlags + { + get => PublicFlags.GetValueOrDefault(); + set => throw new NotSupportedException(); + } } } diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index af1a255321..2bc381fdb3 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; namespace Discord.API { @@ -21,20 +22,52 @@ internal class Emoji : IEmojiModel [JsonProperty("user")] public Optional User { get; set; } - ulong? IEmojiModel.Id => Id; + ulong? IEmojiModel.Id + { + get => Id; + set => throw new NotSupportedException(); + } - string IEmojiModel.Name => Name; + string IEmojiModel.Name + { + get => Name; + set => throw new NotSupportedException(); + } - ulong[] IEmojiModel.Roles => Roles; + ulong[] IEmojiModel.Roles + { + get => Roles; + set => throw new NotSupportedException(); + } - bool IEmojiModel.RequireColons => RequireColons; + bool IEmojiModel.RequireColons + { + get => RequireColons; + set => throw new NotSupportedException(); + } - bool IEmojiModel.IsManaged => Managed; + bool IEmojiModel.IsManaged + { + get => Managed; + set => throw new NotSupportedException(); + } - bool IEmojiModel.IsAnimated => Animated.GetValueOrDefault(); + bool IEmojiModel.IsAnimated + { + get => Animated.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - bool IEmojiModel.IsAvailable => Available.GetValueOrDefault(); + bool IEmojiModel.IsAvailable + { + get => Available.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - ulong? IEmojiModel.CreatorId => User.GetValueOrDefault()?.Id; + 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 32e0d5c513..133943d3e2 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -42,50 +42,96 @@ internal class Game : IActivityModel [JsonProperty("created_at")] public Optional CreatedAt { get; set; } - string IActivityModel.Id => Id.GetValueOrDefault(); + string IActivityModel.Id { + get => Id.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.Url => StreamUrl.GetValueOrDefault(); + string IActivityModel.Url { + get => StreamUrl.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.State => State.GetValueOrDefault(); + string IActivityModel.State { + get => State.GetValueOrDefault(); set => throw new NotSupportedException(); + } - IEmojiModel IActivityModel.Emoji => Emoji.GetValueOrDefault(); + IEmojiModel IActivityModel.Emoji { + get => Emoji.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.Name => Name; + string IActivityModel.Name { + get => Name; set => throw new NotSupportedException(); + } - ActivityType IActivityModel.Type => Type.GetValueOrDefault().GetValueOrDefault(); + ActivityType IActivityModel.Type { + get => Type.GetValueOrDefault().GetValueOrDefault(); set => throw new NotSupportedException(); + } - ActivityProperties IActivityModel.Flags => Flags.GetValueOrDefault(); + ActivityProperties IActivityModel.Flags { + get => Flags.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.Details => Details.GetValueOrDefault(); - DateTimeOffset IActivityModel.CreatedAt => DateTimeOffset.FromUnixTimeMilliseconds(CreatedAt.GetValueOrDefault()); + 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 => ApplicationId.ToNullable(); + ulong? IActivityModel.ApplicationId { + get => ApplicationId.ToNullable(); set => throw new NotSupportedException(); + } - string IActivityModel.SyncId => SyncId.GetValueOrDefault(); + string IActivityModel.SyncId { + get => SyncId.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.SessionId => SessionId.GetValueOrDefault(); + string IActivityModel.SessionId { + get => SessionId.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.LargeImage => Assets.GetValueOrDefault()?.LargeImage.GetValueOrDefault(); + string IActivityModel.LargeImage { + get => Assets.GetValueOrDefault()?.LargeImage.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.LargeText => Assets.GetValueOrDefault()?.LargeText.GetValueOrDefault(); + string IActivityModel.LargeText { + get => Assets.GetValueOrDefault()?.LargeText.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.SmallImage => Assets.GetValueOrDefault()?.SmallImage.GetValueOrDefault(); + string IActivityModel.SmallImage { + get => Assets.GetValueOrDefault()?.SmallImage.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.SmallText => Assets.GetValueOrDefault()?.SmallText.GetValueOrDefault(); + string IActivityModel.SmallText { + get => Assets.GetValueOrDefault()?.SmallText.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IActivityModel.PartyId => Party.GetValueOrDefault()?.Id; + string IActivityModel.PartyId { + get => Party.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); + } - long[] IActivityModel.PartySize => Party.GetValueOrDefault()?.Size; + long[] IActivityModel.PartySize { + get => Party.GetValueOrDefault()?.Size; set => throw new NotSupportedException(); + } - string IActivityModel.JoinSecret => Secrets.GetValueOrDefault()?.Join; + string IActivityModel.JoinSecret { + get => Secrets.GetValueOrDefault()?.Join; set => throw new NotSupportedException(); + } - string IActivityModel.SpectateSecret => Secrets.GetValueOrDefault()?.Spectate; + string IActivityModel.SpectateSecret { + get => Secrets.GetValueOrDefault()?.Spectate; set => throw new NotSupportedException(); + } - string IActivityModel.MatchSecret => Secrets.GetValueOrDefault()?.Match; + string IActivityModel.MatchSecret { + get => Secrets.GetValueOrDefault()?.Match; set => throw new NotSupportedException(); + } - DateTimeOffset? IActivityModel.TimestampStart => Timestamps.GetValueOrDefault()?.Start.ToNullable(); + DateTimeOffset? IActivityModel.TimestampStart { + get => Timestamps.GetValueOrDefault()?.Start.ToNullable(); set => throw new NotSupportedException(); + } - DateTimeOffset? IActivityModel.TimestampEnd => Timestamps.GetValueOrDefault()?.End.ToNullable(); + DateTimeOffset? IActivityModel.TimestampEnd { + get => Timestamps.GetValueOrDefault()?.End.ToNullable(); set => throw new NotSupportedException(); + } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index cfe4e652e8..18aac38225 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -27,24 +27,44 @@ internal class GuildMember : IMemberModel public Optional TimedOutUntil { get; set; } // IMemberModel - string IMemberModel.Nickname => Nick.GetValueOrDefault(); + string IMemberModel.Nickname { + get => Nick.GetValueOrDefault(); set => throw new NotSupportedException(); + } - string IMemberModel.GuildAvatar => Avatar.GetValueOrDefault(); + string IMemberModel.GuildAvatar { + get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); + } - ulong[] IMemberModel.Roles => Roles.GetValueOrDefault(Array.Empty()); + ulong[] IMemberModel.Roles { + get => Roles.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); + } - DateTimeOffset IMemberModel.JoinedAt => JoinedAt.GetValueOrDefault(); + DateTimeOffset IMemberModel.JoinedAt { + get => JoinedAt.GetValueOrDefault(); set => throw new NotSupportedException(); + } - DateTimeOffset? IMemberModel.PremiumSince => PremiumSince.GetValueOrDefault(); + DateTimeOffset? IMemberModel.PremiumSince { + get => PremiumSince.GetValueOrDefault(); set => throw new NotSupportedException(); + } - bool IMemberModel.IsDeaf => Deaf.GetValueOrDefault(false); + bool IMemberModel.IsDeaf { + get => Deaf.GetValueOrDefault(false); set => throw new NotSupportedException(); + } - bool IMemberModel.IsMute => Mute.GetValueOrDefault(false); + bool IMemberModel.IsMute { + get => Mute.GetValueOrDefault(false); set => throw new NotSupportedException(); + } - bool? IMemberModel.IsPending => Pending.ToNullable(); + bool? IMemberModel.IsPending { + get => Pending.ToNullable(); set => throw new NotSupportedException(); + } - DateTimeOffset? IMemberModel.CommunicationsDisabledUntil => TimedOutUntil.GetValueOrDefault(); + DateTimeOffset? IMemberModel.CommunicationsDisabledUntil { + get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); + } - IUserModel IMemberModel.User => User; + IUserModel IMemberModel.User { + get => User; 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 1734602424..4269074c04 100644 --- a/src/Discord.Net.Rest/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -30,16 +30,24 @@ internal class Presence : IPresenceModel [JsonProperty("premium_since")] public Optional PremiumSince { get; set; } - ulong IPresenceModel.UserId => User.Id; + ulong IPresenceModel.UserId { + get => User.Id; set => throw new NotSupportedException(); + } - ulong? IPresenceModel.GuildId => GuildId.ToNullable(); + ulong? IPresenceModel.GuildId { + get => GuildId.ToNullable(); set => throw new NotSupportedException(); + } - UserStatus IPresenceModel.Status => Status; + UserStatus IPresenceModel.Status { + get => Status; set => throw new NotSupportedException(); + } - ClientType[] IPresenceModel.ActiveClients => ClientStatus.IsSpecified - ? ClientStatus.Value.Select(x => (ClientType)Enum.Parse(typeof(ClientType), x.Key, true)).ToArray() - : Array.Empty(); + 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 => Activities.ToArray(); + IActivityModel[] IPresenceModel.Activities { + get => Activities.ToArray(); 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 26e5deafdc..03d23374f4 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; namespace Discord.API { @@ -21,14 +22,31 @@ internal class User : IUserModel // IUserModel - string IUserModel.Username => Username.GetValueOrDefault(); + string IUserModel.Username + { + get => Username.GetValueOrDefault(); + set => throw new NotSupportedException(); + } - string IUserModel.Discriminator => Discriminator.GetValueOrDefault(); + string IUserModel.Discriminator { + get => Discriminator.GetValueOrDefault(); set => throw new NotSupportedException(); + } - bool? IUserModel.IsBot => Bot.ToNullable(); + bool? IUserModel.IsBot + { + get => Bot.ToNullable(); + set => throw new NotSupportedException(); + } - string IUserModel.Avatar => Avatar.GetValueOrDefault(); + string IUserModel.Avatar + { + get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); + } - ulong IEntity.Id => Id; + ulong IUserModel.Id + { + get => Id; + set => throw new NotSupportedException(); + } } } diff --git a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs index 2655801930..5d67b892ed 100644 --- a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs @@ -29,7 +29,7 @@ public interface ICacheProvider #region Presence ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode); - ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode); + ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel model, CacheRunMode runmode); ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode); #endregion diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index cc59c57aa8..4ad2460a34 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -41,7 +41,10 @@ private void EnsureSync(ValueTask vt) #region Global users internal void RemoveReferencedGlobalUser(ulong id) - => _userReferences.TryRemove(id, out _); + { + Console.WriteLine("Global user untracked"); + _userReferences.TryRemove(id, out _); + } private void TrackGlobalUser(ulong id, SocketGlobalUser user) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index e6cd61bcc1..9e64bc2bb6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -116,6 +116,45 @@ private struct CacheModel : Model public ulong? GuildId { get; set; } } + private struct 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 struct 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() { return new CacheModel @@ -132,18 +171,18 @@ internal Model ToModel() switch (game) { case RichGame richGame: - return richGame.ToModel(); + return richGame.ToModel(); case SpotifyGame spotify: - return spotify.ToModel(); + return spotify.ToModel(); case CustomStatusGame custom: - return custom.ToModel(); + return custom.ToModel(); case StreamingGame stream: - return stream.ToModel(); + return stream.ToModel(); } break; } - return new WritableActivityModel + return new ActivityCacheModel { Name = x.Name, Details = x.Details, diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index fca36184b7..d61fe8ea75 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket /// Represents a WebSocket-based user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class SocketUser : SocketEntity, IUser, ICached + public abstract class SocketUser : SocketEntity, IUser, ICached, IDisposable { /// public abstract bool IsBot { get; internal set; } @@ -41,9 +41,9 @@ public abstract class SocketUser : SocketEntity, IUser, ICached /// public UserStatus Status => Presence.Value.Status; /// - public IReadOnlyCollection ActiveClients => Presence.Value.ActiveClients ?? ImmutableHashSet.Empty; + public IReadOnlyCollection ActiveClients => Presence.Value?.ActiveClients ?? ImmutableHashSet.Empty; /// - public IReadOnlyCollection Activities => Presence.Value.Activities ?? ImmutableList.Empty; + public IReadOnlyCollection Activities => Presence.Value?.Activities ?? ImmutableList.Empty; /// /// Gets mutual guilds shared with this user. /// @@ -59,7 +59,7 @@ internal SocketUser(DiscordSocketClient discord, ulong id) } internal virtual bool Update(ClientStateManager state, Model model) { - Presence ??= new Lazy(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); + Presence ??= new Lazy(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); bool hasChanges = false; if (model.Avatar != AvatarId) { @@ -117,6 +117,8 @@ public string GetDefaultAvatarUrl() /// The full name of the user. /// public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); + ~SocketUser() => GlobalUser?.Dispose(); + public void Dispose() => GlobalUser?.Dispose(); private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; From adcb58e473414dc1cf22364bea03379707e74413 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Mon, 18 Apr 2022 09:02:39 -0300 Subject: [PATCH 03/11] refactor default state provider --- .../State/DefaultStateProvider.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs b/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs index 1604fce0c8..a95227d347 100644 --- a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs +++ b/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs @@ -45,14 +45,6 @@ private void RunAsyncWithLogs(ValueTask task) }); } - private TResult WaitSynchronouslyForTask(Task t) - { - var sw = new SpinWait(); - while (!t.IsCompleted) - sw.SpinOnce(); - return t.GetAwaiter().GetResult(); - } - private TType ValidateAsSocketEntity(ISnowflakeEntity entity) where TType : SocketEntity { if(entity is not TType val) @@ -63,7 +55,6 @@ private TType ValidateAsSocketEntity(ISnowflakeEntity entity) where TType private StateBehavior ResolveBehavior(StateBehavior behavior) => behavior == StateBehavior.Default ? _defaultBehavior : behavior; - public ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user) { var socketGuildUser = ValidateAsSocketEntity(user); @@ -96,7 +87,7 @@ public ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavi } else { - return new ValueTask(Task.Run(async () => + return new ValueTask(Task.Run(async () => // review: task.run here? { var result = await memberLookupTask; @@ -144,6 +135,9 @@ public ValueTask> GetMembersAsync(ulong guildId, StateBe } } + if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) + return new ValueTask>(_client.Rest.GetGuildUsersAsync(guildId, options).ContinueWith(x => x.Result.Cast())); + return default; } @@ -227,16 +221,18 @@ public ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBe return new ValueTask(SocketPresence.Create(fetchTask.Result)); else { - return new ValueTask(fetchTask.AsTask().ContinueWith(x => + return new ValueTask(Task.Run(async () => { - if (x.Result != null) - return (IPresence)SocketPresence.Create(x.Result); + var result = await fetchTask; + + if(result != null) + return (IPresence)SocketPresence.Create(result); return null; })); } } - // theres no rest call to download presence so return null + // no download path return new ValueTask((IPresence)null); } From 17306d51397316e731f73f857b0c510973cde564 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Sat, 23 Apr 2022 14:07:17 -0300 Subject: [PATCH 04/11] updates --- src/Discord.Net.Core/Cache/ICached.cs | 4 + .../Cache/Models/IEntityModel.cs | 13 + .../Cache/Models/Presense/IPresenceModel.cs | 2 +- .../Cache/Models/Users/IMemberModel.cs | 5 +- .../Cache/Models/Users/IThreadMemberModel.cs | 15 + .../Cache/Models/Users/IUserModel.cs | 3 +- .../API/Common/GuildMember.cs | 4 +- src/Discord.Net.Rest/API/Common/Presence.cs | 3 + .../API/Common/ThreadMember.cs | 10 +- src/Discord.Net.Rest/API/Common/User.cs | 4 +- .../Cache/DefaultConcurrentCacheProvider.cs | 118 +++--- .../Cache/ICacheProvider.cs | 40 +- .../ClientStateManager.Experiment.cs | 355 +++++++++++++----- .../ClientStateManager.cs | 14 +- .../DiscordSocketClient.cs | 90 ++--- .../DiscordSocketConfig.cs | 12 +- .../Entities/Guilds/SocketGuild.cs | 36 +- .../Entities/Users/SocketGlobalUser.cs | 30 +- .../Entities/Users/SocketGroupUser.cs | 33 +- .../Entities/Users/SocketGuildUser.cs | 65 ++-- .../Entities/Users/SocketPresence.cs | 13 +- .../Entities/Users/SocketSelfUser.cs | 43 ++- .../Entities/Users/SocketThreadUser.cs | 180 +++++---- .../Entities/Users/SocketUnknownUser.cs | 10 +- .../Entities/Users/SocketUser.cs | 39 +- .../Entities/Users/SocketWebhookUser.cs | 9 +- .../Extensions/StateExtensions.cs | 21 -- .../State/DefaultStateProvider.cs | 252 ------------- .../State/IStateProvider.cs | 25 -- .../State/StateBehavior.cs | 53 --- 30 files changed, 697 insertions(+), 804 deletions(-) create mode 100644 src/Discord.Net.Core/Cache/Models/IEntityModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs delete mode 100644 src/Discord.Net.WebSocket/Extensions/StateExtensions.cs delete mode 100644 src/Discord.Net.WebSocket/State/DefaultStateProvider.cs delete mode 100644 src/Discord.Net.WebSocket/State/IStateProvider.cs delete mode 100644 src/Discord.Net.WebSocket/State/StateBehavior.cs diff --git a/src/Discord.Net.Core/Cache/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs index 3146741bbc..6f7a99bfc6 100644 --- a/src/Discord.Net.Core/Cache/ICached.cs +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -8,6 +8,10 @@ namespace Discord { internal interface ICached { + void Update(TType model); + TType ToModel(); + + TResult ToModel() where TResult : TType, new(); } } 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/Presense/IPresenceModel.cs b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs index 887aa858be..b1b45f21e6 100644 --- a/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs @@ -6,7 +6,7 @@ namespace Discord { - public interface IPresenceModel + public interface IPresenceModel : IEntityModel { ulong UserId { get; set; } ulong? GuildId { get; set; } diff --git a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs index 972fa7354f..a6daa66d0b 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs @@ -6,10 +6,9 @@ namespace Discord { - public interface IMemberModel + public interface IMemberModel : IEntityModel { - IUserModel User { get; set; } - + //IUserModel User { get; set; } string Nickname { get; set; } string GuildAvatar { get; set; } ulong[] Roles { 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..b8ad311fb5 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.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 IThreadMemberModel : IEntityModel + { + ulong? ThreadId { get; set; } + ulong? UserId { 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 index 6dcf1d9a7a..88b0505200 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs @@ -6,9 +6,8 @@ namespace Discord { - public interface IUserModel + public interface IUserModel : IEntityModel { - ulong Id { get; set; } string Username { get; set; } string Discriminator { get; set; } bool? IsBot { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 18aac38225..8a373fbab7 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -63,8 +63,8 @@ bool IMemberModel.IsMute { get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); } - IUserModel IMemberModel.User { - get => User; set => throw new NotSupportedException(); + ulong IEntityModel.Id { + get => User.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 4269074c04..de450172be 100644 --- a/src/Discord.Net.Rest/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -49,5 +49,8 @@ ClientType[] IPresenceModel.ActiveClients { 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/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs index 30249ee44d..11531c77fc 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,9 @@ 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(); } + ulong? IThreadMemberModel.UserId { get => UserId.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 03d23374f4..5c8a5b240c 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -43,10 +43,10 @@ string IUserModel.Avatar get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); } - ulong IUserModel.Id + ulong IEntityModel.Id { get => Id; set => throw new NotSupportedException(); - } + } } } diff --git a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs index f0131759dc..136d88a75d 100644 --- a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs @@ -9,74 +9,74 @@ namespace Discord.WebSocket { public class DefaultConcurrentCacheProvider : ICacheProvider { - private readonly ConcurrentDictionary _users; - private readonly ConcurrentDictionary> _members; - private readonly ConcurrentDictionary _presense; + private readonly ConcurrentDictionary _storeCache = new(); + private readonly ConcurrentDictionary _subStoreCache = new(); - private ValueTask CompletedValueTask => new ValueTask(Task.CompletedTask).Preserve(); - - public DefaultConcurrentCacheProvider(int defaultConcurrency, int defaultCapacity) + private class DefaultEntityStore : IEntityStore + where TModel : IEntityModel + where TId : IEquatable { - _users = new(defaultConcurrency, defaultCapacity); - _members = new(defaultConcurrency, defaultCapacity); - _presense = new(defaultConcurrency, defaultCapacity); - } + private ConcurrentDictionary _cache; - public ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode mode) - { - _users.AddOrUpdate(model.Id, model, (_, __) => model); - return CompletedValueTask; - } - public ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode mode) - { - var guildMemberCache = _members.GetOrAdd(guildId, (_) => new ConcurrentDictionary()); - guildMemberCache.AddOrUpdate(model.User.Id, model, (_, __) => model); - return CompletedValueTask; - } - public ValueTask GetMemberAsync(ulong id, ulong guildId, CacheRunMode mode) - => new ValueTask(_members.FirstOrDefault(x => x.Key == guildId).Value?.FirstOrDefault(x => x.Key == id).Value); + public DefaultEntityStore(ConcurrentDictionary cache) + { + _cache = cache; + } - public ValueTask> GetMembersAsync(ulong guildId, CacheRunMode mode) - { - if(_members.TryGetValue(guildId, out var inner)) - return new ValueTask>(inner.ToArray().Select(x => x.Value)); // ToArray here is important before .Select due to concurrency - return new ValueTask>(Array.Empty()); - } - public ValueTask GetUserAsync(ulong id, CacheRunMode mode) - { - if (_users.TryGetValue(id, out var result)) - return new ValueTask(result); - return new ValueTask((IUserModel)null); - } - public ValueTask> GetUsersAsync(CacheRunMode mode) - => new ValueTask>(_users.ToArray().Select(x => x.Value)); - public ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode mode) - { - if (_members.TryGetValue(guildId, out var inner)) - inner.TryRemove(id, out var _); - return CompletedValueTask; - } - public ValueTask RemoveUserAsync(ulong id, CacheRunMode mode) - { - _members.TryRemove(id, out var _); - return CompletedValueTask; - } + public ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode) + { + _cache.AddOrUpdate(model.Id, model, (_, __) => model); + return default; + } - public ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode) - { - if (_presense.TryGetValue(userId, out var presense)) - return new ValueTask(presense); - return new ValueTask((IPresenceModel)null); + public ValueTask AddOrUpdateBatchAsync(IEnumerable models, CacheRunMode runmode) + { + foreach (var model in models) + _cache.AddOrUpdate(model.Id, model, (_, __) => model); + return default; + } + + public IAsyncEnumerable GetAllAsync(CacheRunMode runmode) + { + var coll = _cache.Select(x => x.Value).GetEnumerator(); + return AsyncEnumerable.Create((_) => AsyncEnumerator.Create( + () => new ValueTask(coll.MoveNext()), + () => coll.Current, + () => new ValueTask())); + } + public ValueTask GetAsync(TId id, CacheRunMode runmode) + { + if (_cache.TryGetValue(id, out var model)) + return new ValueTask(model); + return default; + } + public ValueTask RemoveAsync(TId id, CacheRunMode runmode) + { + _cache.TryRemove(id, out _); + return default; + } + + public ValueTask PurgeAllAsync(CacheRunMode runmode) + { + _cache.Clear(); + return default; + } } - public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode) + + public virtual ValueTask> GetStoreAsync() + where TModel : IEntityModel + where TId : IEquatable { - _presense.AddOrUpdate(userId, presense, (_, __) => presense); - return CompletedValueTask; + var store = _storeCache.GetOrAdd(typeof(TModel), (_) => new DefaultEntityStore(new ConcurrentDictionary())); + return new ValueTask>((IEntityStore)store); } - public ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode) + + public virtual ValueTask> GetSubStoreAsync(TId parentId) + where TModel : IEntityModel + where TId : IEquatable { - _presense.TryRemove(userId, out var _); - return CompletedValueTask; + 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 index 5d67b892ed..f4ae3b9947 100644 --- a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs @@ -8,30 +8,24 @@ namespace Discord.WebSocket { public interface ICacheProvider { - #region Users + ValueTask> GetStoreAsync() + where TModel : IEntityModel + where TId : IEquatable; - ValueTask GetUserAsync(ulong id, CacheRunMode runmode); - ValueTask> GetUsersAsync(CacheRunMode runmode); - ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode runmode); - ValueTask RemoveUserAsync(ulong id, CacheRunMode runmode); - - #endregion - - #region Members - - ValueTask GetMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); - ValueTask> GetMembersAsync(ulong guildId, CacheRunMode runmode); - ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode runmode); - ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); - - #endregion - - #region Presence - - ValueTask GetPresenceAsync(ulong userId, CacheRunMode runmode); - ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel model, CacheRunMode runmode); - ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode); + ValueTask> GetSubStoreAsync(TId parentId) + where TModel : IEntityModel + where TId : IEquatable; + } - #endregion + public interface IEntityStore + where TModel : IEntityModel + where TId : IEquatable + { + ValueTask GetAsync(TId id, CacheRunMode runmode); + IAsyncEnumerable GetAllAsync(CacheRunMode runmode); + ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode); + ValueTask AddOrUpdateBatchAsync(IEnumerable models, CacheRunMode runmode); + ValueTask RemoveAsync(TId id, CacheRunMode runmode); + ValueTask PurgeAllAsync(CacheRunMode runmode); } } diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index 4ad2460a34..f657e526db 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -1,163 +1,342 @@ +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 CacheWeakReference : WeakReference + internal class CacheReference where TType : class { - public new T Target { get => (T)base.Target; set => base.Target = value; } - public CacheWeakReference(T target) - : base(target, false) + public WeakReference Reference { get; } + + public bool CanRelease + => !Reference.TryGetTarget(out _) || _referenceCount <= 0; + + private int _referenceCount; + + private readonly object _lock = new object(); + + 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 bool TryGetTarget(out T target) + public void ReleaseReference() { - target = Target; - return IsAlive; + lock (_lock) + { + if (_referenceCount > 0) + _referenceCount--; + } } } - - internal partial class ClientStateManager + internal class ReferenceStore + where TEntity : class, ICached, ISharedEntity + where TModel : IEntityModel + where TId : IEquatable + where ISharedEntity : class { - private readonly ConcurrentDictionary> _userReferences = new(); - private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), CacheWeakReference> _memberReferences = new(); - - - #region Helpers - - private void EnsureSync(ValueTask vt) + private readonly ICacheProvider _cacheProvider; + private readonly ConcurrentDictionary> _references = new(); + private IEntityStore _store; + private Func _entityBuilder; + private Func> _restLookup; + private readonly bool _allowSyncWaits; + private readonly object _lock = new(); + + public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) { - if (!vt.IsCompleted) - throw new NotSupportedException($"Cannot use async context for value task lookup"); + _allowSyncWaits = allowSyncWaits; + _cacheProvider = cacheProvider; + _entityBuilder = entityBuilder; + _restLookup = restLookup; } - #endregion + internal void ClearDeadReferences() + { + lock (_lock) + { + var references = _references.Where(x => x.Value.CanRelease).ToArray(); + foreach (var reference in references) + _references.TryRemove(reference.Key, out _); + } + } - #region Global users - internal void RemoveReferencedGlobalUser(ulong id) + private TResult RunOrThrowValueTask(ValueTask t) { - Console.WriteLine("Global user untracked"); - _userReferences.TryRemove(id, out _); + if (_allowSyncWaits) + { + return t.GetAwaiter().GetResult(); + } + else if (t.IsCompleted) + return t.Result; + else + throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); } - private void TrackGlobalUser(ulong id, SocketGlobalUser user) + private void RunOrThrowValueTask(ValueTask t) { - if (user != null) + if (_allowSyncWaits) { - _userReferences.TryAdd(id, new CacheWeakReference(user)); + t.GetAwaiter().GetResult(); } + else if (!t.IsCompleted) + throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); } - internal ValueTask GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - => _state.GetUserAsync(id, mode.ToBehavior(), options); + public async ValueTask InitializeAsync() + { + _store ??= await _cacheProvider.GetStoreAsync().ConfigureAwait(false); + } + + public async ValueTask InitializeAsync(TId parentId) + { + _store ??= await _cacheProvider.GetSubStoreAsync(parentId).ConfigureAwait(false); + } - internal SocketGlobalUser GetUser(ulong id) + private bool TryGetReference(TId id, out TEntity entity) { - if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) - return user; + 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; + } - user = (SocketGlobalUser)_state.GetUserAsync(id, StateBehavior.SyncOnly).Result; + var model = RunOrThrowValueTask(_store.GetAsync(id, CacheRunMode.Sync)); - if(user != null) - TrackGlobalUser(id, user); + if (model != null) + { + entity = _entityBuilder(model); + _references.TryAdd(id, new CacheReference(entity)); + return entity; + } - return user; + return null; } - internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) + public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) { - if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) - return user; + if (TryGetReference(id, out var entity)) + { + return entity; + } + + var model = await _store.GetAsync(id, CacheRunMode.Async).ConfigureAwait(false); - user = GetUser(id); + if (model != null) + { + entity = _entityBuilder(model); + _references.TryAdd(id, new CacheReference(entity)); + return entity; + } - if (user == null) + if(mode == CacheMode.AllowDownload) { - user ??= userFactory(id); - _state.AddOrUpdateUserAsync(user); - TrackGlobalUser(id, user); + return await _restLookup(id, options).ConfigureAwait(false); } - return user; + return null; } - internal void RemoveUser(ulong id) + public IEnumerable GetAll() { - _state.RemoveUserAsync(id); + var models = RunOrThrowValueTask(_store.GetAllAsync(CacheRunMode.Sync).ToArrayAsync()); + return models.Select(x => + { + var entity = _entityBuilder(x); + _references.TryAdd(x.Id, new CacheReference(entity)); + return entity; + }); } - #endregion - #region GuildUsers - private void TrackMember(ulong userId, ulong guildId, SocketGuildUser user) + public async IAsyncEnumerable GetAllAsync() { - if(user != null) + await foreach(var model in _store.GetAllAsync(CacheRunMode.Async)) { - _memberReferences.TryAdd((guildId, userId), new CacheWeakReference(user)); + var entity = _entityBuilder(model); + _references.TryAdd(model.Id, new CacheReference(entity)); + yield return entity; } } - internal void RemovedReferencedMember(ulong userId, ulong guildId) - => _memberReferences.TryRemove((guildId, userId), out _); - internal ValueTask GetMemberAsync(ulong userId, ulong guildId, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - => _state.GetMemberAsync(guildId, userId, mode.ToBehavior(), options); + 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); + return _entityBuilder(model); + } - internal SocketGuildUser GetMember(ulong userId, ulong guildId) + public void AddOrUpdate(TModel model) { - if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) - return member; - member = (SocketGuildUser)_state.GetMemberAsync(guildId, userId, StateBehavior.SyncOnly).Result; - if(member != null) - TrackMember(userId, guildId, member); - return member; + RunOrThrowValueTask(_store.AddOrUpdateAsync(model, CacheRunMode.Sync)); + if (TryGetReference(model.Id, out var reference)) + reference.Update(model); } - internal SocketGuildUser GetOrAddMember(ulong userId, ulong guildId, Func memberFactory) + public ValueTask AddOrUpdateAsync(TModel model) { - if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) - return member; + if (TryGetReference(model.Id, out var reference)) + reference.Update(model); + return _store.AddOrUpdateAsync(model, CacheRunMode.Async); + } - member = GetMember(userId, guildId); + public void Remove(TId id) + { + RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); + _references.TryRemove(id, out _); + } - if (member == null) - { - member ??= memberFactory(userId, guildId); - TrackMember(userId, guildId, member); - Task.Run(async () => await _state.AddOrUpdateMemberAsync(guildId, member)); // can run async, think of this as fire and forget. - } + public ValueTask RemoveAsync(TId id) + { + _references.TryRemove(id, out _); + return _store.RemoveAsync(id, CacheRunMode.Async); + } - return member; + public void Purge() + { + RunOrThrowValueTask(_store.PurgeAllAsync(CacheRunMode.Sync)); + _references.Clear(); } - internal IEnumerable GetMembers(ulong guildId) - => _state.GetMembersAsync(guildId, StateBehavior.SyncOnly).Result; + public ValueTask PurgeAsync() + { + _references.Clear(); + return _store.PurgeAllAsync(CacheRunMode.Async); + } + } - internal void AddOrUpdateMember(ulong guildId, SocketGuildUser user) - => EnsureSync(_state.AddOrUpdateMemberAsync(guildId, user)); + internal partial class ClientStateManager + { + public ReferenceStore UserStore; + public ReferenceStore PresenceStore; + private ConcurrentDictionary> _memberStores; + private ConcurrentDictionary> _threadMemberStores; - internal void RemoveMember(ulong userId, ulong guildId) - => EnsureSync(_state.RemoveMemberAsync(guildId, userId)); + private SemaphoreSlim _memberStoreLock; + private SemaphoreSlim _threadMemberLock; - #endregion + private void CreateStores() + { + UserStore = new ReferenceStore( + _cacheProvider, + m => SocketGlobalUser.Create(_client, m), + async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), + AllowSyncWaits); + + PresenceStore = new ReferenceStore( + _cacheProvider, + m => SocketPresence.Create(m), + (id, options) => Task.FromResult(null), + AllowSyncWaits); + + _memberStores = new(); + _threadMemberStores = new(); + + _threadMemberLock = new(1, 1); + _memberStoreLock = new(1,1); + } - #region Presence - internal void AddOrUpdatePresence(SocketPresence presence) + public void ClearDeadReferences() { - EnsureSync(_state.AddOrUpdatePresenseAsync(presence.UserId, presence, StateBehavior.SyncOnly)); + UserStore.ClearDeadReferences(); + PresenceStore.ClearDeadReferences(); } - internal SocketPresence GetPresence(ulong userId) + public async ValueTask InitializeAsync() { - if (_state.GetPresenceAsync(userId, StateBehavior.SyncOnly).Result is not SocketPresence socketPresence) - throw new NotSupportedException("Cannot use non-socket entity for presence"); + await UserStore.InitializeAsync(); + await PresenceStore.InitializeAsync(); + } + + public bool TryGetMemberStore(ulong guildId, out ReferenceStore store) + => _memberStores.TryGetValue(guildId, out store); - return socketPresence; + 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), + AllowSyncWaits); + + await store.InitializeAsync(guildId).ConfigureAwait(false); + + _memberStores.TryAdd(guildId, store); + return store; + } + finally + { + _memberStoreLock.Release(); + } + } + + 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), + AllowSyncWaits); + + await store.InitializeAsync().ConfigureAwait(false); + + _threadMemberStores.TryAdd(threadId, store); + return store; + } + finally + { + _threadMemberLock.Release(); + } } - #endregion } } diff --git a/src/Discord.Net.WebSocket/ClientStateManager.cs b/src/Discord.Net.WebSocket/ClientStateManager.cs index 1416e9cf92..d506387a71 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.cs @@ -30,11 +30,17 @@ internal partial class ClientStateManager _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); - private readonly IStateProvider _state; + internal bool AllowSyncWaits + => _client.AllowSynchronousWaiting; - public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCount) + private readonly ICacheProvider _cacheProvider; + private readonly DiscordSocketClient _client; + + + public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) { - _state = state; + _client = client; + _cacheProvider = client.CacheProvider; double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; _channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); @@ -43,6 +49,8 @@ public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCou _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) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 355dec0061..f482e3afd7 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -70,16 +70,17 @@ 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 ICacheProvider CacheProvider { get; private set; } internal ClientStateManager StateManager { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } - internal IStateProvider StateProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } internal bool AlwaysDownloadDefaultStickers { get; private set; } 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 => StateManager.Guilds; @@ -155,6 +156,8 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; + CacheProvider = config.CacheProvider ?? new DefaultConcurrentCacheProvider(); + AllowSynchronousWaiting = config.AllowSynchronousWaiting; Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _gatewayIntents = config.GatewayIntents; @@ -166,7 +169,6 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); - StateProvider = config.StateProvider ?? new DefaultStateProvider(_gatewayLogger, config.CacheProvider ?? new DefaultConcurrentCacheProvider(5, 50), this, config.DefaultStateBehavior); _nextAudioId = 1; _shardedClient = shardedClient; @@ -206,10 +208,14 @@ private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig co #region State public ValueTask GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) - => StateManager.GetUserAsync(id, cacheMode, options); + => StateManager.UserStore.GetAsync(id, cacheMode, options); public ValueTask GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) - => StateManager.GetMemberAsync(userId, guildId, cacheMode, options); + { + if (StateManager.TryGetMemberStore(guildId, out var store)) + return store.GetAsync(userId, cacheMode, options); + return ValueTask.FromResult(null); + } #endregion @@ -409,7 +415,7 @@ public async ValueTask GetChannelAsync(ulong id, RequestOptions option /// public override SocketUser GetUser(ulong id) - => StateManager.GetUser(id); + => StateManager.UserStore.Get(id); /// public override SocketUser GetUser(string username, string discriminator) => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); @@ -496,23 +502,18 @@ public async Task> BulkOverwriteGl 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(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(ClientStateManager state, ICurrentUserModel model) { - return state.GetOrAddUser(model.Id, x => - { - var user = SocketGlobalUser.Create(this, state, model); - user.GlobalUser.AddRef(); - return user; - }); + return state.UserStore.GetOrAdd(model.Id, x => model); } internal void RemoveUser(ulong id) - => StateManager.RemoveUser(id); + => StateManager.UserStore.Remove(id); /// public override async Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) @@ -689,7 +690,7 @@ private async Task SendStatusAsync() if (CurrentUser == null) return; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -813,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); } @@ -859,21 +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 ClientStateManager(StateProvider, data.Guilds.Length, data.PrivateChannels.Length); + + var state = new ClientStateManager(this, data.Guilds.Length, data.PrivateChannels.Length); StateManager = state; + await StateManager.InitializeAsync().ConfigureAwait(false); - var currentUser = SocketSelfUser.Create(this, state, data.User); + var currentUser = SocketSelfUser.Create(this, data.User); Rest.CreateRestSelfUser(data.User); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(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 @@ -950,6 +957,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (guild != null) { guild.Update(StateManager, data); + await guild.UpdateCacheAsync(data).ConfigureAwait(false); if (_unavailableGuildCount != 0) _unavailableGuildCount--; @@ -971,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, StateManager); + var guild = await AddGuildAsync(data).ConfigureAwait(false); if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); @@ -1290,13 +1298,13 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (user != null) { var before = user.Clone(); - if (user.GlobalUser.Update(StateManager, 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(StateManager, data); + user.Update(data); var cacheableBefore = new Cacheable(before, user.Id, true, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); @@ -1332,12 +1340,12 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - user ??= StateManager.GetUser(data.User.Id); + user ??= (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); if (user != null) - user.Update(StateManager, data.User); + user.Update(data.User); else - user = StateManager.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, StateManager, data.User)); + user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } @@ -1957,8 +1965,8 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } else { - var globalBefore = user.GlobalUser.Clone(); - if (user.GlobalUser.Update(StateManager, data.User)) + var globalBefore = user.GlobalUser.Value.Clone(); + if (user.GlobalUser.Value.Update(StateManager, data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); @@ -1978,7 +1986,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var before = user.Presence?.Value?.Clone(); user.Update(StateManager, data.User); var after = SocketPresence.Create(data); - StateManager.AddOrUpdatePresence(after); + StateManager.AddOrUpdatePresence(data); await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); } break; @@ -2324,7 +2332,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } SocketUser user = data.User.IsSpecified - ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, StateManager, data.User.Value)) + ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value) : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. SocketChannel channel = null; @@ -2579,9 +2587,9 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty 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); } @@ -2594,11 +2602,11 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var data = (payload as JToken).ToObject(_serializer); - var thread = (SocketThreadChannel)StateManager.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; } @@ -2948,10 +2956,11 @@ private async Task SyncGuildsAsync() await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } - internal SocketGuild AddGuild(ExtendedGuild model, ClientStateManager state) + internal async Task AddGuildAsync(ExtendedGuild model) { - var guild = SocketGuild.Create(this, state, model); - state.AddGuild(guild); + await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false); + var guild = SocketGuild.Create(this, StateManager, model); + StateManager.AddGuild(guild); if (model.Large) _largeGuilds.Enqueue(model.Id); return guild; @@ -2977,19 +2986,12 @@ internal SocketDMChannel CreateDMChannel(ulong channelId, SocketUser user, Clien internal ISocketPrivateChannel RemovePrivateChannel(ulong id) { var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; - if (channel != null) - { - foreach (var recipient in channel.Recipients) - recipient.GlobalUser.RemoveRef(this); - } return channel; } internal void RemoveDMChannels() { var channels = StateManager.DMChannels; StateManager.PurgeDMChannels(); - foreach (var channel in channels) - channel.Recipient.GlobalUser.RemoveRef(this); } internal void EnsureGatewayIntent(GatewayIntents intents) diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 21d84ba3b9..d4d10719b5 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -29,7 +29,12 @@ public class DiscordSocketConfig : DiscordRestConfig /// Gets or sets the cache provider to use /// public ICacheProvider CacheProvider { get; set; } - public IStateProvider StateProvider { 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. @@ -199,11 +204,6 @@ public int MaxWaitBetweenGuildAvailablesBeforeReady /// public bool SuppressUnknownDispatchWarnings { get; set; } = true; - /// - /// Gets or sets the default state behavior clients will use. - /// - public StateBehavior DefaultStateBehavior { get; set; } = StateBehavior.Default; - /// /// Initializes a new instance of the class with the default configuration. /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 1a16e8a253..5330f08262 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -452,17 +452,8 @@ internal void Update(ClientStateManager state, ExtendedModel model) } _events = events; - for (int i = 0; i < model.Members.Length; i++) - { - Discord.StateManager.AddOrUpdateMember(Id, SocketGuildUser.Create(Id, Discord, model.Members[i])); - } DownloadedMemberCount = model.Members.Length; - for (int i = 0; i < model.Presences.Length; i++) - { - Discord.StateManager.AddOrUpdatePresence(SocketPresence.Create(model.Presences[i])); - } - MemberCount = model.MemberCount; @@ -553,29 +544,12 @@ internal void Update(ClientStateManager state, Model model) else _stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 7); } - /*internal void Update(ClientStateManager state, GuildSyncModel model) //TODO remove? userbot related - { - 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; - - 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; - var _ = _syncPromise.TrySetResultAsync(true); - //if (!model.Large) - // _ = _downloaderPromise.TrySetResultAsync(true); - }*/ + internal async ValueTask UpdateCacheAsync(ExtendedModel model) + { + await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false); + await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false); + } internal void Update(ClientStateManager state, EmojiUpdateModel model) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 41eadcc4c8..80dce6170c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -12,45 +12,27 @@ internal class SocketGlobalUser : SocketUser, IDisposable public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal 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, ClientStateManager 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() => Discord.StateManager.RemoveReferencedGlobalUser(Id); + public override void Dispose() { - lock (_lockObj) - { - if (--_references <= 0) - discord.RemoveUser(Id); - } + GC.SuppressFinalize(this); + Discord.StateManager.RemoveReferencedGlobalUser(Id); } - ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); - public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id); - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 9d5fb0ef80..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 Lazy 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, ClientStateManager 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 2930139388..00593d8ba3 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -25,7 +25,6 @@ public class SocketGuildUser : SocketUser, IGuildUser, ICached, IDi private ImmutableArray _roleIds; private ulong _guildId; - internal override SocketGlobalUser GlobalUser { get; set; } /// /// Gets the guild the user is in. /// @@ -43,13 +42,13 @@ public class SocketGuildUser : SocketUser, IGuildUser, ICached, IDi /// 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.Value, this)); @@ -137,32 +136,29 @@ public int Hierarchy } } - internal SocketGuildUser(ulong guildId, SocketGlobalUser globalUser, DiscordSocketClient client) - : base(client, globalUser.Id) + internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client) + : base(client, userId) { _guildId = guildId; Guild = new Lazy(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); - GlobalUser = globalUser; } internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) { - var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, (Discord.API.User)model), client); - if (entity.Update(client.StateManager, model)) - client.StateManager.AddOrUpdateMember(guildId, entity); + var entity = new SocketGuildUser(guildId, model.Id, client); + if (entity.Update(model)) + client.StateManager.AddOrUpdateMember(guildId, entity.ToModel()); entity.UpdateRoles(Array.Empty()); return entity; } internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) { - var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, model.User), client); - entity.Update(client.StateManager, model); - client.StateManager.AddOrUpdateMember(guildId, entity); + var entity = new SocketGuildUser(guildId, model.Id, client); + entity.Update(model); + client.StateManager.AddOrUpdateMember(guildId, model); return entity; } - internal void Update(ClientStateManager state, MemberModel model) + internal void Update(MemberModel model) { - base.Update(state, model.User); - _joinedAtTicks = model.JoinedAt.UtcTicks; Nickname = model.Nickname; GuildAvatarId = model.GuildAvatar; @@ -234,12 +230,7 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; - internal new SocketGuildUser Clone() - { - var clone = MemberwiseClone() as SocketGuildUser; - clone.GlobalUser = GlobalUser.Clone(); - return clone; - } + internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; #endregion @@ -260,8 +251,7 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si private struct CacheModel : MemberModel { - public UserModel User { get; set; } - + public ulong Id { get; set; } public string Nickname { get; set; } public string GuildAvatar { get; set; } @@ -280,15 +270,14 @@ private struct CacheModel : MemberModel public DateTimeOffset? CommunicationsDisabledUntil { get; set; } } + internal new MemberModel ToModel() + => ToModel(); - MemberModel ICached.ToModel() - => ToMemberModel(); - - internal MemberModel ToMemberModel() + internal new TModel ToModel() where TModel : MemberModel, new() { - return new CacheModel + return new TModel { - User = ((ICached)this).ToModel(), + Id = Id, CommunicationsDisabledUntil = TimedOutUntil, GuildAvatar = GuildAvatarId, IsDeaf = IsDeafened, @@ -301,7 +290,19 @@ internal MemberModel ToMemberModel() }; } - public void Dispose() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); + MemberModel ICached.ToModel() + => ToModel(); + + TResult ICached.ToModel() + => ToModel(); + + void ICached.Update(MemberModel model) => Update(model); + + public override void Dispose() + { + GC.SuppressFinalize(this); + Discord.StateManager.RemovedReferencedMember(Id, _guildId); + } ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); #endregion diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 9e64bc2bb6..8e2464a225 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -114,6 +114,12 @@ private struct CacheModel : Model public ulong UserId { get; set; } public ulong? GuildId { get; set; } + + ulong IEntityModel.Id + { + get => UserId; + set => throw new NotSupportedException(); + } } private struct ActivityCacheModel : IActivityModel @@ -156,8 +162,11 @@ private struct EmojiCacheModel : IEmojiModel } internal Model ToModel() + => ToModel(); + + internal TModel ToModel() where TModel : Model, new() { - return new CacheModel + return new TModel { Status = Status, ActiveClients = ActiveClients.ToArray(), @@ -194,6 +203,8 @@ internal Model ToModel() } Model ICached.ToModel() => ToModel(); + TResult ICached.ToModel() => ToModel(); + void ICached.Update(Model model) => Update(model); #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 45b3ebc4f0..cc6bcd912e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -19,18 +19,17 @@ public class SocketSelfUser : SocketUser, ISelfUser, ICached 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 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; } } /// - internal override Lazy Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + internal override Lazy Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } } /// public UserProperties Flags { get; internal set; } /// @@ -41,20 +40,20 @@ public class SocketSelfUser : SocketUser, ISelfUser, ICached /// 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, ClientStateManager 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(ClientStateManager state, UserModel model) + internal override bool Update(UserModel model) { - bool hasGlobalChanges = base.Update(state, model); + bool hasGlobalChanges = base.Update(model); if (model is not Model currentUserModel) throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); @@ -98,9 +97,13 @@ 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() + { + GC.SuppressFinalize(this); + Discord.StateManager.RemoveReferencedGlobalUser(Id); + } #region Cache - private struct CacheModel : Model { public bool? IsVerified { get; set; } @@ -128,9 +131,12 @@ private struct CacheModel : Model public ulong Id { get; set; } } - Model ICached.ToModel() + internal new Model ToModel() + => ToModel(); + + internal new TModel ToModel() where TModel : Model, new() { - return new CacheModel + return new TModel { Avatar = AvatarId, Discriminator = Discriminator, @@ -147,6 +153,9 @@ Model ICached.ToModel() }; } + Model ICached.ToModel() => ToModel(); + TResult 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 e42805e4ee..a00a78a4aa 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.UserId.Value); + 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.UserId.Value); entity.Update(model); return entity; } @@ -150,89 +166,117 @@ 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 Lazy Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } - internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } + public override void Dispose() + { + GC.SuppressFinalize(this); + } - internal override Lazy 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 + private class CacheModel : Model + { + public ulong? ThreadId { get; set; } + public ulong? UserId { get; set; } + public DateTimeOffset JoinedAt { get; set; } + + ulong IEntityModel.Id { get => UserId.GetValueOrDefault(); set => throw new NotSupportedException(); } + } + + internal new Model ToModel() => ToModel(); + + internal new TModel ToModel() where TModel : Model, new() + { + return new TModel + { + JoinedAt = ThreadJoinedAt, + ThreadId = _threadId, + UserId = Id + }; + } + + Model ICached.ToModel() => ToModel(); + TResult 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 5d2ddef327..151f00b72b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -27,21 +27,21 @@ public class SocketUnknownUser : SocketUser public override bool IsWebhook => false; /// internal override Lazy Presence { get { return new Lazy(() => 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 Lazy GlobalUser { get => new Lazy(() => null); set { } } internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientStateManager 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 d61fe8ea75..0495c5118c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -18,18 +18,18 @@ namespace Discord.WebSocket public abstract class SocketUser : SocketEntity, IUser, ICached, IDisposable { /// - 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 virtual Lazy GlobalUser { get; set; } internal virtual Lazy Presence { get; set; } /// @@ -57,9 +57,10 @@ internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal virtual bool Update(ClientStateManager state, Model model) + internal virtual bool Update(Model model) { - Presence ??= new Lazy(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); + Presence ??= new Lazy(() => Discord.StateManager.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); + GlobalUser ??= new Lazy(() => Discord.StateManager.GetUser(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); bool hasChanges = false; if (model.Avatar != AvatarId) { @@ -98,6 +99,8 @@ internal virtual bool Update(ClientStateManager state, Model model) return hasChanges; } + public abstract void Dispose(); + /// public async Task CreateDMChannelAsync(RequestOptions options = null) => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); @@ -117,8 +120,6 @@ public string GetDefaultAvatarUrl() /// The full name of the user. /// public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); - ~SocketUser() => GlobalUser?.Dispose(); - public void Dispose() => GlobalUser?.Dispose(); private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; @@ -136,12 +137,9 @@ private struct CacheModel : Model public ulong Id { get; set; } } - Model ICached.ToModel() - => ToModel(); - - internal Model ToModel() + internal TModel ToModel() where TModel : Model, new() { - return new CacheModel + return new TModel { Avatar = AvatarId, Discriminator = Discriminator, @@ -151,6 +149,17 @@ internal Model ToModel() }; } + internal Model ToModel() + => ToModel(); + + Model ICached.ToModel() + => ToModel(); + + TResult ICached.ToModel() + => ToModel(); + + void ICached.Update(Model model) => Update(model); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 06f9a8ab5b..bd3c9fd5e6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -34,7 +34,7 @@ public class SocketWebhookUser : SocketUser, IWebhookUser public override bool IsWebhook => true; /// internal override Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } - internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + internal override Lazy GlobalUser { get => new Lazy(() => 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, ClientStateManager 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/StateExtensions.cs b/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs deleted file mode 100644 index 7719b26c11..0000000000 --- a/src/Discord.Net.WebSocket/Extensions/StateExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - internal static class StateExtensions - { - public static StateBehavior ToBehavior(this CacheMode mode) - { - return mode switch - { - CacheMode.AllowDownload => StateBehavior.AllowDownload, - CacheMode.CacheOnly => StateBehavior.CacheOnly, - _ => StateBehavior.AllowDownload - }; - } - } -} diff --git a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs b/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs deleted file mode 100644 index a95227d347..0000000000 --- a/src/Discord.Net.WebSocket/State/DefaultStateProvider.cs +++ /dev/null @@ -1,252 +0,0 @@ -using Discord.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - internal class DefaultStateProvider : IStateProvider - { - private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 - private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 - private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth - - private readonly ICacheProvider _cache; - private readonly StateBehavior _defaultBehavior; - private readonly DiscordSocketClient _client; - private readonly Logger _logger; - public DefaultStateProvider(Logger logger, ICacheProvider cacheProvider, DiscordSocketClient client, StateBehavior stateBehavior) - { - _cache = cacheProvider; - _client = client; - _logger = logger; - - if (stateBehavior == StateBehavior.Default) - throw new ArgumentException("Cannot use \"default\" as the default state behavior"); - - _defaultBehavior = stateBehavior; - } - - private void RunAsyncWithLogs(ValueTask task) - { - _ = Task.Run(async () => - { - try - { - await task.ConfigureAwait(false); - } - catch (Exception x) - { - await _logger.ErrorAsync("Cache provider failed", x).ConfigureAwait(false); - } - }); - } - - private TType ValidateAsSocketEntity(ISnowflakeEntity entity) where TType : SocketEntity - { - if(entity is not TType val) - throw new NotSupportedException("Cannot cache non-socket entities"); - return val; - } - - private StateBehavior ResolveBehavior(StateBehavior behavior) - => behavior == StateBehavior.Default ? _defaultBehavior : behavior; - - public ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user) - { - var socketGuildUser = ValidateAsSocketEntity(user); - var model = socketGuildUser.ToMemberModel(); - RunAsyncWithLogs(_cache.AddOrUpdateMemberAsync(model, guildId, CacheRunMode.Async)); - return default; - } - public ValueTask AddOrUpdateUserAsync(IUser user) - { - var socketUser = ValidateAsSocketEntity(user); - var model = socketUser.ToModel(); - RunAsyncWithLogs(_cache.AddOrUpdateUserAsync(model, CacheRunMode.Async)); - return default; - } - public ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(behavior != StateBehavior.DownloadOnly) - { - var memberLookupTask = _cache.GetMemberAsync(id, guildId, cacheMode); - - if (memberLookupTask.IsCompleted) - { - var model = memberLookupTask.Result; - if(model != null) - return new ValueTask(SocketGuildUser.Create(guildId, _client, model)); - } - else - { - return new ValueTask(Task.Run(async () => // review: task.run here? - { - var result = await memberLookupTask; - - if (result != null) - return (IGuildUser)SocketGuildUser.Create(guildId, _client, result); - else if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false); - return null; - })); - } - } - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return new ValueTask(_client.Rest.GetGuildUserAsync(guildId, id, options).ContinueWith(x => (IGuildUser)x.Result)); - - return default; - } - - public ValueTask> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(behavior != StateBehavior.DownloadOnly) - { - var memberLookupTask = _cache.GetMembersAsync(guildId, cacheMode); - - if (memberLookupTask.IsCompleted) - return new ValueTask>(memberLookupTask.Result?.Select(x => SocketGuildUser.Create(guildId, _client, x))); - else - { - return new ValueTask>(Task.Run(async () => - { - var result = await memberLookupTask; - - if (result != null && result.Any()) - return result.Select(x => (IGuildUser)SocketGuildUser.Create(guildId, _client, x)); - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return await _client.Rest.GetGuildUsersAsync(guildId, options); - - return null; - })); - } - } - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return new ValueTask>(_client.Rest.GetGuildUsersAsync(guildId, options).ContinueWith(x => x.Result.Cast())); - - return default; - } - - public ValueTask GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if (behavior != StateBehavior.DownloadOnly) - { - var userLookupTask = _cache.GetUserAsync(id, cacheMode); - - if (userLookupTask.IsCompleted) - { - var model = userLookupTask.Result; - if(model != null) - return new ValueTask(SocketGlobalUser.Create(_client, null, model)); - } - else - { - return new ValueTask(Task.Run(async () => - { - var result = await userLookupTask; - - if (result != null) - return SocketGlobalUser.Create(_client, null, result); - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return await _client.Rest.GetUserAsync(id, options); - - return null; - })); - } - } - - if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) - return new ValueTask(_client.Rest.GetUserAsync(id, options).ContinueWith(x => (IUser)x.Result)); - - return default; - } - - public ValueTask> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(behavior != StateBehavior.DownloadOnly) - { - var usersTask = _cache.GetUsersAsync(cacheMode); - - if (usersTask.IsCompleted) - return new ValueTask>(usersTask.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x))); - else - { - return new ValueTask>(usersTask.AsTask().ContinueWith(x => x.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x)))); - } - } - - // no download path - return default; - } - - public ValueTask RemoveMemberAsync(ulong id, ulong guildId) - => _cache.RemoveMemberAsync(id, guildId, CacheRunMode.Async); - public ValueTask RemoveUserAsync(ulong id) - => _cache.RemoveUserAsync(id, CacheRunMode.Async); - - public ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBehavior) - { - var behavior = ResolveBehavior(stateBehavior); - - var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; - - if(stateBehavior != StateBehavior.DownloadOnly) - { - var fetchTask = _cache.GetPresenceAsync(userId, cacheMode); - - if (fetchTask.IsCompleted) - return new ValueTask(SocketPresence.Create(fetchTask.Result)); - else - { - return new ValueTask(Task.Run(async () => - { - var result = await fetchTask; - - if(result != null) - return (IPresence)SocketPresence.Create(result); - return null; - })); - } - } - - // no download path - return new ValueTask((IPresence)null); - } - - public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior) - { - if (presense is not SocketPresence socketPresense) - throw new ArgumentException($"Expected socket entity but got {presense?.GetType()}"); - - var model = socketPresense.ToModel(); - - RunAsyncWithLogs(_cache.AddOrUpdatePresenseAsync(userId, model, CacheRunMode.Async)); - return default; - } - public ValueTask RemovePresenseAsync(ulong userId) - => _cache.RemovePresenseAsync(userId, CacheRunMode.Async); - } -} diff --git a/src/Discord.Net.WebSocket/State/IStateProvider.cs b/src/Discord.Net.WebSocket/State/IStateProvider.cs deleted file mode 100644 index c944d9f199..0000000000 --- a/src/Discord.Net.WebSocket/State/IStateProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - public interface IStateProvider - { - ValueTask GetPresenceAsync(ulong userId, StateBehavior stateBehavior); - ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior); - ValueTask RemovePresenseAsync(ulong userId); - - ValueTask GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null); - ValueTask> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null); - ValueTask AddOrUpdateUserAsync(IUser user); - ValueTask RemoveUserAsync(ulong id); - - ValueTask GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null); - ValueTask> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null); - ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user); - ValueTask RemoveMemberAsync(ulong guildId, ulong id); - } -} diff --git a/src/Discord.Net.WebSocket/State/StateBehavior.cs b/src/Discord.Net.WebSocket/State/StateBehavior.cs deleted file mode 100644 index 4a387d5a9e..0000000000 --- a/src/Discord.Net.WebSocket/State/StateBehavior.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - public enum StateBehavior - { - /// - /// Use the default Cache Behavior of the client. - /// - /// - Default = 0, - /// - /// The entity will only be retrieved via a synchronous cache lookup. - /// - /// For the default , this is equivalent to using - /// - /// - /// This flag is used to indicate that the retrieval of this entity should not leave the - /// synchronous path of the . When true, - /// the calling method *should* not ever leave the calling task, and never generate an async - /// state machine. - /// - /// Bear in mind that the true behavior of this flag depends entirely on the to - /// abide by design implications of this flag. Once Discord.Net has called out to the state provider with this - /// flag, it is out of our control whether or not an async method is evaluated. - /// - SyncOnly = 1, - /// - /// The entity will only be retrieved via a cache lookup - the Discord API will not be contacted to retrieve the entity. - /// - /// - /// When using an alternative , usage of this flag implies that it is - /// okay for the state provider to make an external call if the local cache missed the entity. - /// - /// Note that when designing an , this flag does not imply that the state - /// provider itself should contact Discord for the entity; rather that if using a dual-layer caching system, - /// it would be okay to contact an external layer, e.g. Redis, for the entity. - /// - CacheOnly = 2, - /// - /// The entity will be downloaded from the Discord REST API if the on hand cannot locate it. - /// - AllowDownload = 3, - /// - /// The entity will be downloaded from the Discord REST API. The local will not be contacted to find the entity. - /// - DownloadOnly = 4 - } -} From d89d13d7030878eed9ca97bd1cdaf78e81165fe4 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Sun, 24 Apr 2022 06:24:35 -0300 Subject: [PATCH 05/11] Final POC for users --- src/Discord.Net.Core/Cache/ICached.cs | 7 +- src/Discord.Net.WebSocket/Cache/LazyCached.cs | 76 +++++++++++++++++++ .../ClientStateManager.Experiment.cs | 70 +++++++++++++---- .../DiscordSocketClient.cs | 38 +++++----- .../Entities/Channels/SocketDMChannel.cs | 4 +- .../Entities/Channels/SocketGroupChannel.cs | 6 +- .../Entities/Channels/SocketThreadChannel.cs | 1 - .../Entities/Guilds/SocketGuild.cs | 32 ++++---- .../Entities/Guilds/SocketGuildEvent.cs | 2 +- .../SocketMessageComponent.cs | 2 +- .../SocketBaseCommand/SocketResolvableData.cs | 2 +- .../Entities/Messages/SocketMessage.cs | 4 +- .../Entities/Messages/SocketUserMessage.cs | 4 +- .../Entities/Users/SocketGlobalUser.cs | 4 +- .../Entities/Users/SocketGuildUser.cs | 10 +-- .../Entities/Users/SocketPresence.cs | 34 ++++++++- .../Entities/Users/SocketSelfUser.cs | 17 ++--- .../Entities/Users/SocketThreadUser.cs | 2 +- .../Entities/Users/SocketUnknownUser.cs | 4 +- .../Entities/Users/SocketUser.cs | 16 ++-- .../Entities/Users/SocketWebhookUser.cs | 4 +- 21 files changed, 241 insertions(+), 98 deletions(-) create mode 100644 src/Discord.Net.WebSocket/Cache/LazyCached.cs diff --git a/src/Discord.Net.Core/Cache/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs index 6f7a99bfc6..19445598fd 100644 --- a/src/Discord.Net.Core/Cache/ICached.cs +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -6,7 +6,7 @@ namespace Discord { - internal interface ICached + internal interface ICached : ICached, IDisposable { void Update(TType model); @@ -14,4 +14,9 @@ internal interface ICached TResult ToModel() where TResult : TType, new(); } + + public interface ICached + { + bool IsFreed { get; } + } } 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 index f657e526db..ccbcc2f772 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -19,8 +19,6 @@ public bool CanRelease private int _referenceCount; - private readonly object _lock = new object(); - public CacheReference(TType value) { Reference = new(value); @@ -39,28 +37,31 @@ public bool TryObtainReference(out TType reference) public void ReleaseReference() { - lock (_lock) - { - if (_referenceCount > 0) - _referenceCount--; - } + Interlocked.Decrement(ref _referenceCount); } } - internal class ReferenceStore - where TEntity : class, ICached, ISharedEntity + + internal interface ILookupReferenceStore + { + TEntity Get(TId id); + ValueTask GetAsync(TId id); + } + + internal class ReferenceStore : ILookupReferenceStore + where TEntity : class, ICached, TSharedEntity where TModel : IEntityModel where TId : IEquatable - where ISharedEntity : class + where TSharedEntity : class { private readonly ICacheProvider _cacheProvider; private readonly ConcurrentDictionary> _references = new(); private IEntityStore _store; private Func _entityBuilder; - private Func> _restLookup; + private Func> _restLookup; private readonly bool _allowSyncWaits; private readonly object _lock = new(); - public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) + public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) { _allowSyncWaits = allowSyncWaits; _cacheProvider = cacheProvider; @@ -68,6 +69,19 @@ public ReferenceStore(ICacheProvider cacheProvider, Func entity _restLookup = restLookup; } + 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) @@ -135,7 +149,7 @@ public TEntity Get(TId id) return null; } - public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) + public async ValueTask GetAsync(TId id, CacheMode mode, RequestOptions options = null) { if (TryGetReference(id, out var entity)) { @@ -216,6 +230,28 @@ public ValueTask AddOrUpdateAsync(TModel model) return _store.AddOrUpdateAsync(model, CacheRunMode.Async); } + public void BulkAddOrUpdate(IEnumerable models) + { + RunOrThrowValueTask(_store.AddOrUpdateBatchAsync(models, CacheRunMode.Sync)); + + 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) + { + await _store.AddOrUpdateBatchAsync(models, CacheRunMode.Async).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) { RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); @@ -239,6 +275,9 @@ public ValueTask PurgeAsync() _references.Clear(); return _store.PurgeAllAsync(CacheRunMode.Async); } + + 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 @@ -261,7 +300,7 @@ private void CreateStores() PresenceStore = new ReferenceStore( _cacheProvider, - m => SocketPresence.Create(m), + m => SocketPresence.Create(_client, m), (id, options) => Task.FromResult(null), AllowSyncWaits); @@ -284,6 +323,9 @@ public async ValueTask InitializeAsync() await PresenceStore.InitializeAsync(); } + 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); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f482e3afd7..a7a0372433 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -214,7 +214,7 @@ public ValueTask GetGuildUserAsync(ulong userId, ulong guildId, Cach { if (StateManager.TryGetMemberStore(guildId, out var store)) return store.GetAsync(userId, cacheMode, options); - return ValueTask.FromResult(null); + return new ValueTask((IGuildUser)null); } #endregion @@ -690,7 +690,7 @@ private async Task SendStatusAsync() if (CurrentUser == null) return; var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -870,7 +870,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty Rest.CreateRestSelfUser(data.User); var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; - await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); + await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false); ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentApplicationId = data.Application.Id; @@ -1345,7 +1345,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (user != null) user.Update(data.User); else - user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User); + user = await StateManager.UserStore.GetOrAddAsync(data.User.Id, _ => data.User).ConfigureAwait(false); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); } @@ -1560,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, StateManager, data.User); + user = SocketUnknownUser.Create(this, data.User); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else @@ -1584,9 +1584,9 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - SocketUser user = StateManager.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, StateManager, data.User); + user = SocketUnknownUser.Create(this, data.User); await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); } else @@ -1630,7 +1630,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (guild != null) { if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, StateManager, data.Author.Value, data.WebhookId.Value); + author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1695,7 +1695,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (guild != null) { if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, StateManager, data.Author.Value, data.WebhookId.Value); + author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value); else author = guild.GetUser(data.Author.Value.Id); } @@ -1966,7 +1966,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty else { var globalBefore = user.GlobalUser.Value.Clone(); - if (user.GlobalUser.Value.Update(StateManager, data.User)) + if (user.GlobalUser.Value.Update(data.User)) { //Global data was updated, trigger UserUpdated await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); @@ -1975,7 +1975,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } else { - user = StateManager.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); @@ -1984,9 +1984,9 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } var before = user.Presence?.Value?.Clone(); - user.Update(StateManager, data.User); - var after = SocketPresence.Create(data); - StateManager.AddOrUpdatePresence(data); + 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; @@ -2114,7 +2114,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty if (data.Id == CurrentUser.Id) { var before = CurrentUser.Clone(); - CurrentUser.Update(StateManager, data); + CurrentUser.Update(data); await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else @@ -2277,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, StateManager, 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); @@ -2332,7 +2332,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty } SocketUser user = data.User.IsSpecified - ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value) + ? 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; @@ -2821,7 +2821,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - var user = (SocketUser)guild.GetUser(data.UserId) ?? StateManager.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)); @@ -2958,7 +2958,7 @@ private async Task SyncGuildsAsync() internal async Task AddGuildAsync(ExtendedGuild model) { - await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false); + await StateManager.GetMemberStoreAsync(model.Id).ConfigureAwait(false); var guild = SocketGuild.Create(this, StateManager, model); StateManager.AddGuild(guild); if (model.Large) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 755fa7ab34..e3c56b987e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -43,7 +43,7 @@ internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateM } 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, ClientStateManager state, ulong channelId, API.User recipient) { @@ -53,7 +53,7 @@ internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateM } 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 f6736245d5..b5885c1d97 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -77,7 +77,7 @@ 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.StateManager, 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; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 4ff39e5e54..3fbf9d6e42 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -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/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 5330f08262..13da7cce70 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -305,7 +305,7 @@ public IReadOnlyCollection ThreadChannels /// /// Gets the current logged-in user. /// - public SocketGuildUser CurrentUser => Discord.StateManager.GetMember(Discord.CurrentUser.Id, Id); + 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. /// @@ -356,7 +356,7 @@ public IReadOnlyCollection Stickers /// /// A collection of guild users found within this guild. /// - public IReadOnlyCollection Users => Discord.StateManager.GetMembers(Id).Cast().ToImmutableArray(); + public IReadOnlyCollection Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray.Empty; /// /// Gets a collection of all roles in this guild. /// @@ -547,8 +547,12 @@ internal void Update(ClientStateManager state, Model model) internal async ValueTask UpdateCacheAsync(ExtendedModel model) { - await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false); - await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false); + await Discord.StateManager.PresenceStore.BulkAddOrUpdateAsync(model.Presences); + + await Discord.StateManager.UserStore.BulkAddOrUpdateAsync(model.Members.Select(x => x.User)); + + if(Discord.StateManager.TryGetMemberStore(Id, out var store)) + store.BulkAddOrUpdate(model.Members); } internal void Update(ClientStateManager state, EmojiUpdateModel model) @@ -1055,7 +1059,7 @@ public Task AddGuildUserAsync(ulong id, string accessToken, Actio /// A guild user associated with the specified ; if none is found. /// public SocketGuildUser GetUser(ulong id) - => Discord.StateManager.GetMember(id, Id); + => 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); @@ -1064,11 +1068,10 @@ internal SocketGuildUser AddOrUpdateUser(UserModel model) { SocketGuildUser member; if ((member = GetUser(model.Id)) != null) - member.GlobalUser?.Update(Discord.StateManager, model); + member.Update(model); else { member = SocketGuildUser.Create(Id, Discord, model); - member.GlobalUser.AddRef(); DownloadedMemberCount++; } return member; @@ -1076,12 +1079,11 @@ internal SocketGuildUser AddOrUpdateUser(UserModel model) internal SocketGuildUser AddOrUpdateUser(MemberModel model) { SocketGuildUser member; - if ((member = GetUser(model.User.Id)) != null) - member.Update(Discord.StateManager, model); + if ((member = GetUser(model.Id)) != null) + member.Update(model); else { member = SocketGuildUser.Create(Id, Discord, model); - member.GlobalUser.AddRef(); DownloadedMemberCount++; } return member; @@ -1092,8 +1094,8 @@ internal SocketGuildUser RemoveUser(ulong id) if ((member = GetUser(id)) != null) { DownloadedMemberCount--; - member.GlobalUser.RemoveRef(Discord); - Discord.StateManager.RemoveMember(id, Id); + if (Discord.StateManager.TryGetMemberStore(Id, out var store)) + store.Remove(id); return member; } return null; @@ -1114,8 +1116,9 @@ 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); - foreach (var member in membersToPurge) - Discord.StateManager.RemoveMember(member.Id, Id); + if(Discord.StateManager.TryGetMemberStore(Id, out var store)) + foreach (var member in membersToPurge) + store.Remove(member.Id); _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = membersToKeep.Count(); @@ -1240,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. diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs index 9f019cdb1e..bdf89d1abe 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -89,7 +89,7 @@ internal void Update(Model model) if(guildUser != null) { if(model.Creator.IsSpecified) - guildUser.Update(Discord.StateManager, model.Creator.Value); + guildUser.Update(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 28a922e650..d019a87a9b 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -56,7 +56,7 @@ internal override void Update(Model model) if (Channel is SocketGuildChannel channel) { if (model.Message.Value.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(channel.Guild, Discord.StateManager, 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); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index d369607497..e8687dab30 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -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.StateManager, 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); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 51a691b6f3..c7bc5e873e 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -251,7 +251,7 @@ internal virtual void Update(ClientStateManager state, Model model) if (user != null) newMentions.Add(user); else - newMentions.Add(SocketUnknownUser.Create(Discord, state, val)); + newMentions.Add(SocketUnknownUser.Create(Discord, val)); } } _userMentions = newMentions.ToImmutable(); @@ -263,7 +263,7 @@ internal virtual void Update(ClientStateManager state, Model model) Interaction = new MessageInteraction(model.Interaction.Value.Id, model.Interaction.Value.Type, model.Interaction.Value.Name, - SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User)); + SocketGlobalUser.Create(Discord, model.Interaction.Value.User)); } if (model.Flags.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 94c081d75b..4c8e944322 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -122,14 +122,14 @@ internal override void Update(ClientStateManager state, Model model) if (guild != null) { if (webhookId != null) - refMsgAuthor = SocketWebhookUser.Create(guild, state, refMsg.Author.Value, webhookId.Value); + refMsgAuthor = SocketWebhookUser.Create(guild, refMsg.Author.Value, webhookId.Value); else refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); } else refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); if (refMsgAuthor == null) - refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); + refMsgAuthor = SocketUnknownUser.Create(Discord, refMsg.Author.Value); } else // Message author wasn't specified in the payload, so create a completely anonymous unknown user diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 80dce6170c..1187a235a7 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -26,11 +26,11 @@ internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model return entity; } - ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); + ~SocketGlobalUser() => Dispose(); public override void Dispose() { GC.SuppressFinalize(this); - Discord.StateManager.RemoveReferencedGlobalUser(Id); + Discord.StateManager.UserStore.RemoveReference(Id); } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 00593d8ba3..42169f5ada 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -28,7 +28,7 @@ public class SocketGuildUser : SocketUser, IGuildUser, ICached, IDi /// /// Gets the guild the user is in. /// - public Lazy Guild { get; } + public Lazy Guild { get; } // TODO: convert to LazyCached once guilds are cached. /// /// Gets the guilds id that the user is in. /// @@ -146,7 +146,7 @@ internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client { var entity = new SocketGuildUser(guildId, model.Id, client); if (entity.Update(model)) - client.StateManager.AddOrUpdateMember(guildId, entity.ToModel()); + client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(entity.ToModel()); entity.UpdateRoles(Array.Empty()); return entity; } @@ -154,7 +154,7 @@ internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client { var entity = new SocketGuildUser(guildId, model.Id, client); entity.Update(model); - client.StateManager.AddOrUpdateMember(guildId, model); + client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(model); return entity; } internal void Update(MemberModel model) @@ -301,9 +301,9 @@ TResult ICached.ToModel() public override void Dispose() { GC.SuppressFinalize(this); - Discord.StateManager.RemovedReferencedMember(Id, _guildId); + Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); } - ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); + ~SocketGuildUser() => Dispose(); #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 8e2464a225..aa02204533 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -15,6 +15,8 @@ public class SocketPresence : IPresence, ICached { internal ulong UserId; internal ulong? GuildId; + internal bool IsFreed; + internal DiscordSocketClient Discord; /// public UserStatus Status { get; private set; } @@ -23,17 +25,24 @@ public class SocketPresence : IPresence, ICached /// 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; } @@ -102,6 +111,22 @@ private static IImmutableList ConvertActivitiesList(IActivityModel[] 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 private struct CacheModel : Model { @@ -205,6 +230,7 @@ internal Model ToModel() Model ICached.ToModel() => ToModel(); TResult 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 cc6bcd912e..1d1c3bb6c4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -19,17 +19,6 @@ public class SocketSelfUser : SocketUser, ISelfUser, ICached public bool IsVerified { get; private set; } /// public bool IsMfaEnabled { get; private set; } - - /// - public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } - /// - public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } - /// - public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } - /// - public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } - /// - internal override Lazy Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } } /// public UserProperties Flags { get; internal set; } /// @@ -99,8 +88,12 @@ public Task ModifyAsync(Action func, RequestOptions options internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; public override void Dispose() { + if (IsFreed) + return; + GC.SuppressFinalize(this); - Discord.StateManager.RemoveReferencedGlobalUser(Id); + Discord.StateManager.UserStore.RemoveReference(Id); + IsFreed = true; } #region Cache diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index a00a78a4aa..48aebc92e5 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -238,7 +238,7 @@ internal void Update(Model model) /// string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); - internal override Lazy Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } + internal override LazyCached Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } public override void Dispose() { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 151f00b72b..6da6160fd6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -26,8 +26,8 @@ public class SocketUnknownUser : SocketUser /// public override bool IsWebhook => false; /// - internal override Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } - internal override Lazy GlobalUser { get => new Lazy(() => null); set { } } + 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) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 0495c5118c..43b3b0e54d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket /// Represents a WebSocket-based user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public abstract class SocketUser : SocketEntity, IUser, ICached, IDisposable + public abstract class SocketUser : SocketEntity, IUser, ICached { /// public virtual bool IsBot { get; internal set; } @@ -29,9 +29,9 @@ public abstract class SocketUser : SocketEntity, IUser, ICached, I public virtual bool IsWebhook { get; } /// public UserProperties? PublicFlags { get; private set; } - internal virtual Lazy GlobalUser { get; set; } - internal virtual Lazy 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); /// @@ -56,11 +56,11 @@ 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(Model model) { - Presence ??= new Lazy(() => Discord.StateManager.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); - GlobalUser ??= new Lazy(() => Discord.StateManager.GetUser(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); bool hasChanges = false; if (model.Avatar != AvatarId) { @@ -124,7 +124,7 @@ public string GetDefaultAvatarUrl() internal SocketUser Clone() => MemberwiseClone() as SocketUser; #region Cache - private struct CacheModel : Model + private class CacheModel : Model { public string Username { get; set; } @@ -160,6 +160,8 @@ TResult ICached.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 bd3c9fd5e6..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 Lazy Presence { get { return new Lazy(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } - internal override Lazy GlobalUser { get => new Lazy(() => null); set { } } + 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) From a4e1f54d6ec19395fd43eaff4ee6d59b0393362f Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 24 Apr 2022 10:06:04 -0300 Subject: [PATCH 06/11] Update src/Discord.Net.WebSocket/DiscordSocketConfig.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- src/Discord.Net.WebSocket/DiscordSocketConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index d4d10719b5..9804587151 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -26,7 +26,7 @@ namespace Discord.WebSocket public class DiscordSocketConfig : DiscordRestConfig { /// - /// Gets or sets the cache provider to use + /// Gets or sets the cache provider to use. /// public ICacheProvider CacheProvider { get; set; } From 9826cf699ff3f991a82b06591e1b722b798d1a74 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 24 Apr 2022 10:06:11 -0300 Subject: [PATCH 07/11] Update src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 42169f5ada..9b25eb7412 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -30,7 +30,7 @@ public class SocketGuildUser : SocketUser, IGuildUser, ICached, IDi /// public Lazy Guild { get; } // TODO: convert to LazyCached once guilds are cached. /// - /// Gets the guilds id that the user is in. + /// Gets the ID of the guild that the user is in. /// public ulong GuildId => _guildId; /// From 59c334ac60ef7f44304a037e8b8ffbde7c5e8006 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Mon, 25 Apr 2022 14:03:01 -0300 Subject: [PATCH 08/11] refactor models and remove cache run mode --- src/Discord.Net.Core/Cache/ICached.cs | 2 - .../Cache/Models/Users/IMemberModel.cs | 2 +- .../Cache/Models/Users/IThreadMemberModel.cs | 1 - .../API/Common/GuildMember.cs | 4 +- .../API/Common/ThreadMember.cs | 1 - .../Cache/CacheRunMode.cs | 21 ----- .../Cache/DefaultConcurrentCacheProvider.cs | 76 ++++++++++++++----- .../Cache/ICacheProvider.cs | 20 +++-- .../ClientStateManager.Experiment.cs | 70 ++++++++--------- .../Entities/Users/SocketGuildUser.cs | 57 +++++++------- .../Entities/Users/SocketPresence.cs | 70 ++++++++--------- .../Entities/Users/SocketSelfUser.cs | 34 ++++----- .../Entities/Users/SocketThreadUser.cs | 29 ++++--- .../Entities/Users/SocketUser.cs | 25 ++---- 14 files changed, 201 insertions(+), 211 deletions(-) delete mode 100644 src/Discord.Net.WebSocket/Cache/CacheRunMode.cs diff --git a/src/Discord.Net.Core/Cache/ICached.cs b/src/Discord.Net.Core/Cache/ICached.cs index 19445598fd..955b170172 100644 --- a/src/Discord.Net.Core/Cache/ICached.cs +++ b/src/Discord.Net.Core/Cache/ICached.cs @@ -11,8 +11,6 @@ internal interface ICached : ICached, IDisposable void Update(TType model); TType ToModel(); - - TResult ToModel() where TResult : TType, new(); } public interface ICached diff --git a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs index a6daa66d0b..f13920c945 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs @@ -12,7 +12,7 @@ public interface IMemberModel : IEntityModel string Nickname { get; set; } string GuildAvatar { get; set; } ulong[] Roles { get; set; } - DateTimeOffset JoinedAt { get; set; } + DateTimeOffset? JoinedAt { get; set; } DateTimeOffset? PremiumSince { get; set; } bool IsDeaf { get; set; } bool IsMute { get; set; } diff --git a/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs index b8ad311fb5..12a52f5749 100644 --- a/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs @@ -9,7 +9,6 @@ namespace Discord public interface IThreadMemberModel : IEntityModel { ulong? ThreadId { get; set; } - ulong? UserId { get; set; } DateTimeOffset JoinedAt { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 8a373fbab7..e2321a3b1e 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -39,8 +39,8 @@ ulong[] IMemberModel.Roles { get => Roles.GetValueOrDefault(Array.Empty()); set => throw new NotSupportedException(); } - DateTimeOffset IMemberModel.JoinedAt { - get => JoinedAt.GetValueOrDefault(); set => throw new NotSupportedException(); + DateTimeOffset? IMemberModel.JoinedAt { + get => JoinedAt.ToNullable(); set => throw new NotSupportedException(); } DateTimeOffset? IMemberModel.PremiumSince { diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs index 11531c77fc..10172cc70d 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMember.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -15,7 +15,6 @@ internal class ThreadMember : IThreadMemberModel public DateTimeOffset JoinTimestamp { get; set; } ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } - ulong? IThreadMemberModel.UserId { get => UserId.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.WebSocket/Cache/CacheRunMode.cs b/src/Discord.Net.WebSocket/Cache/CacheRunMode.cs deleted file mode 100644 index f53719e06c..0000000000 --- a/src/Discord.Net.WebSocket/Cache/CacheRunMode.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - public enum CacheRunMode - { - /// - /// The cache should preform a synchronous cache lookup. - /// - Sync, - - /// - /// The cache should preform either a or asynchronous cache lookup. - /// - Async - } -} diff --git a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs index 136d88a75d..7a146677b6 100644 --- a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs @@ -12,6 +12,16 @@ public class DefaultConcurrentCacheProvider : ICacheProvider private readonly ConcurrentDictionary _storeCache = new(); private readonly ConcurrentDictionary _subStoreCache = new(); + private readonly Dictionary _models = new() + { + { typeof(IUserModel), typeof(API.User) }, + { typeof(ICurrentUserModel), typeof(API.CurrentUser) }, + { typeof(IMemberModel), typeof(API.GuildMember) }, + { typeof(IThreadMemberModel), typeof(API.ThreadMember)}, + { typeof(IPresenceModel), typeof(API.Presence)}, + { typeof(IActivityModel), typeof(API.Game)} + }; + private class DefaultEntityStore : IEntityStore where TModel : IEntityModel where TId : IEquatable @@ -23,44 +33,72 @@ public DefaultEntityStore(ConcurrentDictionary cache) _cache = cache; } - public ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode) + public TModel Get(TId id) { - _cache.AddOrUpdate(model.Id, model, (_, __) => model); + if (_cache.TryGetValue(id, out var model)) + return model; return default; } - - public ValueTask AddOrUpdateBatchAsync(IEnumerable models, CacheRunMode runmode) + 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); - return default; + } + public void Remove(TId id) + { + _cache.TryRemove(id, out _); + } + public void PurgeAll() + { + _cache.Clear(); } - public IAsyncEnumerable GetAllAsync(CacheRunMode runmode) + ValueTask IEntityStore.GetAsync(TId id) => new ValueTask(Get(id)); + IAsyncEnumerable IEntityStore.GetAllAsync() { - var coll = _cache.Select(x => x.Value).GetEnumerator(); - return AsyncEnumerable.Create((_) => AsyncEnumerator.Create( - () => new ValueTask(coll.MoveNext()), - () => coll.Current, - () => new ValueTask())); + var enumerator = GetAll().GetEnumerator(); + return AsyncEnumerable.Create((cancellationToken) + => AsyncEnumerator.Create( + () => new ValueTask(enumerator.MoveNext()), + () => enumerator.Current, + () => new ValueTask()) + ); } - public ValueTask GetAsync(TId id, CacheRunMode runmode) + ValueTask IEntityStore.AddOrUpdateAsync(TModel model) { - if (_cache.TryGetValue(id, out var model)) - return new ValueTask(model); + AddOrUpdate(model); return default; } - public ValueTask RemoveAsync(TId id, CacheRunMode runmode) + ValueTask IEntityStore.AddOrUpdateBatchAsync(IEnumerable models) { - _cache.TryRemove(id, out _); + AddOrUpdateBatch(models); return default; } - - public ValueTask PurgeAllAsync(CacheRunMode runmode) + ValueTask IEntityStore.RemoveAsync(TId id) { - _cache.Clear(); + Remove(id); return default; } + ValueTask IEntityStore.PurgeAllAsync() + { + PurgeAll(); + return default; + } + } + + public Type GetModel() + { + if (_models.TryGetValue(typeof(TInterface), out var t)) + return t; + return null; } public virtual ValueTask> GetStoreAsync() diff --git a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs index f4ae3b9947..42238a16ed 100644 --- a/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/ICacheProvider.cs @@ -8,6 +8,8 @@ namespace Discord.WebSocket { public interface ICacheProvider { + Type GetModel(); + ValueTask> GetStoreAsync() where TModel : IEntityModel where TId : IEquatable; @@ -21,11 +23,17 @@ public interface IEntityStore where TModel : IEntityModel where TId : IEquatable { - ValueTask GetAsync(TId id, CacheRunMode runmode); - IAsyncEnumerable GetAllAsync(CacheRunMode runmode); - ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode); - ValueTask AddOrUpdateBatchAsync(IEnumerable models, CacheRunMode runmode); - ValueTask RemoveAsync(TId id, CacheRunMode runmode); - ValueTask PurgeAllAsync(CacheRunMode runmode); + 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/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index ccbcc2f772..86c59d0bcf 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -92,28 +92,6 @@ internal void ClearDeadReferences() } } - private TResult RunOrThrowValueTask(ValueTask t) - { - if (_allowSyncWaits) - { - return t.GetAwaiter().GetResult(); - } - else if (t.IsCompleted) - return t.Result; - else - throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); - } - - private void RunOrThrowValueTask(ValueTask t) - { - if (_allowSyncWaits) - { - t.GetAwaiter().GetResult(); - } - else if (!t.IsCompleted) - throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); - } - public async ValueTask InitializeAsync() { _store ??= await _cacheProvider.GetStoreAsync().ConfigureAwait(false); @@ -137,7 +115,7 @@ public TEntity Get(TId id) return entity; } - var model = RunOrThrowValueTask(_store.GetAsync(id, CacheRunMode.Sync)); + var model = _store.Get(id); if (model != null) { @@ -156,7 +134,7 @@ public async ValueTask GetAsync(TId id, CacheMode mode, RequestOp return entity; } - var model = await _store.GetAsync(id, CacheRunMode.Async).ConfigureAwait(false); + var model = await _store.GetAsync(id).ConfigureAwait(false); if (model != null) { @@ -175,7 +153,7 @@ public async ValueTask GetAsync(TId id, CacheMode mode, RequestOp public IEnumerable GetAll() { - var models = RunOrThrowValueTask(_store.GetAllAsync(CacheRunMode.Sync).ToArrayAsync()); + var models = _store.GetAll(); return models.Select(x => { var entity = _entityBuilder(x); @@ -186,7 +164,7 @@ public IEnumerable GetAll() public async IAsyncEnumerable GetAllAsync() { - await foreach(var model in _store.GetAllAsync(CacheRunMode.Async)) + await foreach(var model in _store.GetAllAsync()) { var entity = _entityBuilder(model); _references.TryAdd(model.Id, new CacheReference(entity)); @@ -212,13 +190,13 @@ public async ValueTask GetOrAddAsync(TId id, Func valueFac return (TEntity)entity; var model = valueFactory(id); - await AddOrUpdateAsync(model); + await AddOrUpdateAsync(model).ConfigureAwait(false); return _entityBuilder(model); } public void AddOrUpdate(TModel model) { - RunOrThrowValueTask(_store.AddOrUpdateAsync(model, CacheRunMode.Sync)); + _store.AddOrUpdate(model); if (TryGetReference(model.Id, out var reference)) reference.Update(model); } @@ -227,14 +205,13 @@ public ValueTask AddOrUpdateAsync(TModel model) { if (TryGetReference(model.Id, out var reference)) reference.Update(model); - return _store.AddOrUpdateAsync(model, CacheRunMode.Async); + return _store.AddOrUpdateAsync(model); } public void BulkAddOrUpdate(IEnumerable models) { - RunOrThrowValueTask(_store.AddOrUpdateBatchAsync(models, CacheRunMode.Sync)); - - foreach(var model in models) + _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); @@ -243,7 +220,7 @@ public void BulkAddOrUpdate(IEnumerable models) public async ValueTask BulkAddOrUpdateAsync(IEnumerable models) { - await _store.AddOrUpdateBatchAsync(models, CacheRunMode.Async).ConfigureAwait(false); + await _store.AddOrUpdateBatchAsync(models).ConfigureAwait(false); foreach (var model in models) { @@ -254,26 +231,26 @@ public async ValueTask BulkAddOrUpdateAsync(IEnumerable models) public void Remove(TId id) { - RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); + _store.Remove(id); _references.TryRemove(id, out _); } public ValueTask RemoveAsync(TId id) { _references.TryRemove(id, out _); - return _store.RemoveAsync(id, CacheRunMode.Async); + return _store.RemoveAsync(id); } public void Purge() { - RunOrThrowValueTask(_store.PurgeAllAsync(CacheRunMode.Sync)); + _store.PurgeAll(); _references.Clear(); } public ValueTask PurgeAsync() { _references.Clear(); - return _store.PurgeAllAsync(CacheRunMode.Async); + return _store.PurgeAllAsync(); } TEntity ILookupReferenceStore.Get(TId id) => Get(id); @@ -380,5 +357,24 @@ public async Task GetThreadMemberStore(ulong threadId) + => _threadMemberStores.TryGetValue(threadId, out var store) ? store : null; + + public TModel GetModel() + where TFallback : class, TModel, new() + { + 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 new TFallback(); + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 9b25eb7412..f876283a42 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -74,7 +74,6 @@ public class SocketGuildUser : SocketUser, IGuildUser, ICached, IDi /// public bool? IsPending { get; private set; } - /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); /// @@ -159,7 +158,7 @@ internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client } internal void Update(MemberModel model) { - _joinedAtTicks = model.JoinedAt.UtcTicks; + _joinedAtTicks = model.JoinedAt.HasValue ? model.JoinedAt.Value.UtcTicks : null; Nickname = model.Nickname; GuildAvatarId = model.GuildAvatar; UpdateRoles(model.Roles); @@ -232,6 +231,17 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + public override void Dispose() + { + if (IsFreed) + return; + + GC.SuppressFinalize(this); + Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); + IsFreed = true; + } + ~SocketGuildUser() => Dispose(); + #endregion #region IGuildUser @@ -249,7 +259,7 @@ public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort si #region Cache - private struct CacheModel : MemberModel + private class CacheModel : MemberModel { public ulong Id { get; set; } public string Nickname { get; set; } @@ -258,7 +268,7 @@ private struct CacheModel : MemberModel public ulong[] Roles { get; set; } - public DateTimeOffset JoinedAt { get; set; } + public DateTimeOffset? JoinedAt { get; set; } public DateTimeOffset? PremiumSince { get; set; } @@ -271,40 +281,25 @@ private struct CacheModel : MemberModel public DateTimeOffset? CommunicationsDisabledUntil { get; set; } } internal new MemberModel ToModel() - => ToModel(); - - internal new TModel ToModel() where TModel : MemberModel, new() { - return new TModel - { - Id = Id, - CommunicationsDisabledUntil = TimedOutUntil, - GuildAvatar = GuildAvatarId, - IsDeaf = IsDeafened, - IsMute = IsMuted, - IsPending = IsPending, - JoinedAt = JoinedAt ?? DateTimeOffset.UtcNow, // review: nullable joined at here? should our model reflect this? - Nickname = Nickname, - PremiumSince = PremiumSince, - Roles = _roleIds.ToArray() - }; + 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(); - TResult ICached.ToModel() - => ToModel(); - void ICached.Update(MemberModel model) => Update(model); - - public override void Dispose() - { - GC.SuppressFinalize(this); - Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); - } - ~SocketGuildUser() => Dispose(); - #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index aa02204533..b1a4629ad4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -112,7 +112,6 @@ private static IImmutableList ConvertActivitiesList(IActivityModel[] internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; ~SocketPresence() => Dispose(); - public void Dispose() { if (IsFreed) @@ -128,7 +127,7 @@ public void Dispose() } #region Cache - private struct CacheModel : Model + private class CacheModel : Model { public UserStatus Status { get; set; } @@ -187,48 +186,43 @@ private struct EmojiCacheModel : IEmojiModel } internal Model ToModel() - => ToModel(); - - internal TModel ToModel() where TModel : Model, new() { - return new TModel + var model = Discord.StateManager.GetModel(); + model.Status = Status; + model.ActiveClients = ActiveClients.ToArray(); + model.UserId = UserId; + model.GuildId = GuildId; + model.Activities = Activities.Select(x => { - Status = Status, - ActiveClients = ActiveClients.ToArray(), - UserId = UserId, - GuildId = GuildId, - 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 { - 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(), - }; + Name = x.Name, + Details = x.Details, + Flags = x.Flags, + Type = x.Type + }; + }).ToArray(); + return model; } Model ICached.ToModel() => ToModel(); - TResult ICached.ToModel() => ToModel(); void ICached.Update(Model model) => Update(model); bool ICached.IsFreed => IsFreed; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 1d1c3bb6c4..6047c34f1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -97,7 +97,7 @@ public override void Dispose() } #region Cache - private struct CacheModel : Model + private class CacheModel : Model { public bool? IsVerified { get; set; } @@ -125,29 +125,23 @@ private struct CacheModel : Model } internal new Model ToModel() - => ToModel(); - - internal new TModel ToModel() where TModel : Model, new() { - return new TModel - { - Avatar = AvatarId, - Discriminator = Discriminator, - Email = Email, - Flags = Flags, - Id = Id, - IsBot = IsBot, - IsMfaEnabled = IsMfaEnabled, - IsVerified = IsVerified, - Locale = Locale, - PremiumType = this.PremiumType, - PublicFlags = PublicFlags ?? UserProperties.None, - Username = Username - }; + 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(); - TResult 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 48aebc92e5..fc67a1e1b9 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -151,14 +151,14 @@ internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threa internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) { - var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, 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.UserId.Value); + var entity = new SocketThreadUser(client, guildId, threadId, model.Id); entity.Update(model); return entity; } @@ -242,7 +242,12 @@ internal void Update(Model model) public override void Dispose() { + if (IsFreed) + return; + GC.SuppressFinalize(this); + Discord.StateManager.GetThreadMemberStore(_threadId)?.RemoveReference(Id); + IsFreed = true; } @@ -255,27 +260,21 @@ public override void Dispose() #region Cache private class CacheModel : Model { + public ulong Id { get; set; } public ulong? ThreadId { get; set; } - public ulong? UserId { get; set; } public DateTimeOffset JoinedAt { get; set; } - - ulong IEntityModel.Id { get => UserId.GetValueOrDefault(); set => throw new NotSupportedException(); } } - internal new Model ToModel() => ToModel(); - - internal new TModel ToModel() where TModel : Model, new() + internal new Model ToModel() { - return new TModel - { - JoinedAt = ThreadJoinedAt, - ThreadId = _threadId, - UserId = Id - }; + var model = Discord.StateManager.GetModel(); + model.Id = Id; + model.ThreadId = _threadId; + model.JoinedAt = ThreadJoinedAt; + return model; } Model ICached.ToModel() => ToModel(); - TResult ICached.ToModel() => ToModel(); void ICached.Update(Model model) => Update(model); #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 43b3b0e54d..3821fac500 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -137,29 +137,20 @@ private class CacheModel : Model public ulong Id { get; set; } } - internal TModel ToModel() where TModel : Model, new() + internal Model ToModel() { - return new TModel - { - Avatar = AvatarId, - Discriminator = Discriminator, - Id = Id, - IsBot = IsBot, - Username = Username - }; + var model = Discord.StateManager.GetModel(); + model.Avatar = AvatarId; + model.Discriminator = Discriminator; + model.Id = Id; + model.IsBot = IsBot; + model.Username = Username; + return model; } - internal Model ToModel() - => ToModel(); - Model ICached.ToModel() => ToModel(); - - TResult ICached.ToModel() - => ToModel(); - void ICached.Update(Model model) => Update(model); - bool ICached.IsFreed => IsFreed; #endregion From 157063c7edbf92d9f849e2271c9e88a6e44c73f8 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 27 Apr 2022 10:54:44 -0300 Subject: [PATCH 09/11] default model maps --- .../ClientStateManager.Experiment.cs | 47 +++++++++++-------- .../Entities/Users/SocketGuildUser.cs | 2 +- .../Entities/Users/SocketPresence.cs | 2 +- .../Entities/Users/SocketSelfUser.cs | 2 +- .../Entities/Users/SocketThreadUser.cs | 2 +- .../Entities/Users/SocketUser.cs | 2 +- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index 86c59d0bcf..dc63dc7971 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -267,26 +267,15 @@ internal partial class ClientStateManager private SemaphoreSlim _memberStoreLock; private SemaphoreSlim _threadMemberLock; - private void CreateStores() + private readonly Dictionary> _defaultModelFactory = new() { - UserStore = new ReferenceStore( - _cacheProvider, - m => SocketGlobalUser.Create(_client, m), - async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), - AllowSyncWaits); + { 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() }, + }; - PresenceStore = new ReferenceStore( - _cacheProvider, - m => SocketPresence.Create(_client, m), - (id, options) => Task.FromResult(null), - AllowSyncWaits); - - _memberStores = new(); - _threadMemberStores = new(); - - _threadMemberLock = new(1, 1); - _memberStoreLock = new(1,1); - } public void ClearDeadReferences() { @@ -374,7 +363,27 @@ public TModel GetModel() return (TModel)Activator.CreateInstance(type); } else - return new TFallback(); + return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : new TFallback(); + } + private void CreateStores() + { + UserStore = new ReferenceStore( + _cacheProvider, + m => SocketGlobalUser.Create(_client, m), + async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), + AllowSyncWaits); + + PresenceStore = new ReferenceStore( + _cacheProvider, + m => SocketPresence.Create(_client, m), + (id, options) => Task.FromResult(null), + AllowSyncWaits); + + _memberStores = new(); + _threadMemberStores = new(); + + _threadMemberLock = new(1, 1); + _memberStoreLock = new(1,1); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index f876283a42..8757d206a0 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -259,7 +259,7 @@ public override void Dispose() #region Cache - private class CacheModel : MemberModel + internal new class CacheModel : MemberModel { public ulong Id { get; set; } public string Nickname { get; set; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index b1a4629ad4..b6e9cd764c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -127,7 +127,7 @@ public void Dispose() } #region Cache - private class CacheModel : Model + internal class CacheModel : Model { public UserStatus Status { get; set; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 6047c34f1c..087d4ba297 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -97,7 +97,7 @@ public override void Dispose() } #region Cache - private class CacheModel : Model + internal new class CacheModel : Model { public bool? IsVerified { get; set; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index fc67a1e1b9..0b614adaec 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -258,7 +258,7 @@ public override void Dispose() public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value; #region Cache - private class CacheModel : Model + internal new class CacheModel : Model { public ulong Id { get; set; } public ulong? ThreadId { get; set; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 3821fac500..20c1951039 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -124,7 +124,7 @@ public string GetDefaultAvatarUrl() internal SocketUser Clone() => MemberwiseClone() as SocketUser; #region Cache - private class CacheModel : Model + internal class CacheModel : Model { public string Username { get; set; } From dc2dafa3ac3d04f753c627ef59691cb80c8378df Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 27 Apr 2022 13:21:35 -0300 Subject: [PATCH 10/11] custom model factory --- .../Cache/Models/Message/IMessageModel.cs | 24 ++++++++ .../Cache/DefaultConcurrentCacheProvider.cs | 17 +----- .../ClientStateManager.Experiment.cs | 45 ++++++++++----- .../Entities/Users/SocketPresence.cs | 6 +- .../Extensions/CacheModelExtensions.cs | 56 +++++++++++++++++++ 5 files changed, 116 insertions(+), 32 deletions(-) create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs create mode 100644 src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs 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..c1dc6001c4 --- /dev/null +++ b/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs @@ -0,0 +1,24 @@ +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; } + DateTimeOffset Timestamp { get; set; } + DateTimeOffset? EditedTimestamp { get; set; } + bool IsTextToSpeech { get; set; } + bool MentionEveryone { get; set; } + ulong[] UserMentionIds { get; set; } + ulong[] RoleMentionIds { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs index 7a146677b6..f182bd1f60 100644 --- a/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs +++ b/src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs @@ -12,16 +12,6 @@ public class DefaultConcurrentCacheProvider : ICacheProvider private readonly ConcurrentDictionary _storeCache = new(); private readonly ConcurrentDictionary _subStoreCache = new(); - private readonly Dictionary _models = new() - { - { typeof(IUserModel), typeof(API.User) }, - { typeof(ICurrentUserModel), typeof(API.CurrentUser) }, - { typeof(IMemberModel), typeof(API.GuildMember) }, - { typeof(IThreadMemberModel), typeof(API.ThreadMember)}, - { typeof(IPresenceModel), typeof(API.Presence)}, - { typeof(IActivityModel), typeof(API.Game)} - }; - private class DefaultEntityStore : IEntityStore where TModel : IEntityModel where TId : IEquatable @@ -94,12 +84,7 @@ ValueTask IEntityStore.PurgeAllAsync() } } - public Type GetModel() - { - if (_models.TryGetValue(typeof(TInterface), out var t)) - return t; - return null; - } + public Type GetModel() => null; public virtual ValueTask> GetStoreAsync() where TModel : IEntityModel diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index dc63dc7971..b4b91ff69b 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -57,18 +57,24 @@ internal class ReferenceStore : ILookupRefe private readonly ConcurrentDictionary> _references = new(); private IEntityStore _store; private Func _entityBuilder; + private Func _modelFactory; private Func> _restLookup; - private readonly bool _allowSyncWaits; private readonly object _lock = new(); - public ReferenceStore(ICacheProvider cacheProvider, Func entityBuilder, Func> restLookup, bool allowSyncWaits) + public ReferenceStore(ICacheProvider cacheProvider, + Func entityBuilder, + Func> restLookup, + Func userDefinedModelFactory) { - _allowSyncWaits = allowSyncWaits; _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)) @@ -196,20 +202,23 @@ public async ValueTask GetOrAddAsync(TId id, Func valueFac public void AddOrUpdate(TModel model) { - _store.AddOrUpdate(model); + var userDefinedModel = GetUserDefinedModel(model); + _store.AddOrUpdate(userDefinedModel); if (TryGetReference(model.Id, out var reference)) - reference.Update(model); + reference.Update(userDefinedModel); } public ValueTask AddOrUpdateAsync(TModel model) { - if (TryGetReference(model.Id, out var reference)) - reference.Update(model); - return _store.AddOrUpdateAsync(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) { @@ -220,6 +229,7 @@ public void BulkAddOrUpdate(IEnumerable models) public async ValueTask BulkAddOrUpdateAsync(IEnumerable models) { + models = models.Select(x => GetUserDefinedModel(x)); await _store.AddOrUpdateBatchAsync(models).ConfigureAwait(false); foreach (var model in models) @@ -274,6 +284,7 @@ internal partial class ClientStateManager { typeof(ICurrentUserModel), () => new SocketSelfUser.CacheModel() }, { typeof(IThreadMemberModel), () => new SocketThreadUser.CacheModel() }, { typeof(IPresenceModel), () => new SocketPresence.CacheModel() }, + { typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() } }; @@ -308,7 +319,7 @@ public async ValueTask SocketGuildUser.Create(guildId, _client, m), async (id, options) => await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false), - AllowSyncWaits); + GetModel); await store.InitializeAsync(guildId).ConfigureAwait(false); @@ -334,7 +345,7 @@ public async Task SocketThreadUser.Create(_client, guildId, threadId, m), async (id, options) => await ThreadHelper.GetUserAsync(id, _client.GetChannel(threadId) as SocketThreadChannel, _client, options).ConfigureAwait(false), - AllowSyncWaits); + GetModel); await store.InitializeAsync().ConfigureAwait(false); @@ -352,6 +363,13 @@ public ReferenceStore 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(); @@ -363,21 +381,22 @@ public TModel GetModel() return (TModel)Activator.CreateInstance(type); } else - return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : new TFallback(); + return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; } + private void CreateStores() { UserStore = new ReferenceStore( _cacheProvider, m => SocketGlobalUser.Create(_client, m), async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), - AllowSyncWaits); + GetModel); PresenceStore = new ReferenceStore( _cacheProvider, m => SocketPresence.Create(_client, m), (id, options) => Task.FromResult(null), - AllowSyncWaits); + GetModel); _memberStores = new(); _threadMemberStores = new(); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index b6e9cd764c..220e853059 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -142,11 +142,11 @@ internal class CacheModel : Model ulong IEntityModel.Id { get => UserId; - set => throw new NotSupportedException(); + set => UserId = value; } } - private struct ActivityCacheModel : IActivityModel + internal class ActivityCacheModel : IActivityModel { public string Id { get; set; } public string Url { get; set; } @@ -173,7 +173,7 @@ private struct ActivityCacheModel : IActivityModel public DateTimeOffset? TimestampEnd { get; set; } } - private struct EmojiCacheModel : IEmojiModel + private class EmojiCacheModel : IEmojiModel { public ulong? Id { get; set; } public string Name { get; set; } diff --git a/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs new file mode 100644 index 0000000000..5b1da074cd --- /dev/null +++ b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs @@ -0,0 +1,56 @@ +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 ToSpecifiedModel(this IEntityModel source, TDest dest) + where TId : IEquatable + where TDest : IEntityModel + { + if (source == null || dest == null) + throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); + + // get the shared model interface + var sourceType = source.GetType(); + var destType = dest.GetType(); + + if (sourceType == destType) + return (TDest)source; + + 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; + } + } +} From 67c50944625fa71e32adc3f637fbf60493137592 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Mon, 23 May 2022 03:44:42 -0300 Subject: [PATCH 11/11] more work on cache provider --- .../Cache/CacheableEntityExtensions.cs | 19 +- .../Application/IPartialApplicationModel.cs | 16 + .../Components/IMessageComponentModel.cs | 35 ++ .../IMessageComponentOptionModel.cs | 22 + .../Cache/Models/Message/IAttachmentModel.cs | 21 + .../Cache/Models/Message/IEmbedModel.cs | 44 ++ .../Models/Message/IMessageActivityModel.cs | 14 + .../Cache/Models/Message/IMessageModel.cs | 28 +- .../Cache/Models/Message/IReactionModel.cs | 14 + .../Cache/Models/Message/IStickerModel.cs | 15 + .../Entities/Messages/IMessage.cs | 5 + .../Entities/Messages/MessageInteraction.cs | 28 +- src/Discord.Net.Core/Utils/Optional.cs | 4 + .../API/Common/ActionRowComponent.cs | 24 +- src/Discord.Net.Rest/API/Common/Attachment.cs | 13 +- .../API/Common/ButtonComponent.cs | 24 +- src/Discord.Net.Rest/API/Common/Embed.cs | 12 +- .../API/Common/EmbedAuthor.cs | 2 +- src/Discord.Net.Rest/API/Common/EmbedField.cs | 4 +- .../API/Common/EmbedFooter.cs | 2 +- src/Discord.Net.Rest/API/Common/EmbedImage.cs | 5 +- .../API/Common/EmbedProvider.cs | 2 +- .../API/Common/EmbedThumbnail.cs | 5 +- src/Discord.Net.Rest/API/Common/EmbedVideo.cs | 7 +- src/Discord.Net.Rest/API/Common/Message.cs | 35 +- .../API/Common/MessageActivity.cs | 5 +- .../API/Common/MessageApplication.cs | 7 +- src/Discord.Net.Rest/API/Common/Reaction.cs | 8 +- .../API/Common/SelectMenuComponent.cs | 25 +- .../API/Common/SelectMenuOption.cs | 10 +- .../API/Common/StickerItem.cs | 7 +- .../API/Common/TextInputComponent.cs | 26 +- src/Discord.Net.Rest/ClientHelper.cs | 10 + src/Discord.Net.Rest/DiscordRestClient.cs | 3 + .../Entities/Messages/Attachment.cs | 12 +- .../Entities/Messages/MessageHelper.cs | 10 +- .../Extensions/EntityExtensions.cs | 59 +-- .../ClientStateManager.Experiment.cs | 149 +++++-- .../Entities/Messages/SocketMessage.cs | 413 +++++++++++++++--- .../Entities/Messages/SocketReaction.cs | 55 ++- .../Entities/Messages/SocketUserMessage.cs | 72 +-- .../Entities/Stickers/SocketUnknownSticker.cs | 4 +- .../Extensions/CacheModelExtensions.cs | 22 +- .../Extensions/EntityCacheExtensions.cs | 16 + .../Extensions/EntityExtensions.cs | 12 + 45 files changed, 1066 insertions(+), 259 deletions(-) create mode 100644 src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs create mode 100644 src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs create mode 100644 src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs diff --git a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs index 6e6b0945fa..d92e5b2111 100644 --- a/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs +++ b/src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs @@ -78,17 +78,14 @@ public static IActivityModel ToModel(this CustomStatusGame }; } - public static IEmojiModel ToModel(this IEmote emote) where TModel : IEmojiModel, new() + public static IEmojiModel ToModel(this IEmote emote, IEmojiModel model) { if (emote == null) return null; - var model = new TModel() - { - Name = emote.Name - }; + model.Name = emote.Name; - if(emote is GuildEmote guildEmote) + if (emote is GuildEmote guildEmote) { model.Id = guildEmote.Id; model.IsAnimated = guildEmote.Animated; @@ -99,7 +96,7 @@ public static IActivityModel ToModel(this CustomStatusGame model.Roles = guildEmote.RoleIds.ToArray(); } - if(emote is Emote e) + if (emote is Emote e) { model.IsAnimated = e.Animated; model.Id = e.Id; @@ -107,5 +104,13 @@ public static IActivityModel ToModel(this CustomStatusGame return model; } + + public static IEmojiModel ToModel(this IEmote emote) where TModel : IEmojiModel, new() + { + if (emote == null) + return null; + + return emote.ToModel(new TModel()); + } } } diff --git a/src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs b/src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs new file mode 100644 index 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/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 index c1dc6001c4..ca718ec660 100644 --- a/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs +++ b/src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs @@ -14,11 +14,35 @@ public interface IMessageModel : IEntityModel ulong AuthorId { get; set; } bool IsWebhookMessage { get; set; } string Content { get; set; } - DateTimeOffset Timestamp { get; set; } - DateTimeOffset? EditedTimestamp { get; set; } + long Timestamp { get; set; } + long? EditedTimestamp { get; set; } bool IsTextToSpeech { get; set; } bool MentionEveryone { get; set; } ulong[] UserMentionIds { get; set; } ulong[] RoleMentionIds { get; set; } + + IAttachmentModel[] Attachments { get; set; } + IEmbedModel[] Embeds { get; set; } + IReactionMetadataModel[] Reactions { get; set; } + bool Pinned { get; set; } + IMessageActivityModel Activity { get; set; } + IPartialApplicationModel Application { get; set; } + ulong? ApplicationId { get; set; } + + // message reference + ulong? ReferenceMessageId { get; set; } + ulong? ReferenceMessageChannelId { get; set; } + ulong? ReferenceMessageGuildId { get; set; } + + MessageFlags Flags { get; set; } + + // interaction + ulong? InteractionId { get; set; } + string InteractionName { get; set; } + InteractionType? InteractionType { get; set; } + ulong? InteractionUserId { get; set; } + + IMessageComponentModel[] Components { get; set; } + IStickerItemModel[] Stickers { get; set; } } } diff --git a/src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs b/src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs new file mode 100644 index 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/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/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/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/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/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index ab0238fee0..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); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 6ae8bc0b96..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); 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/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 5de5b7b5d7..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; @@ -40,17 +41,25 @@ public static GuildEmote ToEntity(this API.Emoji model) ImmutableArray.Create(model.Roles), model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); - public static Embed ToEntity(this API.Embed model) + public static Embed ToEntity(this IEmbedModel model) { - return new Embed(model.Type, model.Title, model.Description, model.Url, model.Timestamp, + return new Embed(model.Type, model.Title, model.Description, model.Url, + model.Timestamp.HasValue ? new DateTimeOffset(model.Timestamp.Value, TimeSpan.Zero) : null, model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, - model.Image.IsSpecified ? model.Image.Value.ToEntity() : (EmbedImage?)null, - model.Video.IsSpecified ? model.Video.Value.ToEntity() : (EmbedVideo?)null, - model.Author.IsSpecified ? model.Author.Value.ToEntity() : (EmbedAuthor?)null, - model.Footer.IsSpecified ? model.Footer.Value.ToEntity() : (EmbedFooter?)null, - model.Provider.IsSpecified ? model.Provider.Value.ToEntity() : (EmbedProvider?)null, - model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, - model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); + model.Image != null + ? new EmbedImage(model.Image.Url, model.Image.ProxyUrl, model.Image.Height, model.Image.Width) : (EmbedImage?)null, + model.Video != null + ? new EmbedVideo(model.Video.Url, model.Video.Height, model.Video.Width) : (EmbedVideo?)null, + model.AuthorIconUrl != null || model.AuthorName != null || model.AuthorProxyIconUrl != null || model.AuthorUrl != null + ? new EmbedAuthor(model.AuthorName, model.AuthorUrl, model.AuthorIconUrl, model.AuthorProxyIconUrl) : (EmbedAuthor?)null, + model.FooterIconUrl != null || model.FooterProxyUrl != null || model.FooterText != null + ? new EmbedFooter(model.FooterText, model.FooterIconUrl, model.FooterProxyUrl) : (EmbedFooter?)null, + model.ProviderUrl != null || model.ProviderName != null + ? new EmbedProvider(model.ProviderName, model.ProviderUrl) : (EmbedProvider?)null, + model.Thumbnail != null + ? new EmbedThumbnail(model.Thumbnail.Url, model.Thumbnail.ProxyUrl, model.Thumbnail.Height, model.Thumbnail.Width) : (EmbedThumbnail?)null, + model.Fields != null + ? model.Fields.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); } public static RoleTags ToEntity(this API.RoleTags model) { @@ -116,15 +125,11 @@ 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); } @@ -132,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 }; diff --git a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs index b4b91ff69b..53298b476b 100644 --- a/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs +++ b/src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs @@ -49,7 +49,7 @@ internal interface ILookupReferenceStore internal class ReferenceStore : ILookupReferenceStore where TEntity : class, ICached, TSharedEntity - where TModel : IEntityModel + where TModel : class, IEntityModel where TId : IEquatable where TSharedEntity : class { @@ -263,20 +263,40 @@ public ValueTask PurgeAsync() return _store.PurgeAllAsync(); } + public IEnumerable GetEnumerable(IEnumerable ids) + { + foreach (var id in ids) + { + yield return Get(id); + } + } + + public async IAsyncEnumerable GetEnumerableAsync(IEnumerable ids) + { + foreach (var id in ids) + { + yield return (TEntity)await GetAsync(id, CacheMode.CacheOnly); + } + } + TEntity ILookupReferenceStore.Get(TId id) => Get(id); async ValueTask ILookupReferenceStore.GetAsync(TId id) => (TEntity)await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); } internal partial class ClientStateManager { - public ReferenceStore UserStore; + public ReferenceStore UserStore; public ReferenceStore PresenceStore; + private ConcurrentDictionary> _memberStores; private ConcurrentDictionary> _threadMemberStores; + private ConcurrentDictionary> _messageStores; private SemaphoreSlim _memberStoreLock; + private SemaphoreSlim _messageStoreLock; private SemaphoreSlim _threadMemberLock; + #region Models private readonly Dictionary> _defaultModelFactory = new() { { typeof(IUserModel), () => new SocketUser.CacheModel() }, @@ -284,10 +304,45 @@ internal partial class ClientStateManager { typeof(ICurrentUserModel), () => new SocketSelfUser.CacheModel() }, { typeof(IThreadMemberModel), () => new SocketThreadUser.CacheModel() }, { typeof(IPresenceModel), () => new SocketPresence.CacheModel() }, - { typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() } + { typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() }, + { typeof(IMessageModel), () => new SocketMessage.CacheModel() }, + { typeof(IMessageActivityModel), () => new SocketMessage.CacheModel.MessageActivityModel() }, + { typeof(IMessageComponentModel), () => new SocketMessage.CacheModel.MessageComponentModel() }, + { typeof(IMessageComponentOptionModel), () => new SocketMessage.CacheModel.MessageComponentModel.MessageComponentOptionModel() }, + { typeof(IPartialApplicationModel), () => new SocketMessage.CacheModel.PartialApplicationModel() }, + { typeof(IStickerItemModel), () => new SocketMessage.CacheModel.StickerItemModel() }, + { typeof(IReactionMetadataModel), () => new SocketMessage.CacheModel.ReactionModel() }, + { typeof(IEmbedModel), () => new SocketMessage.CacheModel.EmbedModel() }, + { typeof(IEmbedFieldModel), () => new SocketMessage.CacheModel.EmbedModel.EmbedFieldModel() }, + { typeof(IEmbedMediaModel), () => new SocketMessage.CacheModel.EmbedModel.EmbedMediaModel()} + }; + public TModel GetModel() + where TFallback : class, TModel, new() + where TModel : class + { + return GetModel() ?? new TFallback(); + } + + public TModel GetModel() + where TModel : class + { + var type = _cacheProvider.GetModel(); + if (type != null) + { + if (!type.GetInterfaces().Contains(typeof(TModel))) + throw new InvalidOperationException($"Cannot use {type.Name} as a model for {typeof(TModel).Name}"); + + return (TModel)Activator.CreateInstance(type); + } + else + return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; + } + #endregion + + #region References & Initialization public void ClearDeadReferences() { UserStore.ClearDeadReferences(); @@ -300,6 +355,29 @@ public async ValueTask 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; @@ -331,7 +409,9 @@ public async ValueTask> GetThreadMemberStoreAsync(ulong threadId, ulong guildId) { if (_threadMemberStores.TryGetValue(threadId, out var store)) @@ -360,49 +440,40 @@ public async Task GetThreadMemberStore(ulong threadId) => _threadMemberStores.TryGetValue(threadId, out var store) ? store : null; + #endregion - public TModel GetModel() - where TFallback : class, TModel, new() - where TModel : class - { - return GetModel() ?? new TFallback(); - } + #region Messages + public ReferenceStore GetMessageStore(ulong channelId) + => TryGetMessageStore(channelId, out var store) ? store : null; - public TModel GetModel() - where TModel : class - { - var type = _cacheProvider.GetModel(); - - if (type != null) - { - if (!type.GetInterfaces().Contains(typeof(TModel))) - throw new InvalidOperationException($"Cannot use {type.Name} as a model for {typeof(TModel).Name}"); - - return (TModel)Activator.CreateInstance(type); - } - else - return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; - } + public bool TryGetMessageStore(ulong channelId, out ReferenceStore store) + => _messageStores.TryGetValue(channelId, out store); - private void CreateStores() + public async ValueTask> GetMessageStoreAsync(ulong channelId) { - UserStore = new ReferenceStore( - _cacheProvider, - m => SocketGlobalUser.Create(_client, m), - async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), - GetModel); + if (_messageStores.TryGetValue(channelId, out var store)) + return store; - PresenceStore = new ReferenceStore( - _cacheProvider, - m => SocketPresence.Create(_client, m), - (id, options) => Task.FromResult(null), - GetModel); + await _messageStoreLock.WaitAsync().ConfigureAwait(false); - _memberStores = new(); - _threadMemberStores = new(); + try + { + store = new ReferenceStore( + _cacheProvider, + m => SocketMessage.Create(_client, m, channelId), + async (id, options) => await _client.Rest.GetMessageAsync(channelId, id).ConfigureAwait(false), + GetModel); - _threadMemberLock = new(1, 1); - _memberStoreLock = new(1,1); + await store.InitializeAsync(channelId).ConfigureAwait(false); + + _messageStores.TryAdd(channelId, store); + return store; + } + finally + { + _memberStoreLock.Release(); + } } + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index c7bc5e873e..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,6 +139,12 @@ internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChan Author = author; Source = source; } + + //internal static SocketMessage Create(DiscordSocketClient discord, Model model, ulong channelId) + //{ + + //} + internal static SocketMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) { if (model.Type == MessageType.Default || @@ -140,55 +156,52 @@ internal static SocketMessage Create(DiscordSocketClient discord, ClientStateMan else return SocketSystemMessage.Create(discord, state, author, channel, model); } - internal virtual void Update(ClientStateManager state, Model model) + internal virtual void Update(Model model) { Type = model.Type; - if (model.Timestamp.IsSpecified) - _timestampTicks = model.Timestamp.Value.UtcTicks; - - if (model.Content.IsSpecified) - { - Content = model.Content.Value; - } + _timestampTicks = model.Timestamp; + ApplicationId = model.ApplicationId; + Content = model.Content; + _userMentionIds = model.UserMentionIds; - if (model.Application.IsSpecified) + if (model.Application != null) { // create a new Application from the API model Application = new MessageApplication() { - Id = model.Application.Value.Id, - CoverImage = model.Application.Value.CoverImage, - Description = model.Application.Value.Description, - Icon = model.Application.Value.Icon, - Name = model.Application.Value.Name + Id = model.Application.Id, + CoverImage = model.Application.CoverImage, + Description = model.Application.Description, + Icon = model.Application.Icon, + Name = model.Application.Name }; } - if (model.Activity.IsSpecified) + if (model.Activity != null) { // create a new Activity from the API model Activity = new MessageActivity() { - Type = model.Activity.Value.Type.Value, - PartyId = model.Activity.Value.PartyId.GetValueOrDefault() + Type = model.Activity.Type.Value, + PartyId = model.Activity.PartyId }; } - if (model.Reference.IsSpecified) + if (model.ReferenceMessageId.HasValue) { // Creates a new Reference from the API model Reference = new MessageReference { - GuildId = model.Reference.Value.GuildId, - InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + GuildId = model.ReferenceMessageGuildId.ToOptional(), + InternalChannelId = model.ReferenceMessageChannelId.ToOptional(), + MessageId = model.ReferenceMessageId.ToOptional() }; } - if (model.Components.IsSpecified) + if (model.Components != null && model.Components.Length > 0) { - Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select(y => + Components = model.Components.Select(x => new ActionRowComponent(x.Components.Select(y => { switch (y.Type) { @@ -236,38 +249,16 @@ internal virtual void Update(ClientStateManager 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, val)); - } - } - _userMentions = newMentions.ToImmutable(); - } + Interaction = new MessageInteraction(model.InteractionId.Value, + model.InteractionType.Value, + model.InteractionName, + model.InteractionUserId.Value, + Discord.StateManager.UserStore.Get); } - if (model.Interaction.IsSpecified) - { - Interaction = new MessageInteraction(model.Interaction.Value.Id, - model.Interaction.Value.Type, - model.Interaction.Value.Name, - SocketGlobalUser.Create(Discord, model.Interaction.Value.User)); - } - - if (model.Flags.IsSpecified) - Flags = model.Flags.Value; + Flags = model.Flags; } /// @@ -309,7 +300,6 @@ 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/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 4c8e944322..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(); /// @@ -56,30 +57,26 @@ internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessage internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) { var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); - entity.Update(state, model); + entity.Update(model); return entity; } - internal override void Update(ClientStateManager state, Model model) + internal override void Update(Model model) { - base.Update(state, model); + base.Update(model); SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; - if (model.IsTextToSpeech.IsSpecified) - _isTTS = model.IsTextToSpeech.Value; - if (model.Pinned.IsSpecified) - _isPinned = model.Pinned.Value; - if (model.EditedTimestamp.IsSpecified) - _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; - if (model.MentionEveryone.IsSpecified) - _isMentioningEveryone = model.MentionEveryone.Value; - if (model.RoleMentions.IsSpecified) - _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); - - if (model.Attachments.IsSpecified) + _isTTS = model.IsTextToSpeech; + _isPinned = model.Pinned; + _editedTimestampTicks = model.EditedTimestamp; + _isMentioningEveryone = model.MentionEveryone; + _roleMentions = model.RoleMentionIds; + _referencedMessageId = model.ReferenceMessageId; + + if (model.Attachments != null && model.Attachments.Length > 0) { - var value = model.Attachments.Value; + var value = model.Attachments; if (value.Length > 0) { var attachments = ImmutableArray.CreateBuilder(value.Length); @@ -91,9 +88,9 @@ internal override void Update(ClientStateManager 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(ClientStateManager 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, refMsg.Author.Value, webhookId.Value); - else - refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); - } - else - refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); - if (refMsgAuthor == null) - refMsgAuthor = SocketUnknownUser.Create(Discord, refMsg.Author.Value); - } - else - // Message author wasn't specified in the payload, so create a completely anonymous unknown user - refMsgAuthor = new SocketUnknownUser(Discord, id: 0); - _referencedMessage = SocketUserMessage.Create(Discord, state, refMsgAuthor, Channel, refMsg); - } - - if (model.StickerItems.IsSpecified) + if (model.Stickers != null && model.Stickers.Length > 0) { - var value = model.StickerItems.Value; + var value = model.Stickers; if (value.Length > 0) { var stickers = ImmutableArray.CreateBuilder(value.Length); diff --git a/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs b/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs index 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/Extensions/CacheModelExtensions.cs b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs index 5b1da074cd..5368426af0 100644 --- a/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs @@ -8,19 +8,26 @@ namespace Discord.WebSocket { internal static class CacheModelExtensions { - public static TDest ToSpecifiedModel(this IEntityModel source, TDest dest) - where TId : IEquatable - where TDest : IEntityModel + public static TDest InterfaceCopy(this object source) + where TDest : class, new() + => source.InterfaceCopy(new TDest()); + + public static TDest InterfaceCopy(this TSource source, TDest dest) + where TSource : class + where TDest : class { if (source == null || dest == null) throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); + if (source == null || dest == null) + throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); + // get the shared model interface var sourceType = source.GetType(); var destType = dest.GetType(); if (sourceType == destType) - return (TDest)source; + return source as TDest; List sharedInterfaceModels = new(); @@ -52,5 +59,12 @@ public static TDest ToSpecifiedModel(this IEntityModel 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 6cde93d871..e3927e6fcf 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -7,6 +7,17 @@ namespace Discord.WebSocket { internal static class EntityExtensions { + #region Emotes + public static IEmote ToEntity(this IEmojiModel model) + { + if (model.Id.HasValue) + return new Emote(model.Id.Value, model.Name, model.IsAnimated); + else + return new Emoji(model.Name); + } + #endregion + + #region Activity public static IActivity ToEntity(this IActivityModel model) { #region Custom Status Game @@ -147,5 +158,6 @@ public static GameTimestamps ToEntity(this API.GameTimestamps model) { return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } + #endregion } }