Cachable models so far - Users - Presencev4/state-cache-providers
| @@ -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<TModel>(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<TModel>(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<TModel, TEmoteModel>(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<TEmoteModel>(), | |||||
| CreatedAt = custom.CreatedAt | |||||
| }; | |||||
| } | |||||
| public static IActivityModel ToModel<TModel>(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<TModel>(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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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> | |||||
| { | |||||
| TType ToModel(); | |||||
| } | |||||
| } | |||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| } | |||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -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<ulong> | |||||
| { | |||||
| string Username { get; } | |||||
| string Discriminator { get; } | |||||
| bool? IsBot { get; } | |||||
| string Avatar { get; } | |||||
| } | |||||
| } | |||||
| @@ -107,6 +107,8 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| public string TrackUrl { get; internal set; } | public string TrackUrl { get; internal set; } | ||||
| internal string AlbumArt { get; set; } | |||||
| internal SpotifyGame() { } | internal SpotifyGame() { } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -24,6 +24,13 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| public bool RequireColons { get; } | public bool RequireColons { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets whether or not the emote is available. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// An emote can be unavailable if the guild has lost its boost status. | |||||
| /// </remarks> | |||||
| public bool IsAvailable { get; } | |||||
| /// <summary> | |||||
| /// Gets the roles that are allowed to use this emoji. | /// Gets the roles that are allowed to use this emoji. | ||||
| /// </summary> | /// </summary> | ||||
| /// <returns> | /// <returns> | ||||
| @@ -39,12 +46,13 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| public ulong? CreatorId { get; } | public ulong? CreatorId { get; } | ||||
| internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds, ulong? userId) : base(id, name, animated) | |||||
| internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool isAvailable, bool requireColons, IReadOnlyList<ulong> roleIds, ulong? userId) : base(id, name, animated) | |||||
| { | { | ||||
| IsManaged = isManaged; | IsManaged = isManaged; | ||||
| RequireColons = requireColons; | RequireColons = requireColons; | ||||
| RoleIds = roleIds; | RoleIds = roleIds; | ||||
| CreatorId = userId; | CreatorId = userId; | ||||
| IsAvailable = isAvailable; | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| @@ -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<bool> Verified { get; set; } | |||||
| [JsonProperty("email")] | |||||
| public Optional<string> Email { get; set; } | |||||
| [JsonProperty("mfa_enabled")] | |||||
| public Optional<bool> MfaEnabled { get; set; } | |||||
| [JsonProperty("flags")] | |||||
| public Optional<UserProperties> Flags { get; set; } | |||||
| [JsonProperty("premium_type")] | |||||
| public Optional<PremiumType> PremiumType { get; set; } | |||||
| [JsonProperty("locale")] | |||||
| public Optional<string> Locale { get; set; } | |||||
| [JsonProperty("public_flags")] | |||||
| public Optional<UserProperties> 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(); | |||||
| } | |||||
| } | |||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Emoji | |||||
| internal class Emoji : IEmojiModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong? Id { get; set; } | public ulong? Id { get; set; } | ||||
| @@ -16,7 +16,25 @@ namespace Discord.API | |||||
| public bool RequireColons { get; set; } | public bool RequireColons { get; set; } | ||||
| [JsonProperty("managed")] | [JsonProperty("managed")] | ||||
| public bool Managed { get; set; } | public bool Managed { get; set; } | ||||
| [JsonProperty("available")] | |||||
| public Optional<bool> Available { get; set; } | |||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public Optional<User> User { get; set; } | public Optional<User> 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; | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,10 +1,11 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Serialization; | using Newtonsoft.Json.Serialization; | ||||
| using System; | |||||
| using System.Runtime.Serialization; | using System.Runtime.Serialization; | ||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Game | |||||
| internal class Game : IActivityModel | |||||
| { | { | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| @@ -32,7 +33,7 @@ namespace Discord.API | |||||
| public Optional<string> SyncId { get; set; } | public Optional<string> SyncId { get; set; } | ||||
| [JsonProperty("session_id")] | [JsonProperty("session_id")] | ||||
| public Optional<string> SessionId { get; set; } | public Optional<string> SessionId { get; set; } | ||||
| [JsonProperty("Flags")] | |||||
| [JsonProperty("flags")] | |||||
| public Optional<ActivityProperties> Flags { get; set; } | public Optional<ActivityProperties> Flags { get; set; } | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public Optional<string> Id { get; set; } | public Optional<string> Id { get; set; } | ||||
| @@ -40,6 +41,54 @@ namespace Discord.API | |||||
| public Optional<Emoji> Emoji { get; set; } | public Optional<Emoji> Emoji { get; set; } | ||||
| [JsonProperty("created_at")] | [JsonProperty("created_at")] | ||||
| public Optional<long> CreatedAt { get; set; } | public Optional<long> 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")] | //[JsonProperty("buttons")] | ||||
| //public Optional<RichPresenceButton[]> Buttons { get; set; } | //public Optional<RichPresenceButton[]> Buttons { get; set; } | ||||
| @@ -3,7 +3,7 @@ using System; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class GuildMember | |||||
| internal class GuildMember : IMemberModel | |||||
| { | { | ||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public User User { get; set; } | public User User { get; set; } | ||||
| @@ -25,5 +25,26 @@ namespace Discord.API | |||||
| public Optional<DateTimeOffset?> PremiumSince { get; set; } | public Optional<DateTimeOffset?> PremiumSince { get; set; } | ||||
| [JsonProperty("communication_disabled_until")] | [JsonProperty("communication_disabled_until")] | ||||
| public Optional<DateTimeOffset?> TimedOutUntil { get; set; } | public Optional<DateTimeOffset?> TimedOutUntil { get; set; } | ||||
| // IMemberModel | |||||
| string IMemberModel.Nickname => Nick.GetValueOrDefault(); | |||||
| string IMemberModel.GuildAvatar => Avatar.GetValueOrDefault(); | |||||
| ulong[] IMemberModel.Roles => Roles.GetValueOrDefault(Array.Empty<ulong>()); | |||||
| 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; | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,10 +1,11 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Presence | |||||
| internal class Presence : IPresenceModel | |||||
| { | { | ||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public User User { get; set; } | public User User { get; set; } | ||||
| @@ -28,5 +29,17 @@ namespace Discord.API | |||||
| public List<Game> Activities { get; set; } | public List<Game> Activities { get; set; } | ||||
| [JsonProperty("premium_since")] | [JsonProperty("premium_since")] | ||||
| public Optional<DateTimeOffset?> PremiumSince { get; set; } | public Optional<DateTimeOffset?> 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<ClientType>(); | |||||
| IActivityModel[] IPresenceModel.Activities => Activities.ToArray(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class User | |||||
| internal class User : IUserModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| @@ -19,20 +19,16 @@ namespace Discord.API | |||||
| [JsonProperty("accent_color")] | [JsonProperty("accent_color")] | ||||
| public Optional<uint?> AccentColor { get; set; } | public Optional<uint?> AccentColor { get; set; } | ||||
| //CurrentUser | |||||
| [JsonProperty("verified")] | |||||
| public Optional<bool> Verified { get; set; } | |||||
| [JsonProperty("email")] | |||||
| public Optional<string> Email { get; set; } | |||||
| [JsonProperty("mfa_enabled")] | |||||
| public Optional<bool> MfaEnabled { get; set; } | |||||
| [JsonProperty("flags")] | |||||
| public Optional<UserProperties> Flags { get; set; } | |||||
| [JsonProperty("premium_type")] | |||||
| public Optional<PremiumType> PremiumType { get; set; } | |||||
| [JsonProperty("locale")] | |||||
| public Optional<string> Locale { get; set; } | |||||
| [JsonProperty("public_flags")] | |||||
| public Optional<UserProperties> 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<ulong>.Id => Id; | |||||
| } | } | ||||
| } | } | ||||
| @@ -151,6 +151,16 @@ namespace Discord.Rest | |||||
| return null; | return null; | ||||
| } | } | ||||
| public static async Task<IReadOnlyCollection<RestGuildUser>> 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<RestWebhook> GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) | public static async Task<RestWebhook> GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) | ||||
| { | { | ||||
| var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); | var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); | ||||
| @@ -2063,10 +2063,10 @@ namespace Discord.API | |||||
| #endregion | #endregion | ||||
| #region Current User/DMs | #region Current User/DMs | ||||
| public async Task<User> GetMyUserAsync(RequestOptions options = null) | |||||
| public async Task<CurrentUser> GetMyUserAsync(RequestOptions options = null) | |||||
| { | { | ||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| return await SendAsync<User>("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); | |||||
| return await SendAsync<CurrentUser>("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task<IReadOnlyCollection<Connection>> GetMyConnectionsAsync(RequestOptions options = null) | public async Task<IReadOnlyCollection<Connection>> GetMyConnectionsAsync(RequestOptions options = null) | ||||
| { | { | ||||
| @@ -185,6 +185,8 @@ namespace Discord.Rest | |||||
| => ClientHelper.GetUserAsync(this, id, options); | => ClientHelper.GetUserAsync(this, id, options); | ||||
| public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | ||||
| => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | ||||
| public Task<IReadOnlyCollection<RestGuildUser>> GetGuildUsersAsync(ulong guildId, RequestOptions options = null) | |||||
| => ClientHelper.GetGuildUsersAsync(this, guildId, options); | |||||
| public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetVoiceRegionsAsync(this, options); | => ClientHelper.GetVoiceRegionsAsync(this, options); | ||||
| @@ -94,6 +94,7 @@ namespace Discord.Rest | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| base.Update(model.User); | base.Update(model.User); | ||||
| if (model.JoinedAt.IsSpecified) | if (model.JoinedAt.IsSpecified) | ||||
| _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | ||||
| if (model.Nick.IsSpecified) | if (model.Nick.IsSpecified) | ||||
| @@ -1,7 +1,8 @@ | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | |||||
| using UserModel = Discord.API.User; | |||||
| using Model = Discord.API.CurrentUser; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| @@ -28,29 +29,26 @@ namespace Discord.Rest | |||||
| : base(discord, 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); | var entity = new RestSelfUser(discord, model.Id); | ||||
| entity.Update(model); | entity.Update(model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Update(Model model) | |||||
| internal override void Update(UserModel model) | |||||
| { | { | ||||
| base.Update(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(); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -78,20 +78,16 @@ namespace Discord.Rest | |||||
| internal virtual void Update(Model model) | 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); | 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; | |||||
| } | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -6,6 +6,23 @@ namespace Discord.Rest | |||||
| { | { | ||||
| internal static class EntityExtensions | 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) | public static IEmote ToIEmote(this API.Emoji model) | ||||
| { | { | ||||
| if (model.Id.HasValue) | if (model.Id.HasValue) | ||||
| @@ -18,6 +35,7 @@ namespace Discord.Rest | |||||
| model.Name, | model.Name, | ||||
| model.Animated.GetValueOrDefault(), | model.Animated.GetValueOrDefault(), | ||||
| model.Managed, | model.Managed, | ||||
| model.Available.GetValueOrDefault(), | |||||
| model.RequireColons, | model.RequireColons, | ||||
| ImmutableArray.Create(model.Roles), | ImmutableArray.Create(model.Roles), | ||||
| model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); | model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); | ||||
| @@ -170,48 +188,5 @@ namespace Discord.Rest | |||||
| { | { | ||||
| return new Overwrite(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)); | 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<string>.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<UserProperties>.Unspecified, | |||||
| Id = command.User.Id, | |||||
| } | |||||
| }; | |||||
| } | |||||
| return messageModel; | |||||
| } | |||||
| return new API.Message | |||||
| { | |||||
| Id = interaction.Id, | |||||
| }; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -17,7 +17,7 @@ namespace Discord.API.Gateway | |||||
| [JsonProperty("v")] | [JsonProperty("v")] | ||||
| public int Version { get; set; } | public int Version { get; set; } | ||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public User User { get; set; } | |||||
| public CurrentUser User { get; set; } | |||||
| [JsonProperty("session_id")] | [JsonProperty("session_id")] | ||||
| public string SessionId { get; set; } | public string SessionId { get; set; } | ||||
| [JsonProperty("read_state")] | [JsonProperty("read_state")] | ||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// The cache should preform a synchronous cache lookup. | |||||
| /// </summary> | |||||
| Sync, | |||||
| /// <summary> | |||||
| /// The cache should preform either a <see cref="Sync"/> or asynchronous cache lookup. | |||||
| /// </summary> | |||||
| Async | |||||
| } | |||||
| } | |||||
| @@ -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<ulong, IUserModel> _users; | |||||
| private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IMemberModel>> _members; | |||||
| private readonly ConcurrentDictionary<ulong, IPresenceModel> _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<ulong, IMemberModel>()); | |||||
| guildMemberCache.AddOrUpdate(model.User.Id, model, (_, __) => model); | |||||
| return CompletedValueTask; | |||||
| } | |||||
| public ValueTask<IMemberModel> GetMemberAsync(ulong id, ulong guildId, CacheRunMode mode) | |||||
| => new ValueTask<IMemberModel>(_members.FirstOrDefault(x => x.Key == guildId).Value?.FirstOrDefault(x => x.Key == id).Value); | |||||
| public ValueTask<IEnumerable<IMemberModel>> GetMembersAsync(ulong guildId, CacheRunMode mode) | |||||
| { | |||||
| if(_members.TryGetValue(guildId, out var inner)) | |||||
| return new ValueTask<IEnumerable<IMemberModel>>(inner.ToArray().Select(x => x.Value)); // ToArray here is important before .Select due to concurrency | |||||
| return new ValueTask<IEnumerable<IMemberModel>>(Array.Empty<IMemberModel>()); | |||||
| } | |||||
| public ValueTask<IUserModel> GetUserAsync(ulong id, CacheRunMode mode) | |||||
| { | |||||
| if (_users.TryGetValue(id, out var result)) | |||||
| return new ValueTask<IUserModel>(result); | |||||
| return new ValueTask<IUserModel>((IUserModel)null); | |||||
| } | |||||
| public ValueTask<IEnumerable<IUserModel>> GetUsersAsync(CacheRunMode mode) | |||||
| => new ValueTask<IEnumerable<IUserModel>>(_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<IPresenceModel> GetPresenceAsync(ulong userId, CacheRunMode runmode) | |||||
| { | |||||
| if (_presense.TryGetValue(userId, out var presense)) | |||||
| return new ValueTask<IPresenceModel>(presense); | |||||
| return new ValueTask<IPresenceModel>((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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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<IUserModel> GetUserAsync(ulong id, CacheRunMode runmode); | |||||
| ValueTask<IEnumerable<IUserModel>> GetUsersAsync(CacheRunMode runmode); | |||||
| ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode runmode); | |||||
| ValueTask RemoveUserAsync(ulong id, CacheRunMode runmode); | |||||
| #endregion | |||||
| #region Members | |||||
| ValueTask<IMemberModel> GetMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); | |||||
| ValueTask<IEnumerable<IMemberModel>> 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<IPresenceModel> GetPresenceAsync(ulong userId, CacheRunMode runmode); | |||||
| ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode); | |||||
| ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode); | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| @@ -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<T> : 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<ulong, CacheWeakReference<SocketGlobalUser>> _userReferences = new(); | |||||
| private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), CacheWeakReference<SocketGuildUser>> _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<SocketGlobalUser>(user)); | |||||
| } | |||||
| } | |||||
| internal ValueTask<IUser> 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<ulong, SocketGlobalUser> 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<SocketGuildUser>(user)); | |||||
| } | |||||
| } | |||||
| internal void RemovedReferencedMember(ulong userId, ulong guildId) | |||||
| => _memberReferences.TryRemove((guildId, userId), out _); | |||||
| internal ValueTask<IGuildUser> 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<ulong, ulong, SocketGuildUser> 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<IGuildUser> 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 | |||||
| } | |||||
| } | |||||
| @@ -5,7 +5,7 @@ using System.Linq; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| internal class ClientState | |||||
| internal partial class ClientStateManager | |||||
| { | { | ||||
| private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | ||||
| private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | ||||
| @@ -30,8 +30,11 @@ namespace Discord.WebSocket | |||||
| _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | ||||
| .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | .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 estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | ||||
| double estimatedUsersCount = guildCount * AverageUsersPerGuild; | double estimatedUsersCount = guildCount * AverageUsersPerGuild; | ||||
| _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | ||||
| @@ -121,22 +124,6 @@ namespace Discord.WebSocket | |||||
| return null; | return null; | ||||
| } | } | ||||
| internal SocketGlobalUser GetUser(ulong id) | |||||
| { | |||||
| if (_users.TryGetValue(id, out SocketGlobalUser user)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| internal SocketGlobalUser GetOrAddUser(ulong id, Func<ulong, SocketGlobalUser> 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() | internal void PurgeUsers() | ||||
| { | { | ||||
| foreach (var guild in _guilds.Values) | foreach (var guild in _guilds.Values) | ||||
| @@ -200,7 +200,7 @@ namespace Discord.WebSocket | |||||
| return _shards[id]; | return _shards[id]; | ||||
| return null; | return null; | ||||
| } | } | ||||
| private int GetShardIdFor(ulong guildId) | |||||
| public int GetShardIdFor(ulong guildId) | |||||
| => (int)((guildId >> 22) % (uint)_totalShards); | => (int)((guildId >> 22) % (uint)_totalShards); | ||||
| public int GetShardIdFor(IGuild guild) | public int GetShardIdFor(IGuild guild) | ||||
| => GetShardIdFor(guild?.Id ?? 0); | => GetShardIdFor(guild?.Id ?? 0); | ||||
| @@ -25,6 +25,12 @@ namespace Discord.WebSocket | |||||
| /// </example> | /// </example> | ||||
| public class DiscordSocketConfig : DiscordRestConfig | public class DiscordSocketConfig : DiscordRestConfig | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Gets or sets the cache provider to use | |||||
| /// </summary> | |||||
| public ICacheProvider CacheProvider { get; set; } | |||||
| public IStateProvider StateProvider { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Returns the encoding gateway should use. | /// Returns the encoding gateway should use. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -193,6 +199,11 @@ namespace Discord.WebSocket | |||||
| /// </summary> | /// </summary> | ||||
| public bool SuppressUnknownDispatchWarnings { get; set; } = true; | public bool SuppressUnknownDispatchWarnings { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets or sets the default state behavior clients will use. | |||||
| /// </summary> | |||||
| public StateBehavior DefaultStateBehavior { get; set; } = StateBehavior.Default; | |||||
| /// <summary> | /// <summary> | ||||
| /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -37,7 +37,7 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, guild) | : 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); | var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -29,7 +29,7 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| /// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception> | /// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception> | ||||
| internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| { | { | ||||
| return model.Type switch | return model.Type switch | ||||
| { | { | ||||
| @@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||||
| _ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), | _ => 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 | #endregion | ||||
| #region User | #region User | ||||
| @@ -35,23 +35,23 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| Recipient = recipient; | 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])); | var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| Recipient.Update(state, model.Recipients.Value[0]); | Recipient.Update(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)); | var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); | ||||
| entity.Update(state, recipient); | entity.Update(state, recipient); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, API.User recipient) | |||||
| internal void Update(ClientStateManager state, API.User recipient) | |||||
| { | { | ||||
| Recipient.Update(state, recipient); | Recipient.Update(state, recipient); | ||||
| } | } | ||||
| @@ -55,13 +55,13 @@ namespace Discord.WebSocket | |||||
| _voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | _voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | ||||
| _users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | _users = new ConcurrentDictionary<ulong, SocketGroupUser>(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); | var entity = new SocketGroupChannel(discord, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| if (model.Name.IsSpecified) | if (model.Name.IsSpecified) | ||||
| Name = model.Name.Value; | Name = model.Name.Value; | ||||
| @@ -73,7 +73,7 @@ namespace Discord.WebSocket | |||||
| RTCRegion = model.RTCRegion.GetValueOrDefault(null); | RTCRegion = model.RTCRegion.GetValueOrDefault(null); | ||||
| } | } | ||||
| private void UpdateUsers(ClientState state, UserModel[] models) | |||||
| private void UpdateUsers(ClientStateManager state, UserModel[] models) | |||||
| { | { | ||||
| var users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); | var users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); | ||||
| for (int i = 0; i < models.Length; i++) | for (int i = 0; i < models.Length; i++) | ||||
| @@ -265,7 +265,7 @@ namespace Discord.WebSocket | |||||
| return user; | return user; | ||||
| else | else | ||||
| { | { | ||||
| var privateUser = SocketGroupUser.Create(this, Discord.State, model); | |||||
| var privateUser = SocketGroupUser.Create(this, Discord.StateManager, model); | |||||
| privateUser.GlobalUser.AddRef(); | privateUser.GlobalUser.AddRef(); | ||||
| _users[privateUser.Id] = privateUser; | _users[privateUser.Id] = privateUser; | ||||
| return privateUser; | return privateUser; | ||||
| @@ -283,7 +283,7 @@ namespace Discord.WebSocket | |||||
| #endregion | #endregion | ||||
| #region Voice States | #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 voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | ||||
| var voiceState = SocketVoiceState.Create(voiceChannel, model); | var voiceState = SocketVoiceState.Create(voiceChannel, model); | ||||
| @@ -49,7 +49,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| Guild = guild; | 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 | return model.Type switch | ||||
| { | { | ||||
| @@ -63,7 +63,7 @@ namespace Discord.WebSocket | |||||
| }; | }; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| Name = model.Name.Value; | Name = model.Name.Value; | ||||
| Position = model.Position.GetValueOrDefault(0); | Position = model.Position.GetValueOrDefault(0); | ||||
| @@ -21,7 +21,7 @@ namespace Discord.WebSocket | |||||
| :base(discord, id, guild) | :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); | var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -43,7 +43,7 @@ namespace Discord.WebSocket | |||||
| internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | ||||
| : base(discord, id, 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); | var entity = new SocketStageChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -63,13 +63,13 @@ namespace Discord.WebSocket | |||||
| if (Discord.MessageCacheSize > 0) | if (Discord.MessageCacheSize > 0) | ||||
| _messages = new MessageCache(Discord); | _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); | var entity = new SocketTextChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| CategoryId = model.CategoryId; | CategoryId = model.CategoryId; | ||||
| @@ -117,7 +117,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); | 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)) | if(Discord.AlwaysDownloadUsers && Discord.HasGatewayIntent(GatewayIntents.GuildMembers)) | ||||
| await thread.DownloadUsersAsync(); | await thread.DownloadUsersAsync(); | ||||
| @@ -118,7 +118,7 @@ namespace Discord.WebSocket | |||||
| CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); | 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 parent = guild.GetChannel(model.CategoryId.Value); | ||||
| var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); | var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); | ||||
| @@ -126,7 +126,7 @@ namespace Discord.WebSocket | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| @@ -55,14 +55,14 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, guild) | : 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); | var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| CategoryId = model.CategoryId; | CategoryId = model.CategoryId; | ||||
| @@ -14,11 +14,11 @@ using ChannelModel = Discord.API.Channel; | |||||
| using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | ||||
| using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | ||||
| using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; | using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; | ||||
| using MemberModel = Discord.API.GuildMember; | |||||
| using MemberModel = Discord.IMemberModel; | |||||
| using Model = Discord.API.Guild; | using Model = Discord.API.Guild; | ||||
| using PresenceModel = Discord.API.Presence; | using PresenceModel = Discord.API.Presence; | ||||
| using RoleModel = Discord.API.Role; | using RoleModel = Discord.API.Role; | ||||
| using UserModel = Discord.API.User; | |||||
| using UserModel = Discord.IUserModel; | |||||
| using VoiceStateModel = Discord.API.VoiceState; | using VoiceStateModel = Discord.API.VoiceState; | ||||
| using StickerModel = Discord.API.Sticker; | using StickerModel = Discord.API.Sticker; | ||||
| using EventModel = Discord.API.GuildScheduledEvent; | using EventModel = Discord.API.GuildScheduledEvent; | ||||
| @@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||||
| private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | ||||
| private TaskCompletionSource<AudioClient> _audioConnectPromise; | private TaskCompletionSource<AudioClient> _audioConnectPromise; | ||||
| private ConcurrentDictionary<ulong, SocketGuildChannel> _channels; | private ConcurrentDictionary<ulong, SocketGuildChannel> _channels; | ||||
| private ConcurrentDictionary<ulong, SocketGuildUser> _members; | |||||
| //private ConcurrentDictionary<ulong, SocketGuildUser> _members; | |||||
| private ConcurrentDictionary<ulong, SocketRole> _roles; | private ConcurrentDictionary<ulong, SocketRole> _roles; | ||||
| private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | ||||
| private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | ||||
| @@ -305,7 +305,7 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the current logged-in user. | /// Gets the current logged-in user. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; | |||||
| public SocketGuildUser CurrentUser => Discord.StateManager.GetMember(Discord.CurrentUser.Id, Id); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the built-in role containing all users in this guild. | /// Gets the built-in role containing all users in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -324,7 +324,7 @@ namespace Discord.WebSocket | |||||
| get | get | ||||
| { | { | ||||
| var channels = _channels; | var channels = _channels; | ||||
| var state = Discord.State; | |||||
| var state = Discord.StateManager; | |||||
| return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); | return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); | ||||
| } | } | ||||
| } | } | ||||
| @@ -356,7 +356,7 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A collection of guild users found within this guild. | /// A collection of guild users found within this guild. | ||||
| /// </returns> | /// </returns> | ||||
| public IReadOnlyCollection<SocketGuildUser> Users => _members.ToReadOnlyCollection(); | |||||
| public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.GetMembers(Id).Cast<SocketGuildUser>().ToImmutableArray(); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of all roles in this guild. | /// Gets a collection of all roles in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -382,13 +382,13 @@ namespace Discord.WebSocket | |||||
| _audioLock = new SemaphoreSlim(1, 1); | _audioLock = new SemaphoreSlim(1, 1); | ||||
| _emotes = ImmutableArray.Create<GuildEmote>(); | _emotes = ImmutableArray.Create<GuildEmote>(); | ||||
| } | } | ||||
| 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); | var entity = new SocketGuild(discord, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, ExtendedModel model) | |||||
| internal void Update(ClientStateManager state, ExtendedModel model) | |||||
| { | { | ||||
| IsAvailable = !(model.Unavailable ?? false); | IsAvailable = !(model.Unavailable ?? false); | ||||
| if (!IsAvailable) | if (!IsAvailable) | ||||
| @@ -397,8 +397,6 @@ namespace Discord.WebSocket | |||||
| _events = new ConcurrentDictionary<ulong, SocketGuildEvent>(); | _events = new ConcurrentDictionary<ulong, SocketGuildEvent>(); | ||||
| if (_channels == null) | if (_channels == null) | ||||
| _channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); | _channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); | ||||
| if (_members == null) | |||||
| _members = new ConcurrentDictionary<ulong, SocketGuildUser>(); | |||||
| if (_roles == null) | if (_roles == null) | ||||
| _roles = new ConcurrentDictionary<ulong, SocketRole>(); | _roles = new ConcurrentDictionary<ulong, SocketRole>(); | ||||
| /*if (Emojis == null) | /*if (Emojis == null) | ||||
| @@ -431,25 +429,6 @@ namespace Discord.WebSocket | |||||
| _channels = channels; | _channels = channels; | ||||
| var members = new ConcurrentDictionary<ulong, SocketGuildUser>(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<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); | var voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); | ||||
| { | { | ||||
| for (int i = 0; i < model.VoiceStates.Length; i++) | for (int i = 0; i < model.VoiceStates.Length; i++) | ||||
| @@ -473,6 +452,19 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| _events = events; | _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<bool>(); | _syncPromise = new TaskCompletionSource<bool>(); | ||||
| _downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
| @@ -480,7 +472,7 @@ namespace Discord.WebSocket | |||||
| /*if (!model.Large) | /*if (!model.Large) | ||||
| _ = _downloaderPromise.TrySetResultAsync(true);*/ | _ = _downloaderPromise.TrySetResultAsync(true);*/ | ||||
| } | } | ||||
| internal void Update(ClientState state, Model model) | |||||
| internal void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| AFKChannelId = model.AFKChannelId; | AFKChannelId = model.AFKChannelId; | ||||
| if (model.WidgetChannelId.IsSpecified) | if (model.WidgetChannelId.IsSpecified) | ||||
| @@ -561,7 +553,7 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(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<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); | var members = new ConcurrentDictionary<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); | ||||
| { | { | ||||
| @@ -585,7 +577,7 @@ namespace Discord.WebSocket | |||||
| // _ = _downloaderPromise.TrySetResultAsync(true); | // _ = _downloaderPromise.TrySetResultAsync(true); | ||||
| }*/ | }*/ | ||||
| internal void Update(ClientState state, EmojiUpdateModel model) | |||||
| internal void Update(ClientStateManager state, EmojiUpdateModel model) | |||||
| { | { | ||||
| var emotes = ImmutableArray.CreateBuilder<GuildEmote>(model.Emojis.Length); | var emotes = ImmutableArray.CreateBuilder<GuildEmote>(model.Emojis.Length); | ||||
| for (int i = 0; i < model.Emojis.Length; i++) | for (int i = 0; i < model.Emojis.Length; i++) | ||||
| @@ -682,7 +674,7 @@ namespace Discord.WebSocket | |||||
| /// </returns> | /// </returns> | ||||
| public SocketGuildChannel GetChannel(ulong id) | 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) | if (channel?.Guild.Id == Id) | ||||
| return channel; | return channel; | ||||
| return null; | return null; | ||||
| @@ -799,7 +791,7 @@ namespace Discord.WebSocket | |||||
| public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | ||||
| => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); | => 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); | var channel = SocketGuildChannel.Create(this, state, model); | ||||
| _channels.TryAdd(model.Id, channel); | _channels.TryAdd(model.Id, channel); | ||||
| @@ -807,26 +799,26 @@ namespace Discord.WebSocket | |||||
| return channel; | return channel; | ||||
| } | } | ||||
| internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) | |||||
| internal SocketGuildChannel AddOrUpdateChannel(ClientStateManager state, ChannelModel model) | |||||
| { | { | ||||
| if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) | if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) | ||||
| channel.Update(Discord.State, model); | |||||
| channel.Update(Discord.StateManager, model); | |||||
| else | else | ||||
| { | { | ||||
| channel = SocketGuildChannel.Create(this, Discord.State, model); | |||||
| channel = SocketGuildChannel.Create(this, Discord.StateManager, model); | |||||
| _channels[channel.Id] = channel; | _channels[channel.Id] = channel; | ||||
| state.AddChannel(channel); | state.AddChannel(channel); | ||||
| } | } | ||||
| return channel; | return channel; | ||||
| } | } | ||||
| internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) | |||||
| internal SocketGuildChannel RemoveChannel(ClientStateManager state, ulong id) | |||||
| { | { | ||||
| if (_channels.TryRemove(id, out var _)) | if (_channels.TryRemove(id, out var _)) | ||||
| return state.RemoveChannel(id) as SocketGuildChannel; | return state.RemoveChannel(id) as SocketGuildChannel; | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal void PurgeChannelCache(ClientState state) | |||||
| internal void PurgeChannelCache(ClientStateManager state) | |||||
| { | { | ||||
| foreach (var channelId in _channels) | foreach (var channelId in _channels) | ||||
| state.RemoveChannel(channelId.Key); | state.RemoveChannel(channelId.Key); | ||||
| @@ -880,7 +872,7 @@ namespace Discord.WebSocket | |||||
| foreach (var command in commands) | foreach (var command in commands) | ||||
| { | { | ||||
| Discord.State.AddCommand(command); | |||||
| Discord.StateManager.AddCommand(command); | |||||
| } | } | ||||
| return commands.ToImmutableArray(); | return commands.ToImmutableArray(); | ||||
| @@ -898,7 +890,7 @@ namespace Discord.WebSocket | |||||
| /// </returns> | /// </returns> | ||||
| public async ValueTask<SocketApplicationCommand> GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | public async ValueTask<SocketApplicationCommand> 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) | if (command != null) | ||||
| return command; | return command; | ||||
| @@ -913,7 +905,7 @@ namespace Discord.WebSocket | |||||
| command = SocketApplicationCommand.Create(Discord, model, Id); | command = SocketApplicationCommand.Create(Discord, model, Id); | ||||
| Discord.State.AddCommand(command); | |||||
| Discord.StateManager.AddCommand(command); | |||||
| return command; | return command; | ||||
| } | } | ||||
| @@ -930,7 +922,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); | 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); | entity.Update(model); | ||||
| @@ -952,11 +944,11 @@ namespace Discord.WebSocket | |||||
| var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); | 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) | foreach(var entity in entities) | ||||
| { | { | ||||
| Discord.State.AddCommand(entity); | |||||
| Discord.StateManager.AddCommand(entity); | |||||
| } | } | ||||
| return entities.ToImmutableArray(); | return entities.ToImmutableArray(); | ||||
| @@ -1020,7 +1012,7 @@ namespace Discord.WebSocket | |||||
| => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); | => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); | ||||
| internal SocketRole AddRole(RoleModel model) | 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; | _roles[model.Id] = role; | ||||
| return role; | return role; | ||||
| } | } | ||||
| @@ -1034,7 +1026,7 @@ namespace Discord.WebSocket | |||||
| internal SocketRole AddOrUpdateRole(RoleModel model) | internal SocketRole AddOrUpdateRole(RoleModel model) | ||||
| { | { | ||||
| if (_roles.TryGetValue(model.Id, out SocketRole role)) | if (_roles.TryGetValue(model.Id, out SocketRole role)) | ||||
| _roles[model.Id].Update(Discord.State, model); | |||||
| _roles[model.Id].Update(Discord.StateManager, model); | |||||
| else | else | ||||
| role = AddRole(model); | role = AddRole(model); | ||||
| @@ -1089,60 +1081,45 @@ namespace Discord.WebSocket | |||||
| /// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. | /// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. | ||||
| /// </returns> | /// </returns> | ||||
| public SocketGuildUser GetUser(ulong id) | public SocketGuildUser GetUser(ulong id) | ||||
| { | |||||
| if (_members.TryGetValue(id, out SocketGuildUser member)) | |||||
| return member; | |||||
| return null; | |||||
| } | |||||
| => Discord.StateManager.GetMember(id, Id); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null) | public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null) | ||||
| => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); | => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); | ||||
| internal SocketGuildUser AddOrUpdateUser(UserModel model) | 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 | else | ||||
| { | { | ||||
| member = SocketGuildUser.Create(this, Discord.State, model); | |||||
| member = SocketGuildUser.Create(Id, Discord, model); | |||||
| member.GlobalUser.AddRef(); | member.GlobalUser.AddRef(); | ||||
| _members[member.Id] = member; | |||||
| DownloadedMemberCount++; | DownloadedMemberCount++; | ||||
| } | } | ||||
| return member; | return member; | ||||
| } | } | ||||
| internal SocketGuildUser AddOrUpdateUser(MemberModel model) | 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 | else | ||||
| { | { | ||||
| member = SocketGuildUser.Create(this, Discord.State, model); | |||||
| member = SocketGuildUser.Create(Id, Discord, model); | |||||
| member.GlobalUser.AddRef(); | 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++; | DownloadedMemberCount++; | ||||
| } | } | ||||
| return member; | return member; | ||||
| } | } | ||||
| internal SocketGuildUser RemoveUser(ulong id) | internal SocketGuildUser RemoveUser(ulong id) | ||||
| { | { | ||||
| if (_members.TryRemove(id, out SocketGuildUser member)) | |||||
| SocketGuildUser member; | |||||
| if ((member = GetUser(id)) != null) | |||||
| { | { | ||||
| DownloadedMemberCount--; | DownloadedMemberCount--; | ||||
| member.GlobalUser.RemoveRef(Discord); | member.GlobalUser.RemoveRef(Discord); | ||||
| Discord.StateManager.RemoveMember(id, Id); | |||||
| return member; | return member; | ||||
| } | } | ||||
| return null; | return null; | ||||
| @@ -1158,18 +1135,16 @@ namespace Discord.WebSocket | |||||
| /// <param name="predicate">The predicate used to select which users to clear.</param> | /// <param name="predicate">The predicate used to select which users to clear.</param> | ||||
| public void PurgeUserCache(Func<SocketGuildUser, bool> predicate) | public void PurgeUserCache(Func<SocketGuildUser, bool> 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<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
| DownloadedMemberCount = _members.Count; | |||||
| DownloadedMemberCount = membersToKeep.Count(); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -1537,7 +1512,7 @@ namespace Discord.WebSocket | |||||
| #endregion | #endregion | ||||
| #region Voice States | #region Voice States | ||||
| internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | |||||
| internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientStateManager state, VoiceStateModel model) | |||||
| { | { | ||||
| var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | ||||
| var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; | var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; | ||||
| @@ -89,13 +89,13 @@ namespace Discord.WebSocket | |||||
| if(guildUser != null) | if(guildUser != null) | ||||
| { | { | ||||
| if(model.Creator.IsSpecified) | if(model.Creator.IsSpecified) | ||||
| guildUser.Update(Discord.State, model.Creator.Value); | |||||
| guildUser.Update(Discord.StateManager, model.Creator.Value); | |||||
| Creator = guildUser; | Creator = guildUser; | ||||
| } | } | ||||
| else if (guildUser == null && model.Creator.IsSpecified) | 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; | Creator = guildUser; | ||||
| } | } | ||||
| } | } | ||||
| @@ -56,18 +56,18 @@ namespace Discord.WebSocket | |||||
| if (Channel is SocketGuildChannel channel) | if (Channel is SocketGuildChannel channel) | ||||
| { | { | ||||
| if (model.Message.Value.WebhookId.IsSpecified) | 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) | else if (model.Message.Value.Author.IsSpecified) | ||||
| author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); | author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); | ||||
| } | } | ||||
| else if (model.Message.Value.Author.IsSpecified) | else if (model.Message.Value.Author.IsSpecified) | ||||
| author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); | 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 | else | ||||
| { | { | ||||
| Message.Update(Discord.State, model.Message.Value); | |||||
| Message.Update(Discord.StateManager, model.Message.Value); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -29,7 +29,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| foreach (var user in resolved.Users.Value) | 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); | Users.Add(ulong.Parse(user.Key), socketUser); | ||||
| } | } | ||||
| @@ -50,11 +50,11 @@ namespace Discord.WebSocket | |||||
| : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); | : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); | ||||
| socketChannel = guild != null | 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); | Channels.Add(ulong.Parse(channel.Key), socketChannel); | ||||
| } | } | ||||
| } | } | ||||
| @@ -88,7 +88,7 @@ namespace Discord.WebSocket | |||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| if (msg.Value.WebhookId.IsSpecified) | 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 | else | ||||
| author = guild.GetUser(msg.Value.Author.Value.Id); | author = guild.GetUser(msg.Value.Author.Value.Id); | ||||
| } | } | ||||
| @@ -99,11 +99,11 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (!msg.Value.GuildId.IsSpecified) // assume it is a DM | 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); | Messages.Add(message.Id, message); | ||||
| } | } | ||||
| } | } | ||||
| @@ -129,7 +129,7 @@ namespace Discord.WebSocket | |||||
| Author = author; | Author = author; | ||||
| Source = source; | 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 || | if (model.Type == MessageType.Default || | ||||
| model.Type == MessageType.Reply || | model.Type == MessageType.Reply || | ||||
| @@ -140,7 +140,7 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| return SocketSystemMessage.Create(discord, state, author, channel, model); | 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; | Type = model.Type; | ||||
| @@ -13,13 +13,13 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, channel, author, MessageSource.System) | : 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); | var entity = new SocketSystemMessage(discord, model.Id, channel, author); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| } | } | ||||
| @@ -53,14 +53,14 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, channel, author, source) | : 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)); | var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| @@ -67,13 +67,13 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| Guild = guild; | 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); | var entity = new SocketRole(guild, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, Model model) | |||||
| internal void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| Name = model.Name; | Name = model.Name; | ||||
| IsHoisted = model.Hoist; | IsHoisted = model.Hoist; | ||||
| @@ -1,18 +1,17 @@ | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.User; | |||||
| using Model = Discord.IUserModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| internal class SocketGlobalUser : SocketUser | |||||
| internal class SocketGlobalUser : SocketUser, IDisposable | |||||
| { | { | ||||
| public override bool IsBot { get; internal set; } | public override bool IsBot { get; internal set; } | ||||
| public override string Username { get; internal set; } | public override string Username { get; internal set; } | ||||
| public override ushort DiscriminatorValue { get; internal set; } | public override ushort DiscriminatorValue { get; internal set; } | ||||
| public override string AvatarId { get; internal set; } | public override string AvatarId { get; internal set; } | ||||
| internal override SocketPresence Presence { get; set; } | |||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } | internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } | ||||
| @@ -23,8 +22,9 @@ namespace Discord.WebSocket | |||||
| private SocketGlobalUser(DiscordSocketClient discord, ulong id) | private SocketGlobalUser(DiscordSocketClient discord, ulong id) | ||||
| : base(discord, 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); | var entity = new SocketGlobalUser(discord, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -48,6 +48,9 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | ||||
| internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | ||||
| } | } | ||||
| @@ -30,7 +30,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| @@ -41,7 +41,7 @@ namespace Discord.WebSocket | |||||
| Channel = channel; | Channel = channel; | ||||
| GlobalUser = globalUser; | 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)); | var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -6,9 +6,9 @@ using System.Collections.Immutable; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | 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 | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -16,19 +16,24 @@ namespace Discord.WebSocket | |||||
| /// Represents a WebSocket-based guild user. | /// Represents a WebSocket-based guild user. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketGuildUser : SocketUser, IGuildUser | |||||
| public class SocketGuildUser : SocketUser, IGuildUser, ICached<MemberModel>, IDisposable | |||||
| { | { | ||||
| #region SocketGuildUser | #region SocketGuildUser | ||||
| private long? _premiumSinceTicks; | private long? _premiumSinceTicks; | ||||
| private long? _timedOutTicks; | private long? _timedOutTicks; | ||||
| private long? _joinedAtTicks; | private long? _joinedAtTicks; | ||||
| private ImmutableArray<ulong> _roleIds; | private ImmutableArray<ulong> _roleIds; | ||||
| private ulong _guildId; | |||||
| internal override SocketGlobalUser GlobalUser { get; set; } | internal override SocketGlobalUser GlobalUser { get; set; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild the user is in. | /// Gets the guild the user is in. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketGuild Guild { get; } | |||||
| public Lazy<SocketGuild> Guild { get; } | |||||
| /// <summary> | |||||
| /// Gets the guilds id that the user is in. | |||||
| /// </summary> | |||||
| public ulong GuildId => _guildId; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string DisplayName => Nickname ?? Username; | public string DisplayName => Nickname ?? Username; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -47,8 +52,7 @@ namespace Discord.WebSocket | |||||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| 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)); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| @@ -78,7 +82,7 @@ namespace Discord.WebSocket | |||||
| /// Returns a collection of roles that the user possesses. | /// Returns a collection of roles that the user possesses. | ||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SocketRole> Roles | public IReadOnlyCollection<SocketRole> 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); | |||||
| /// <summary> | /// <summary> | ||||
| /// Returns the voice channel the user is in, or <c>null</c> if none. | /// Returns the voice channel the user is in, or <c>null</c> if none. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -92,8 +96,8 @@ namespace Discord.WebSocket | |||||
| /// A <see cref="SocketVoiceState" /> representing the user's voice status; <c>null</c> if the user is not | /// A <see cref="SocketVoiceState" /> representing the user's voice status; <c>null</c> if the user is not | ||||
| /// connected to a voice channel. | /// connected to a voice channel. | ||||
| /// </returns> | /// </returns> | ||||
| 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); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); | public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -119,13 +123,13 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| get | get | ||||
| { | { | ||||
| if (Guild.OwnerId == Id) | |||||
| if (Guild.Value.OwnerId == Id) | |||||
| return int.MaxValue; | return int.MaxValue; | ||||
| int maxPos = 0; | int maxPos = 0; | ||||
| for (int i = 0; i < _roleIds.Length; i++) | 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) | if (role != null && role.Position > maxPos) | ||||
| maxPos = role.Position; | maxPos = role.Position; | ||||
| } | } | ||||
| @@ -133,79 +137,46 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| 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<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||||
| GlobalUser = globalUser; | 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<ulong>()); | |||||
| return entity; | 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; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, MemberModel model) | |||||
| internal void Update(ClientStateManager state, MemberModel model) | |||||
| { | { | ||||
| base.Update(state, model.User); | 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) | private void UpdateRoles(ulong[] roleIds) | ||||
| { | { | ||||
| var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | ||||
| roles.Add(Guild.Id); | |||||
| roles.Add(_guildId); | |||||
| for (int i = 0; i < roleIds.Length; i++) | for (int i = 0; i < roleIds.Length; i++) | ||||
| roles.Add(roleIds[i]); | roles.Add(roleIds[i]); | ||||
| _roleIds = roles.ToImmutable(); | _roleIds = roles.ToImmutable(); | ||||
| @@ -249,7 +220,7 @@ namespace Discord.WebSocket | |||||
| => UserHelper.RemoveTimeOutAsync(this, Discord, options); | => UserHelper.RemoveTimeOutAsync(this, Discord, options); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) | public ChannelPermissions GetPermissions(IGuildChannel channel) | ||||
| => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); | |||||
| => new ChannelPermissions(Permissions.ResolveChannel(Guild.Value, this, channel, GuildPermissions.RawValue)); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
| @@ -259,7 +230,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | 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)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | ||||
| @@ -269,13 +240,14 @@ namespace Discord.WebSocket | |||||
| clone.GlobalUser = GlobalUser.Clone(); | clone.GlobalUser = GlobalUser.Clone(); | ||||
| return clone; | return clone; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region IGuildUser | #region IGuildUser | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IGuild IGuildUser.Guild => Guild; | |||||
| IGuild IGuildUser.Guild => Guild.Value; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| ulong IGuildUser.GuildId => Guild.Id; | |||||
| ulong IGuildUser.GuildId => _guildId; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IReadOnlyCollection<ulong> IGuildUser.RoleIds => _roleIds; | IReadOnlyCollection<ulong> IGuildUser.RoleIds => _roleIds; | ||||
| @@ -283,5 +255,55 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; | IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; | ||||
| #endregion | #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<MemberModel>.ToModel() | |||||
| => ToMemberModel(); | |||||
| internal MemberModel ToMemberModel() | |||||
| { | |||||
| return new CacheModel | |||||
| { | |||||
| User = ((ICached<UserModel>)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 | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,7 +3,7 @@ using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.Presence; | |||||
| using Model = Discord.IPresenceModel; | |||||
| namespace Discord.WebSocket | 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. | /// Represents the WebSocket user's presence status. This may include their online status and their activity. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketPresence : IPresence | |||||
| public class SocketPresence : IPresence, ICached<Model> | |||||
| { | { | ||||
| internal ulong UserId; | |||||
| internal ulong? GuildId; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status { get; private set; } | public UserStatus Status { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -38,8 +41,10 @@ namespace Discord.WebSocket | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| Status = model.Status; | Status = model.Status; | ||||
| ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray<ClientType>.Empty; | |||||
| ActiveClients = model.ActiveClients.Length > 0 ? model.ActiveClients.ToImmutableArray() : ImmutableArray<ClientType>.Empty; | |||||
| Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | ||||
| UserId = model.UserId; | |||||
| GuildId = model.GuildId; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -76,9 +81,9 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A list of all <see cref="IActivity"/> that this user currently has available. | /// A list of all <see cref="IActivity"/> that this user currently has available. | ||||
| /// </returns> | /// </returns> | ||||
| private static IImmutableList<IActivity> ConvertActivitiesList(IList<API.Game> activities) | |||||
| private static IImmutableList<IActivity> ConvertActivitiesList(IActivityModel[] activities) | |||||
| { | { | ||||
| if (activities == null || activities.Count == 0) | |||||
| if (activities == null || activities.Length == 0) | |||||
| return ImmutableList<IActivity>.Empty; | return ImmutableList<IActivity>.Empty; | ||||
| var list = new List<IActivity>(); | var list = new List<IActivity>(); | ||||
| foreach (var activity in activities) | foreach (var activity in activities) | ||||
| @@ -96,5 +101,61 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | ||||
| internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | 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<WritableActivityModel>(); | |||||
| case SpotifyGame spotify: | |||||
| return spotify.ToModel<WritableActivityModel>(); | |||||
| case CustomStatusGame custom: | |||||
| return custom.ToModel<WritableActivityModel, WritableEmojiModel>(); | |||||
| case StreamingGame stream: | |||||
| return stream.ToModel<WritableActivityModel>(); | |||||
| } | |||||
| break; | |||||
| } | |||||
| return new WritableActivityModel | |||||
| { | |||||
| Name = x.Name, | |||||
| Details = x.Details, | |||||
| Flags = x.Flags, | |||||
| Type = x.Type | |||||
| }; | |||||
| }).ToArray(), | |||||
| }; | |||||
| } | |||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,8 @@ using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | |||||
| using Model = Discord.ICurrentUserModel; | |||||
| using UserModel = Discord.IUserModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -10,7 +11,7 @@ namespace Discord.WebSocket | |||||
| /// Represents the logged-in WebSocket-based user. | /// Represents the logged-in WebSocket-based user. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketSelfUser : SocketUser, ISelfUser | |||||
| public class SocketSelfUser : SocketUser, ISelfUser, ICached<Model> | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Email { get; private set; } | public string Email { get; private set; } | ||||
| @@ -29,7 +30,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserProperties Flags { get; internal set; } | public UserProperties Flags { get; internal set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -45,43 +46,47 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| GlobalUser = 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)); | var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override bool Update(ClientState state, Model model) | |||||
| internal override bool Update(ClientStateManager state, UserModel model) | |||||
| { | { | ||||
| bool hasGlobalChanges = base.Update(state, 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; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.Verified.IsSpecified) | |||||
| if (currentUserModel.IsVerified.HasValue) | |||||
| { | { | ||||
| IsVerified = model.Verified.Value; | |||||
| IsVerified = currentUserModel.IsVerified.Value; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.MfaEnabled.IsSpecified) | |||||
| if (currentUserModel.IsMfaEnabled.HasValue) | |||||
| { | { | ||||
| IsMfaEnabled = model.MfaEnabled.Value; | |||||
| IsMfaEnabled = currentUserModel.IsMfaEnabled.Value; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.Flags.IsSpecified && model.Flags.Value != Flags) | |||||
| if (currentUserModel.Flags != Flags) | |||||
| { | { | ||||
| Flags = (UserProperties)model.Flags.Value; | |||||
| Flags = currentUserModel.Flags; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType) | |||||
| if (currentUserModel.PremiumType != PremiumType) | |||||
| { | { | ||||
| PremiumType = model.PremiumType.Value; | |||||
| PremiumType = currentUserModel.PremiumType; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.Locale.IsSpecified && model.Locale.Value != Locale) | |||||
| if (currentUserModel.Locale != Locale) | |||||
| { | { | ||||
| Locale = model.Locale.Value; | |||||
| Locale = currentUserModel.Locale; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| return hasGlobalChanges; | return hasGlobalChanges; | ||||
| @@ -93,5 +98,55 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | ||||
| internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | 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<Model>.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 | |||||
| } | } | ||||
| } | } | ||||
| @@ -227,7 +227,7 @@ namespace Discord.WebSocket | |||||
| internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } | 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<SocketPresence> Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild user of this thread user. | /// Gets the guild user of this thread user. | ||||
| @@ -26,7 +26,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } | |||||
| internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| /// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception> | /// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception> | ||||
| internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | ||||
| @@ -35,7 +35,7 @@ namespace Discord.WebSocket | |||||
| : base(discord, 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); | var entity = new SocketUnknownUser(discord, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -6,8 +6,8 @@ using System.Globalization; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using Model = Discord.API.User; | |||||
| using PresenceModel = Discord.API.Presence; | |||||
| using Model = Discord.IUserModel; | |||||
| using PresenceModel = Discord.IPresenceModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -15,7 +15,7 @@ namespace Discord.WebSocket | |||||
| /// Represents a WebSocket-based user. | /// Represents a WebSocket-based user. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public abstract class SocketUser : SocketEntity<ulong>, IUser | |||||
| public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model> | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract bool IsBot { get; internal set; } | public abstract bool IsBot { get; internal set; } | ||||
| @@ -30,7 +30,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserProperties? PublicFlags { get; private set; } | public UserProperties? PublicFlags { get; private set; } | ||||
| internal abstract SocketGlobalUser GlobalUser { get; set; } | internal abstract SocketGlobalUser GlobalUser { get; set; } | ||||
| internal abstract SocketPresence Presence { get; set; } | |||||
| internal virtual Lazy<SocketPresence> Presence { get; set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| @@ -39,11 +39,11 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Mention => MentionUtils.MentionUser(Id); | public string Mention => MentionUtils.MentionUser(Id); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status => Presence.Status; | |||||
| public UserStatus Status => Presence.Value.Status; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.Value.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Value.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets mutual guilds shared with this user. | /// Gets mutual guilds shared with this user. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -57,46 +57,45 @@ namespace Discord.WebSocket | |||||
| : base(discord, 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<SocketPresence>(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); | |||||
| bool hasChanges = false; | bool hasChanges = false; | ||||
| if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) | |||||
| if (model.Avatar != AvatarId) | |||||
| { | { | ||||
| AvatarId = model.Avatar.Value; | |||||
| AvatarId = model.Avatar; | |||||
| hasChanges = true; | 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) | if (newVal != DiscriminatorValue) | ||||
| { | { | ||||
| DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | |||||
| DiscriminatorValue = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); | |||||
| hasChanges = true; | 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; | hasChanges = true; | ||||
| } | } | ||||
| if (model.Username.IsSpecified && model.Username.Value != Username) | |||||
| if (model.Username != Username) | |||||
| { | { | ||||
| Username = model.Username.Value; | |||||
| Username = model.Username; | |||||
| hasChanges = true; | 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; | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -120,5 +119,36 @@ namespace Discord.WebSocket | |||||
| public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | ||||
| private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
| internal SocketUser Clone() => MemberwiseClone() as SocketUser; | 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<Model>.ToModel() | |||||
| => ToModel(); | |||||
| internal Model ToModel() | |||||
| { | |||||
| return new CacheModel | |||||
| { | |||||
| Avatar = AvatarId, | |||||
| Discriminator = Discriminator, | |||||
| Id = Id, | |||||
| IsBot = IsBot, | |||||
| Username = Username | |||||
| }; | |||||
| } | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -33,7 +33,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => true; | public override bool IsWebhook => true; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } | |||||
| internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | |||||
| internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | ||||
| internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | ||||
| @@ -42,7 +42,7 @@ namespace Discord.WebSocket | |||||
| Guild = guild; | Guild = guild; | ||||
| WebhookId = webhookId; | 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); | var entity = new SocketWebhookUser(guild, model.Id, webhookId); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -7,86 +7,97 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| internal static class EntityExtensions | internal static class EntityExtensions | ||||
| { | { | ||||
| public static IActivity ToEntity(this API.Game model) | |||||
| public static IActivity ToEntity(this IActivityModel model) | |||||
| { | { | ||||
| #region Custom Status Game | #region Custom Status Game | ||||
| if (model.Id.IsSpecified && model.Id.Value == "custom") | |||||
| if (model.Id != null && model.Id == "custom") | |||||
| { | { | ||||
| return new CustomStatusGame() | return new CustomStatusGame() | ||||
| { | { | ||||
| Type = ActivityType.CustomStatus, | Type = ActivityType.CustomStatus, | ||||
| Name = model.Name, | 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 | #endregion | ||||
| #region Spotify Game | #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 | return new SpotifyGame | ||||
| { | { | ||||
| Name = model.Name, | 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, | 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, | AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, | ||||
| Type = ActivityType.Listening, | Type = ActivityType.Listening, | ||||
| Flags = model.Flags.GetValueOrDefault(), | |||||
| Flags = model.Flags, | |||||
| AlbumArt = model.LargeImage, | |||||
| }; | }; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region Rich Game | #region Rich Game | ||||
| if (model.ApplicationId.IsSpecified) | |||||
| if (model.ApplicationId.HasValue) | |||||
| { | { | ||||
| ulong appId = model.ApplicationId.Value; | ulong appId = model.ApplicationId.Value; | ||||
| var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); | |||||
| return new RichGame | return new RichGame | ||||
| { | { | ||||
| ApplicationId = appId, | ApplicationId = appId, | ||||
| Name = model.Name, | 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 | #endregion | ||||
| #region Stream Game | #region Stream Game | ||||
| if (model.StreamUrl.IsSpecified) | |||||
| if (model.Url != null) | |||||
| { | { | ||||
| return new StreamingGame( | return new StreamingGame( | ||||
| model.Name, | model.Name, | ||||
| model.StreamUrl.Value) | |||||
| model.Url) | |||||
| { | { | ||||
| Flags = model.Flags.GetValueOrDefault(), | |||||
| Details = model.Details.GetValueOrDefault() | |||||
| Flags = model.Flags, | |||||
| Details = model.Details | |||||
| }; | }; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region Normal Game | #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 | #endregion | ||||
| } | } | ||||
| @@ -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 | |||||
| }; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -19,13 +19,13 @@ namespace Discord.Interactions | |||||
| /// <param name="client">The underlying client.</param> | /// <param name="client">The underlying client.</param> | ||||
| /// <param name="interaction">The underlying interaction.</param> | /// <param name="interaction">The underlying interaction.</param> | ||||
| public ShardedInteractionContext (DiscordShardedClient client, TInteraction 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; | 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; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -45,7 +45,7 @@ namespace Discord.Interactions | |||||
| { | { | ||||
| Client = client; | Client = client; | ||||
| Channel = interaction.Channel; | Channel = interaction.Channel; | ||||
| Guild = (interaction.User as SocketGuildUser)?.Guild; | |||||
| Guild = (interaction.User as SocketGuildUser)?.Guild.Value; | |||||
| User = interaction.User; | User = interaction.User; | ||||
| Interaction = interaction; | Interaction = interaction; | ||||
| } | } | ||||
| @@ -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<TResult>(Task<TResult> t) | |||||
| { | |||||
| var sw = new SpinWait(); | |||||
| while (!t.IsCompleted) | |||||
| sw.SpinOnce(); | |||||
| return t.GetAwaiter().GetResult(); | |||||
| } | |||||
| private TType ValidateAsSocketEntity<TType>(ISnowflakeEntity entity) where TType : SocketEntity<ulong> | |||||
| { | |||||
| 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<SocketGuildUser>(user); | |||||
| var model = socketGuildUser.ToMemberModel(); | |||||
| RunAsyncWithLogs(_cache.AddOrUpdateMemberAsync(model, guildId, CacheRunMode.Async)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask AddOrUpdateUserAsync(IUser user) | |||||
| { | |||||
| var socketUser = ValidateAsSocketEntity<SocketUser>(user); | |||||
| var model = socketUser.ToModel(); | |||||
| RunAsyncWithLogs(_cache.AddOrUpdateUserAsync(model, CacheRunMode.Async)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IGuildUser> 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<IGuildUser>(SocketGuildUser.Create(guildId, _client, model)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return new ValueTask<IGuildUser>(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<IGuildUser>(_client.Rest.GetGuildUserAsync(guildId, id, options).ContinueWith(x => (IGuildUser)x.Result)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IEnumerable<IGuildUser>> 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<IEnumerable<IGuildUser>>(memberLookupTask.Result?.Select(x => SocketGuildUser.Create(guildId, _client, x))); | |||||
| else | |||||
| { | |||||
| return new ValueTask<IEnumerable<IGuildUser>>(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<IUser> 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<IUser>(SocketGlobalUser.Create(_client, null, model)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return new ValueTask<IUser>(Task.Run<IUser>(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<IUser>(_client.Rest.GetUserAsync(id, options).ContinueWith(x => (IUser)x.Result)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IEnumerable<IUser>> 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<IEnumerable<IUser>>(usersTask.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x))); | |||||
| else | |||||
| { | |||||
| return new ValueTask<IEnumerable<IUser>>(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<IPresence> 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<IPresence>(SocketPresence.Create(fetchTask.Result)); | |||||
| else | |||||
| { | |||||
| return new ValueTask<IPresence>(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>((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); | |||||
| } | |||||
| } | |||||
| @@ -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<IPresence> GetPresenceAsync(ulong userId, StateBehavior stateBehavior); | |||||
| ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior); | |||||
| ValueTask RemovePresenseAsync(ulong userId); | |||||
| ValueTask<IUser> GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask<IEnumerable<IUser>> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask AddOrUpdateUserAsync(IUser user); | |||||
| ValueTask RemoveUserAsync(ulong id); | |||||
| ValueTask<IGuildUser> GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask<IEnumerable<IGuildUser>> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user); | |||||
| ValueTask RemoveMemberAsync(ulong guildId, ulong id); | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Use the default Cache Behavior of the client. | |||||
| /// </summary> | |||||
| /// <seealso cref="DiscordSocketConfig.DefaultStateBehavior"/> | |||||
| Default = 0, | |||||
| /// <summary> | |||||
| /// The entity will only be retrieved via a synchronous cache lookup. | |||||
| /// | |||||
| /// For the default <see cref="IStateProvider"/>, this is equivalent to using <see cref="CacheOnly"/> | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This flag is used to indicate that the retrieval of this entity should not leave the | |||||
| /// synchronous path of the <see cref="System.Threading.Tasks.ValueTask"/>. 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 <see cref="IStateProvider"/> 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. | |||||
| /// </remarks> | |||||
| SyncOnly = 1, | |||||
| /// <summary> | |||||
| /// The entity will only be retrieved via a cache lookup - the Discord API will not be contacted to retrieve the entity. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// When using an alternative <see cref="IStateProvider"/>, 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 <see cref="IStateProvider"/>, 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. | |||||
| /// </remarks> | |||||
| CacheOnly = 2, | |||||
| /// <summary> | |||||
| /// The entity will be downloaded from the Discord REST API if the <see cref="ICacheProvider"/> on hand cannot locate it. | |||||
| /// </summary> | |||||
| AllowDownload = 3, | |||||
| /// <summary> | |||||
| /// The entity will be downloaded from the Discord REST API. The local <see cref="ICacheProvider"/> will not be contacted to find the entity. | |||||
| /// </summary> | |||||
| DownloadOnly = 4 | |||||
| } | |||||
| } | |||||