@@ -0,0 +1,111 @@ | |||
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 : IActivityModel, new() | |||
{ | |||
return new TModel() | |||
{ | |||
ApplicationId = richGame.ApplicationId, | |||
SmallImage = richGame.SmallAsset?.ImageId, | |||
SmallText = richGame.SmallAsset?.Text, | |||
LargeImage = richGame.LargeAsset?.ImageId, | |||
LargeText = richGame.LargeAsset?.Text, | |||
Details = richGame.Details, | |||
Flags = richGame.Flags, | |||
Name = richGame.Name, | |||
Type = richGame.Type, | |||
JoinSecret = richGame.Secrets?.Join, | |||
SpectateSecret = richGame.Secrets?.Spectate, | |||
MatchSecret = richGame.Secrets?.Match, | |||
State = richGame.State, | |||
PartyId = richGame.Party?.Id, | |||
PartySize = richGame.Party?.Members != null && richGame.Party?.Capacity != null | |||
? new long[] { richGame.Party.Members, richGame.Party.Capacity } | |||
: null, | |||
TimestampEnd = richGame.Timestamps?.End, | |||
TimestampStart = richGame.Timestamps?.Start | |||
}; | |||
} | |||
public static IActivityModel ToModel<TModel>(this SpotifyGame spotify) where TModel : IActivityModel, new() | |||
{ | |||
return new TModel() | |||
{ | |||
Name = spotify.Name, | |||
SessionId = spotify.SessionId, | |||
SyncId = spotify.TrackId, | |||
LargeText = spotify.AlbumTitle, | |||
Details = spotify.TrackTitle, | |||
State = string.Join(";", spotify.Artists), | |||
TimestampEnd = spotify.EndsAt, | |||
TimestampStart = spotify.StartedAt, | |||
LargeImage = spotify.AlbumArt, | |||
Type = ActivityType.Listening, | |||
Flags = spotify.Flags, | |||
}; | |||
} | |||
public static IActivityModel ToModel<TModel, TEmoteModel>(this CustomStatusGame custom) | |||
where TModel : IActivityModel, new() | |||
where TEmoteModel : IEmojiModel, new() | |||
{ | |||
return new TModel | |||
{ | |||
Id = "custom", | |||
Type = ActivityType.CustomStatus, | |||
Name = custom.Name, | |||
State = custom.State, | |||
Emoji = custom.Emote.ToModel<TEmoteModel>(), | |||
CreatedAt = custom.CreatedAt | |||
}; | |||
} | |||
public static IActivityModel ToModel<TModel>(this StreamingGame stream) where TModel : IActivityModel, new() | |||
{ | |||
return new TModel | |||
{ | |||
Name = stream.Name, | |||
Url = stream.Url, | |||
Flags = stream.Flags, | |||
Details = stream.Details | |||
}; | |||
} | |||
public static IEmojiModel ToModel<TModel>(this IEmote emote) where TModel : IEmojiModel, new() | |||
{ | |||
if (emote == null) | |||
return null; | |||
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,20 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
internal interface ICached<TType> : ICached, IDisposable | |||
{ | |||
void Update(TType model); | |||
TType ToModel(); | |||
} | |||
public interface ICached | |||
{ | |||
bool IsFreed { get; } | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IEmojiModel | |||
{ | |||
ulong? Id { get; set; } | |||
string Name { get; set; } | |||
ulong[] Roles { get; set; } | |||
bool RequireColons { get; set; } | |||
bool IsManaged { get; set; } | |||
bool IsAnimated { get; set; } | |||
bool IsAvailable { get; set; } | |||
ulong? CreatorId { get; set; } | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IEntityModel<TId> where TId : IEquatable<TId> | |||
{ | |||
TId Id { get; set; } | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IMessageModel : IEntityModel<ulong> | |||
{ | |||
MessageType Type { get; set; } | |||
ulong ChannelId { get; set; } | |||
ulong? GuildId { get; set; } | |||
ulong AuthorId { get; set; } | |||
bool IsWebhookMessage { get; set; } | |||
string Content { get; set; } | |||
DateTimeOffset Timestamp { get; set; } | |||
DateTimeOffset? EditedTimestamp { get; set; } | |||
bool IsTextToSpeech { get; set; } | |||
bool MentionEveryone { get; set; } | |||
ulong[] UserMentionIds { get; set; } | |||
ulong[] RoleMentionIds { get; set; } | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IActivityModel | |||
{ | |||
string Id { get; set; } | |||
string Url { get; set; } | |||
string Name { get; set; } | |||
ActivityType Type { get; set; } | |||
string Details { get; set; } | |||
string State { get; set; } | |||
ActivityProperties Flags { get; set; } | |||
DateTimeOffset CreatedAt { get; set; } | |||
IEmojiModel Emoji { get; set; } | |||
ulong? ApplicationId { get; set; } | |||
string SyncId { get; set; } | |||
string SessionId { get; set; } | |||
#region Assets | |||
string LargeImage { get; set; } | |||
string LargeText { get; set; } | |||
string SmallImage { get; set; } | |||
string SmallText { get; set; } | |||
#endregion | |||
#region Party | |||
string PartyId { get; set; } | |||
long[] PartySize { get; set; } | |||
#endregion | |||
#region Secrets | |||
string JoinSecret { get; set; } | |||
string SpectateSecret { get; set; } | |||
string MatchSecret { get; set; } | |||
#endregion | |||
#region Timestamps | |||
DateTimeOffset? TimestampStart { get; set; } | |||
DateTimeOffset? TimestampEnd { get; set; } | |||
#endregion | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IPresenceModel : IEntityModel<ulong> | |||
{ | |||
ulong UserId { get; set; } | |||
ulong? GuildId { get; set; } | |||
UserStatus Status { get; set; } | |||
ClientType[] ActiveClients { get; set; } | |||
IActivityModel[] Activities { get; set; } | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface ICurrentUserModel : IUserModel | |||
{ | |||
bool? IsVerified { get; set; } | |||
string Email { get; set; } | |||
bool? IsMfaEnabled { get; set; } | |||
UserProperties Flags { get; set; } | |||
PremiumType PremiumType { get; set; } | |||
string Locale { get; set; } | |||
UserProperties PublicFlags { get; set; } | |||
} | |||
} |
@@ -0,0 +1,22 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IMemberModel : IEntityModel<ulong> | |||
{ | |||
//IUserModel User { get; set; } | |||
string Nickname { get; set; } | |||
string GuildAvatar { get; set; } | |||
ulong[] Roles { get; set; } | |||
DateTimeOffset? JoinedAt { get; set; } | |||
DateTimeOffset? PremiumSince { get; set; } | |||
bool IsDeaf { get; set; } | |||
bool IsMute { get; set; } | |||
bool? IsPending { get; set; } | |||
DateTimeOffset? CommunicationsDisabledUntil { get; set; } | |||
} | |||
} |
@@ -0,0 +1,14 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IThreadMemberModel : IEntityModel<ulong> | |||
{ | |||
ulong? ThreadId { get; set; } | |||
DateTimeOffset JoinedAt { get; set; } | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface IUserModel : IEntityModel<ulong> | |||
{ | |||
string Username { get; set; } | |||
string Discriminator { get; set; } | |||
bool? IsBot { get; set; } | |||
string Avatar { get; set; } | |||
} | |||
} |
@@ -107,6 +107,8 @@ namespace Discord | |||
/// </returns> | |||
public string TrackUrl { get; internal set; } | |||
internal string AlbumArt { get; set; } | |||
internal SpotifyGame() { } | |||
/// <summary> | |||
@@ -24,6 +24,13 @@ namespace Discord | |||
/// </returns> | |||
public bool RequireColons { get; } | |||
/// <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. | |||
/// </summary> | |||
/// <returns> | |||
@@ -39,12 +46,13 @@ namespace Discord | |||
/// </returns> | |||
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; | |||
RequireColons = requireColons; | |||
RoleIds = roleIds; | |||
CreatorId = userId; | |||
IsAvailable = isAvailable; | |||
} | |||
private string DebuggerDisplay => $"{Name} ({Id})"; | |||
@@ -0,0 +1,70 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.API | |||
{ | |||
internal class CurrentUser : User, ICurrentUserModel | |||
{ | |||
[JsonProperty("verified")] | |||
public Optional<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 | |||
{ | |||
get => Verified.ToNullable(); | |||
set => throw new NotSupportedException(); | |||
} | |||
string ICurrentUserModel.Email | |||
{ | |||
get => Email.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
bool? ICurrentUserModel.IsMfaEnabled | |||
{ | |||
get => MfaEnabled.ToNullable(); | |||
set => throw new NotSupportedException(); | |||
} | |||
UserProperties ICurrentUserModel.Flags | |||
{ | |||
get => Flags.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
PremiumType ICurrentUserModel.PremiumType | |||
{ | |||
get => PremiumType.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
string ICurrentUserModel.Locale | |||
{ | |||
get => Locale.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
UserProperties ICurrentUserModel.PublicFlags | |||
{ | |||
get => PublicFlags.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
} | |||
} |
@@ -1,8 +1,9 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
namespace Discord.API | |||
{ | |||
internal class Emoji | |||
internal class Emoji : IEmojiModel | |||
{ | |||
[JsonProperty("id")] | |||
public ulong? Id { get; set; } | |||
@@ -16,7 +17,57 @@ namespace Discord.API | |||
public bool RequireColons { get; set; } | |||
[JsonProperty("managed")] | |||
public bool Managed { get; set; } | |||
[JsonProperty("available")] | |||
public Optional<bool> Available { get; set; } | |||
[JsonProperty("user")] | |||
public Optional<User> User { get; set; } | |||
ulong? IEmojiModel.Id | |||
{ | |||
get => Id; | |||
set => throw new NotSupportedException(); | |||
} | |||
string IEmojiModel.Name | |||
{ | |||
get => Name; | |||
set => throw new NotSupportedException(); | |||
} | |||
ulong[] IEmojiModel.Roles | |||
{ | |||
get => Roles; | |||
set => throw new NotSupportedException(); | |||
} | |||
bool IEmojiModel.RequireColons | |||
{ | |||
get => RequireColons; | |||
set => throw new NotSupportedException(); | |||
} | |||
bool IEmojiModel.IsManaged | |||
{ | |||
get => Managed; | |||
set => throw new NotSupportedException(); | |||
} | |||
bool IEmojiModel.IsAnimated | |||
{ | |||
get => Animated.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
bool IEmojiModel.IsAvailable | |||
{ | |||
get => Available.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
ulong? IEmojiModel.CreatorId | |||
{ | |||
get => User.GetValueOrDefault()?.Id; | |||
set => throw new NotSupportedException(); | |||
} | |||
} | |||
} |
@@ -1,10 +1,11 @@ | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Serialization; | |||
using System; | |||
using System.Runtime.Serialization; | |||
namespace Discord.API | |||
{ | |||
internal class Game | |||
internal class Game : IActivityModel | |||
{ | |||
[JsonProperty("name")] | |||
public string Name { get; set; } | |||
@@ -32,7 +33,7 @@ namespace Discord.API | |||
public Optional<string> SyncId { get; set; } | |||
[JsonProperty("session_id")] | |||
public Optional<string> SessionId { get; set; } | |||
[JsonProperty("Flags")] | |||
[JsonProperty("flags")] | |||
public Optional<ActivityProperties> Flags { get; set; } | |||
[JsonProperty("id")] | |||
public Optional<string> Id { get; set; } | |||
@@ -40,6 +41,100 @@ namespace Discord.API | |||
public Optional<Emoji> Emoji { get; set; } | |||
[JsonProperty("created_at")] | |||
public Optional<long> CreatedAt { get; set; } | |||
string IActivityModel.Id { | |||
get => Id.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.Url { | |||
get => StreamUrl.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.State { | |||
get => State.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
IEmojiModel IActivityModel.Emoji { | |||
get => Emoji.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.Name { | |||
get => Name; set => throw new NotSupportedException(); | |||
} | |||
ActivityType IActivityModel.Type { | |||
get => Type.GetValueOrDefault().GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
ActivityProperties IActivityModel.Flags { | |||
get => Flags.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.Details { | |||
get => Details.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
DateTimeOffset IActivityModel.CreatedAt { | |||
get => DateTimeOffset.FromUnixTimeMilliseconds(CreatedAt.GetValueOrDefault()); set => throw new NotSupportedException(); | |||
} | |||
ulong? IActivityModel.ApplicationId { | |||
get => ApplicationId.ToNullable(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.SyncId { | |||
get => SyncId.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.SessionId { | |||
get => SessionId.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.LargeImage { | |||
get => Assets.GetValueOrDefault()?.LargeImage.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.LargeText { | |||
get => Assets.GetValueOrDefault()?.LargeText.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.SmallImage { | |||
get => Assets.GetValueOrDefault()?.SmallImage.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.SmallText { | |||
get => Assets.GetValueOrDefault()?.SmallText.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.PartyId { | |||
get => Party.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); | |||
} | |||
long[] IActivityModel.PartySize { | |||
get => Party.GetValueOrDefault()?.Size; set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.JoinSecret { | |||
get => Secrets.GetValueOrDefault()?.Join; set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.SpectateSecret { | |||
get => Secrets.GetValueOrDefault()?.Spectate; set => throw new NotSupportedException(); | |||
} | |||
string IActivityModel.MatchSecret { | |||
get => Secrets.GetValueOrDefault()?.Match; set => throw new NotSupportedException(); | |||
} | |||
DateTimeOffset? IActivityModel.TimestampStart { | |||
get => Timestamps.GetValueOrDefault()?.Start.ToNullable(); set => throw new NotSupportedException(); | |||
} | |||
DateTimeOffset? IActivityModel.TimestampEnd { | |||
get => Timestamps.GetValueOrDefault()?.End.ToNullable(); set => throw new NotSupportedException(); | |||
} | |||
//[JsonProperty("buttons")] | |||
//public Optional<RichPresenceButton[]> Buttons { get; set; } | |||
@@ -3,7 +3,7 @@ using System; | |||
namespace Discord.API | |||
{ | |||
internal class GuildMember | |||
internal class GuildMember : IMemberModel | |||
{ | |||
[JsonProperty("user")] | |||
public User User { get; set; } | |||
@@ -25,5 +25,46 @@ namespace Discord.API | |||
public Optional<DateTimeOffset?> PremiumSince { get; set; } | |||
[JsonProperty("communication_disabled_until")] | |||
public Optional<DateTimeOffset?> TimedOutUntil { get; set; } | |||
// IMemberModel | |||
string IMemberModel.Nickname { | |||
get => Nick.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
string IMemberModel.GuildAvatar { | |||
get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
ulong[] IMemberModel.Roles { | |||
get => Roles.GetValueOrDefault(Array.Empty<ulong>()); set => throw new NotSupportedException(); | |||
} | |||
DateTimeOffset? IMemberModel.JoinedAt { | |||
get => JoinedAt.ToNullable(); set => throw new NotSupportedException(); | |||
} | |||
DateTimeOffset? IMemberModel.PremiumSince { | |||
get => PremiumSince.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
bool IMemberModel.IsDeaf { | |||
get => Deaf.GetValueOrDefault(false); set => throw new NotSupportedException(); | |||
} | |||
bool IMemberModel.IsMute { | |||
get => Mute.GetValueOrDefault(false); set => throw new NotSupportedException(); | |||
} | |||
bool? IMemberModel.IsPending { | |||
get => Pending.ToNullable(); set => throw new NotSupportedException(); | |||
} | |||
DateTimeOffset? IMemberModel.CommunicationsDisabledUntil { | |||
get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
ulong IEntityModel<ulong>.Id { | |||
get => User.Id; set => throw new NotSupportedException(); | |||
} | |||
} | |||
} |
@@ -1,10 +1,11 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Discord.API | |||
{ | |||
internal class Presence | |||
internal class Presence : IPresenceModel | |||
{ | |||
[JsonProperty("user")] | |||
public User User { get; set; } | |||
@@ -28,5 +29,28 @@ namespace Discord.API | |||
public List<Game> Activities { get; set; } | |||
[JsonProperty("premium_since")] | |||
public Optional<DateTimeOffset?> PremiumSince { get; set; } | |||
ulong IPresenceModel.UserId { | |||
get => User.Id; set => throw new NotSupportedException(); | |||
} | |||
ulong? IPresenceModel.GuildId { | |||
get => GuildId.ToNullable(); set => throw new NotSupportedException(); | |||
} | |||
UserStatus IPresenceModel.Status { | |||
get => Status; set => throw new NotSupportedException(); | |||
} | |||
ClientType[] IPresenceModel.ActiveClients { | |||
get => ClientStatus.IsSpecified ? ClientStatus.Value.Select(x => (ClientType)Enum.Parse(typeof(ClientType), x.Key, true)).ToArray() : Array.Empty<ClientType>(); set => throw new NotSupportedException(); | |||
} | |||
IActivityModel[] IPresenceModel.Activities { | |||
get => Activities.ToArray(); set => throw new NotSupportedException(); | |||
} | |||
ulong IEntityModel<ulong>.Id { | |||
get => User.Id; set => throw new NotSupportedException(); | |||
} | |||
} | |||
} |
@@ -3,10 +3,10 @@ using System; | |||
namespace Discord.API | |||
{ | |||
internal class ThreadMember | |||
internal class ThreadMember : IThreadMemberModel | |||
{ | |||
[JsonProperty("id")] | |||
public Optional<ulong> Id { get; set; } | |||
public Optional<ulong> ThreadId { get; set; } | |||
[JsonProperty("user_id")] | |||
public Optional<ulong> UserId { get; set; } | |||
@@ -14,7 +14,8 @@ namespace Discord.API | |||
[JsonProperty("join_timestamp")] | |||
public DateTimeOffset JoinTimestamp { get; set; } | |||
[JsonProperty("flags")] | |||
public int Flags { get; set; } // No enum type (yet?) | |||
ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } | |||
DateTimeOffset IThreadMemberModel.JoinedAt { get => JoinTimestamp; set => throw new NotSupportedException(); } | |||
ulong IEntityModel<ulong>.Id { get => UserId.GetValueOrDefault(0); set => throw new NotSupportedException(); } | |||
} | |||
} |
@@ -1,8 +1,9 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
namespace Discord.API | |||
{ | |||
internal class User | |||
internal class User : IUserModel | |||
{ | |||
[JsonProperty("id")] | |||
public ulong Id { get; set; } | |||
@@ -19,20 +20,33 @@ namespace Discord.API | |||
[JsonProperty("accent_color")] | |||
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 | |||
{ | |||
get => Username.GetValueOrDefault(); | |||
set => throw new NotSupportedException(); | |||
} | |||
string IUserModel.Discriminator { | |||
get => Discriminator.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
bool? IUserModel.IsBot | |||
{ | |||
get => Bot.ToNullable(); | |||
set => throw new NotSupportedException(); | |||
} | |||
string IUserModel.Avatar | |||
{ | |||
get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); | |||
} | |||
ulong IEntityModel<ulong>.Id | |||
{ | |||
get => Id; | |||
set => throw new NotSupportedException(); | |||
} | |||
} | |||
} |
@@ -151,6 +151,16 @@ namespace Discord.Rest | |||
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) | |||
{ | |||
var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); | |||
@@ -2071,10 +2071,10 @@ namespace Discord.API | |||
#endregion | |||
#region Current User/DMs | |||
public async Task<User> GetMyUserAsync(RequestOptions options = null) | |||
public async Task<CurrentUser> GetMyUserAsync(RequestOptions options = null) | |||
{ | |||
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) | |||
{ | |||
@@ -193,6 +193,8 @@ namespace Discord.Rest | |||
=> ClientHelper.GetUserAsync(this, id, options); | |||
public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | |||
=> 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) | |||
=> ClientHelper.GetVoiceRegionsAsync(this, options); | |||
@@ -96,6 +96,7 @@ namespace Discord.Rest | |||
internal void Update(Model model) | |||
{ | |||
base.Update(model.User); | |||
if (model.JoinedAt.IsSpecified) | |||
_joinedAtTicks = model.JoinedAt.Value.UtcTicks; | |||
if (model.Nick.IsSpecified) | |||
@@ -1,7 +1,8 @@ | |||
using System; | |||
using System.Diagnostics; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.User; | |||
using UserModel = Discord.API.User; | |||
using Model = Discord.API.CurrentUser; | |||
namespace Discord.Rest | |||
{ | |||
@@ -28,29 +29,26 @@ namespace Discord.Rest | |||
: base(discord, id) | |||
{ | |||
} | |||
internal new static RestSelfUser Create(BaseDiscordClient discord, Model model) | |||
internal new static RestSelfUser Create(BaseDiscordClient discord, UserModel model) | |||
{ | |||
var entity = new RestSelfUser(discord, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
/// <inheritdoc /> | |||
internal override void Update(Model model) | |||
internal override void Update(UserModel model) | |||
{ | |||
base.Update(model); | |||
if (model.Email.IsSpecified) | |||
Email = model.Email.Value; | |||
if (model.Verified.IsSpecified) | |||
IsVerified = model.Verified.Value; | |||
if (model.MfaEnabled.IsSpecified) | |||
IsMfaEnabled = model.MfaEnabled.Value; | |||
if (model.Flags.IsSpecified) | |||
Flags = (UserProperties)model.Flags.Value; | |||
if (model.PremiumType.IsSpecified) | |||
PremiumType = model.PremiumType.Value; | |||
if (model.Locale.IsSpecified) | |||
Locale = model.Locale.Value; | |||
if (model is not Model currentUserModel) | |||
throw new ArgumentException("Got unexpected model type when updating RestSelfUser"); | |||
Email = currentUserModel.Email.GetValueOrDefault(); | |||
IsVerified = currentUserModel.Verified.GetValueOrDefault(false); | |||
IsMfaEnabled = currentUserModel.MfaEnabled.GetValueOrDefault(false); | |||
Flags = currentUserModel.Flags.GetValueOrDefault(); | |||
PremiumType = currentUserModel.PremiumType.GetValueOrDefault(); | |||
Locale = currentUserModel.Locale.GetValueOrDefault(); | |||
} | |||
/// <inheritdoc /> | |||
@@ -78,20 +78,16 @@ namespace Discord.Rest | |||
internal virtual void Update(Model model) | |||
{ | |||
if (model.Avatar.IsSpecified) | |||
AvatarId = model.Avatar.Value; | |||
if (model.Banner.IsSpecified) | |||
BannerId = model.Banner.Value; | |||
if (model.AccentColor.IsSpecified) | |||
AccentColor = model.AccentColor.Value; | |||
if (model.Discriminator.IsSpecified) | |||
AvatarId = model.Avatar.GetValueOrDefault(); | |||
if(model.Discriminator.IsSpecified) | |||
DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | |||
if (model.Bot.IsSpecified) | |||
IsBot = model.Bot.Value; | |||
if (model.Username.IsSpecified) | |||
Username = model.Username.Value; | |||
if (model.PublicFlags.IsSpecified) | |||
PublicFlags = model.PublicFlags.Value; | |||
IsBot = model.Bot.GetValueOrDefault(false); | |||
Username = model.Username.GetValueOrDefault(); | |||
if(model is ICurrentUserModel currentUserModel) | |||
{ | |||
PublicFlags = currentUserModel.PublicFlags; | |||
} | |||
} | |||
/// <inheritdoc /> | |||
@@ -6,6 +6,23 @@ namespace Discord.Rest | |||
{ | |||
internal static class EntityExtensions | |||
{ | |||
public static IEmote ToIEmote(this IEmojiModel model) | |||
{ | |||
if (model.Id.HasValue) | |||
return model.ToEntity(); | |||
return new Emoji(model.Name); | |||
} | |||
public static GuildEmote ToEntity(this IEmojiModel model) | |||
=> new GuildEmote(model.Id.Value, | |||
model.Name, | |||
model.IsAnimated, | |||
model.IsManaged, | |||
model.IsAvailable, | |||
model.RequireColons, | |||
ImmutableArray.Create(model.Roles), | |||
model.CreatorId); | |||
public static IEmote ToIEmote(this API.Emoji model) | |||
{ | |||
if (model.Id.HasValue) | |||
@@ -18,6 +35,7 @@ namespace Discord.Rest | |||
model.Name, | |||
model.Animated.GetValueOrDefault(), | |||
model.Managed, | |||
model.Available.GetValueOrDefault(), | |||
model.RequireColons, | |||
ImmutableArray.Create(model.Roles), | |||
model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); | |||
@@ -171,48 +189,5 @@ namespace Discord.Rest | |||
{ | |||
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")] | |||
public int Version { get; set; } | |||
[JsonProperty("user")] | |||
public User User { get; set; } | |||
public CurrentUser User { get; set; } | |||
[JsonProperty("session_id")] | |||
public string SessionId { get; set; } | |||
[JsonProperty("read_state")] | |||
@@ -0,0 +1,105 @@ | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.WebSocket | |||
{ | |||
public class DefaultConcurrentCacheProvider : ICacheProvider | |||
{ | |||
private readonly ConcurrentDictionary<Type, object> _storeCache = new(); | |||
private readonly ConcurrentDictionary<object, object> _subStoreCache = new(); | |||
private class DefaultEntityStore<TModel, TId> : IEntityStore<TModel, TId> | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId> | |||
{ | |||
private ConcurrentDictionary<TId, TModel> _cache; | |||
public DefaultEntityStore(ConcurrentDictionary<TId, TModel> cache) | |||
{ | |||
_cache = cache; | |||
} | |||
public TModel Get(TId id) | |||
{ | |||
if (_cache.TryGetValue(id, out var model)) | |||
return model; | |||
return default; | |||
} | |||
public IEnumerable<TModel> GetAll() | |||
{ | |||
return _cache.Select(x => x.Value); | |||
} | |||
public void AddOrUpdate(TModel model) | |||
{ | |||
_cache.AddOrUpdate(model.Id, model, (_, __) => model); | |||
} | |||
public void AddOrUpdateBatch(IEnumerable<TModel> models) | |||
{ | |||
foreach (var model in models) | |||
_cache.AddOrUpdate(model.Id, model, (_, __) => model); | |||
} | |||
public void Remove(TId id) | |||
{ | |||
_cache.TryRemove(id, out _); | |||
} | |||
public void PurgeAll() | |||
{ | |||
_cache.Clear(); | |||
} | |||
ValueTask<TModel> IEntityStore<TModel, TId>.GetAsync(TId id) => new ValueTask<TModel>(Get(id)); | |||
IAsyncEnumerable<TModel> IEntityStore<TModel, TId>.GetAllAsync() | |||
{ | |||
var enumerator = GetAll().GetEnumerator(); | |||
return AsyncEnumerable.Create((cancellationToken) | |||
=> AsyncEnumerator.Create( | |||
() => new ValueTask<bool>(enumerator.MoveNext()), | |||
() => enumerator.Current, | |||
() => new ValueTask()) | |||
); | |||
} | |||
ValueTask IEntityStore<TModel, TId>.AddOrUpdateAsync(TModel model) | |||
{ | |||
AddOrUpdate(model); | |||
return default; | |||
} | |||
ValueTask IEntityStore<TModel, TId>.AddOrUpdateBatchAsync(IEnumerable<TModel> models) | |||
{ | |||
AddOrUpdateBatch(models); | |||
return default; | |||
} | |||
ValueTask IEntityStore<TModel, TId>.RemoveAsync(TId id) | |||
{ | |||
Remove(id); | |||
return default; | |||
} | |||
ValueTask IEntityStore<TModel, TId>.PurgeAllAsync() | |||
{ | |||
PurgeAll(); | |||
return default; | |||
} | |||
} | |||
public Type GetModel<TInterface>() => null; | |||
public virtual ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>() | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId> | |||
{ | |||
var store = _storeCache.GetOrAdd(typeof(TModel), (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>())); | |||
return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store); | |||
} | |||
public virtual ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId) | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId> | |||
{ | |||
var store = _subStoreCache.GetOrAdd(parentId, (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>())); | |||
return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store); | |||
} | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.WebSocket | |||
{ | |||
public interface ICacheProvider | |||
{ | |||
Type GetModel<TModelInterface>(); | |||
ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>() | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId>; | |||
ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId) | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId>; | |||
} | |||
public interface IEntityStore<TModel, TId> | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId> | |||
{ | |||
ValueTask<TModel> GetAsync(TId id); | |||
TModel Get(TId id); | |||
IAsyncEnumerable<TModel> GetAllAsync(); | |||
IEnumerable<TModel> GetAll(); | |||
ValueTask AddOrUpdateAsync(TModel model); | |||
void AddOrUpdate(TModel model); | |||
ValueTask AddOrUpdateBatchAsync(IEnumerable<TModel> models); | |||
void AddOrUpdateBatch(IEnumerable<TModel> models); | |||
ValueTask RemoveAsync(TId id); | |||
void Remove(TId id); | |||
ValueTask PurgeAllAsync(); | |||
void PurgeAll(); | |||
} | |||
} |
@@ -0,0 +1,76 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.WebSocket | |||
{ | |||
/// <summary> | |||
/// Represents a lazily-loaded cached value that can be loaded synchronously or asynchronously. | |||
/// </summary> | |||
/// <typeparam name="TEntity">The type of the entity.</typeparam> | |||
/// <typeparam name="TId">The primary id type of the entity.</typeparam> | |||
public class LazyCached<TEntity, TId> | |||
where TEntity : class, ICached | |||
where TId : IEquatable<TId> | |||
{ | |||
/// <summary> | |||
/// Gets or loads the cached value synchronously. | |||
/// </summary> | |||
public TEntity Value | |||
=> GetOrLoad(); | |||
/// <summary> | |||
/// Gets whether or not the <see cref="Value"/> has been loaded and is still alive. | |||
/// </summary> | |||
public bool IsValueCreated | |||
=> _loadedValue != null && _loadedValue.IsFreed; | |||
private TEntity _loadedValue; | |||
private readonly ILookupReferenceStore<TEntity, TId> _store; | |||
private readonly TId _id; | |||
private readonly object _lock = new(); | |||
internal LazyCached(TEntity value) | |||
{ | |||
_loadedValue = value; | |||
} | |||
internal LazyCached(TId id, ILookupReferenceStore<TEntity, TId> store) | |||
{ | |||
_store = store; | |||
_id = id; | |||
} | |||
private TEntity GetOrLoad() | |||
{ | |||
lock (_lock) | |||
{ | |||
if(!IsValueCreated) | |||
_loadedValue = _store.Get(_id); | |||
return _loadedValue; | |||
} | |||
} | |||
/// <summary> | |||
/// Gets or loads the value from the cache asynchronously. | |||
/// </summary> | |||
/// <returns>The loaded or fetched entity.</returns> | |||
public async ValueTask<TEntity> GetAsync() | |||
{ | |||
if (!IsValueCreated) | |||
_loadedValue = await _store.GetAsync(_id).ConfigureAwait(false); | |||
return _loadedValue; | |||
} | |||
} | |||
public class LazyCached<TEntity> : LazyCached<TEntity, ulong> | |||
where TEntity : class, ICached | |||
{ | |||
internal LazyCached(ulong id, ILookupReferenceStore<TEntity, ulong> store) | |||
: base(id, store) { } | |||
internal LazyCached(TEntity entity) | |||
: base(entity) { } | |||
} | |||
} |
@@ -0,0 +1,408 @@ | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Runtime.CompilerServices; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class CacheReference<TType> where TType : class | |||
{ | |||
public WeakReference<TType> Reference { get; } | |||
public bool CanRelease | |||
=> !Reference.TryGetTarget(out _) || _referenceCount <= 0; | |||
private int _referenceCount; | |||
public CacheReference(TType value) | |||
{ | |||
Reference = new(value); | |||
_referenceCount = 1; | |||
} | |||
public bool TryObtainReference(out TType reference) | |||
{ | |||
if (Reference.TryGetTarget(out reference)) | |||
{ | |||
Interlocked.Increment(ref _referenceCount); | |||
return true; | |||
} | |||
return false; | |||
} | |||
public void ReleaseReference() | |||
{ | |||
Interlocked.Decrement(ref _referenceCount); | |||
} | |||
} | |||
internal interface ILookupReferenceStore<TEntity, TId> | |||
{ | |||
TEntity Get(TId id); | |||
ValueTask<TEntity> GetAsync(TId id); | |||
} | |||
internal class ReferenceStore<TEntity, TModel, TId, TSharedEntity> : ILookupReferenceStore<TEntity, TId> | |||
where TEntity : class, ICached<TModel>, TSharedEntity | |||
where TModel : IEntityModel<TId> | |||
where TId : IEquatable<TId> | |||
where TSharedEntity : class | |||
{ | |||
private readonly ICacheProvider _cacheProvider; | |||
private readonly ConcurrentDictionary<TId, CacheReference<TEntity>> _references = new(); | |||
private IEntityStore<TModel, TId> _store; | |||
private Func<TModel, TEntity> _entityBuilder; | |||
private Func<TModel> _modelFactory; | |||
private Func<TId, RequestOptions, Task<TSharedEntity>> _restLookup; | |||
private readonly object _lock = new(); | |||
public ReferenceStore(ICacheProvider cacheProvider, | |||
Func<TModel, TEntity> entityBuilder, | |||
Func<TId, RequestOptions, Task<TSharedEntity>> restLookup, | |||
Func<TModel> userDefinedModelFactory) | |||
{ | |||
_cacheProvider = cacheProvider; | |||
_entityBuilder = entityBuilder; | |||
_restLookup = restLookup; | |||
_modelFactory = userDefinedModelFactory; | |||
} | |||
private TModel GetUserDefinedModel(TModel t) | |||
=> t.ToSpecifiedModel(_modelFactory()); | |||
internal bool RemoveReference(TId id) | |||
{ | |||
if(_references.TryGetValue(id, out var rf)) | |||
{ | |||
rf.ReleaseReference(); | |||
if (rf.CanRelease) | |||
return _references.TryRemove(id, out _); | |||
} | |||
return false; | |||
} | |||
internal void ClearDeadReferences() | |||
{ | |||
lock (_lock) | |||
{ | |||
var references = _references.Where(x => x.Value.CanRelease).ToArray(); | |||
foreach (var reference in references) | |||
_references.TryRemove(reference.Key, out _); | |||
} | |||
} | |||
public async ValueTask InitializeAsync() | |||
{ | |||
_store ??= await _cacheProvider.GetStoreAsync<TModel, TId>().ConfigureAwait(false); | |||
} | |||
public async ValueTask InitializeAsync(TId parentId) | |||
{ | |||
_store ??= await _cacheProvider.GetSubStoreAsync<TModel, TId>(parentId).ConfigureAwait(false); | |||
} | |||
private bool TryGetReference(TId id, out TEntity entity) | |||
{ | |||
entity = null; | |||
return _references.TryGetValue(id, out var reference) && reference.TryObtainReference(out entity); | |||
} | |||
public TEntity Get(TId id) | |||
{ | |||
if(TryGetReference(id, out var entity)) | |||
{ | |||
return entity; | |||
} | |||
var model = _store.Get(id); | |||
if (model != null) | |||
{ | |||
entity = _entityBuilder(model); | |||
_references.TryAdd(id, new CacheReference<TEntity>(entity)); | |||
return entity; | |||
} | |||
return null; | |||
} | |||
public async ValueTask<TSharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null) | |||
{ | |||
if (TryGetReference(id, out var entity)) | |||
{ | |||
return entity; | |||
} | |||
var model = await _store.GetAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
{ | |||
entity = _entityBuilder(model); | |||
_references.TryAdd(id, new CacheReference<TEntity>(entity)); | |||
return entity; | |||
} | |||
if(mode == CacheMode.AllowDownload) | |||
{ | |||
return await _restLookup(id, options).ConfigureAwait(false); | |||
} | |||
return null; | |||
} | |||
public IEnumerable<TEntity> GetAll() | |||
{ | |||
var models = _store.GetAll(); | |||
return models.Select(x => | |||
{ | |||
var entity = _entityBuilder(x); | |||
_references.TryAdd(x.Id, new CacheReference<TEntity>(entity)); | |||
return entity; | |||
}); | |||
} | |||
public async IAsyncEnumerable<TEntity> GetAllAsync() | |||
{ | |||
await foreach(var model in _store.GetAllAsync()) | |||
{ | |||
var entity = _entityBuilder(model); | |||
_references.TryAdd(model.Id, new CacheReference<TEntity>(entity)); | |||
yield return entity; | |||
} | |||
} | |||
public TEntity GetOrAdd(TId id, Func<TId, TModel> valueFactory) | |||
{ | |||
var entity = Get(id); | |||
if (entity != null) | |||
return entity; | |||
var model = valueFactory(id); | |||
AddOrUpdate(model); | |||
return _entityBuilder(model); | |||
} | |||
public async ValueTask<TEntity> GetOrAddAsync(TId id, Func<TId, TModel> valueFactory) | |||
{ | |||
var entity = await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); | |||
if (entity != null) | |||
return (TEntity)entity; | |||
var model = valueFactory(id); | |||
await AddOrUpdateAsync(model).ConfigureAwait(false); | |||
return _entityBuilder(model); | |||
} | |||
public void AddOrUpdate(TModel model) | |||
{ | |||
var userDefinedModel = GetUserDefinedModel(model); | |||
_store.AddOrUpdate(userDefinedModel); | |||
if (TryGetReference(model.Id, out var reference)) | |||
reference.Update(userDefinedModel); | |||
} | |||
public ValueTask AddOrUpdateAsync(TModel model) | |||
{ | |||
var userDefinedModel = GetUserDefinedModel(model); | |||
if (TryGetReference(userDefinedModel.Id, out var reference)) | |||
reference.Update(userDefinedModel); | |||
return _store.AddOrUpdateAsync(userDefinedModel); | |||
} | |||
public void BulkAddOrUpdate(IEnumerable<TModel> models) | |||
{ | |||
models = models.Select(x => GetUserDefinedModel(x)); | |||
_store.AddOrUpdateBatch(models); | |||
foreach (var model in models) | |||
{ | |||
if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) | |||
entity.Update(model); | |||
} | |||
} | |||
public async ValueTask BulkAddOrUpdateAsync(IEnumerable<TModel> models) | |||
{ | |||
models = models.Select(x => GetUserDefinedModel(x)); | |||
await _store.AddOrUpdateBatchAsync(models).ConfigureAwait(false); | |||
foreach (var model in models) | |||
{ | |||
if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) | |||
entity.Update(model); | |||
} | |||
} | |||
public void Remove(TId id) | |||
{ | |||
_store.Remove(id); | |||
_references.TryRemove(id, out _); | |||
} | |||
public ValueTask RemoveAsync(TId id) | |||
{ | |||
_references.TryRemove(id, out _); | |||
return _store.RemoveAsync(id); | |||
} | |||
public void Purge() | |||
{ | |||
_store.PurgeAll(); | |||
_references.Clear(); | |||
} | |||
public ValueTask PurgeAsync() | |||
{ | |||
_references.Clear(); | |||
return _store.PurgeAllAsync(); | |||
} | |||
TEntity ILookupReferenceStore<TEntity, TId>.Get(TId id) => Get(id); | |||
async ValueTask<TEntity> ILookupReferenceStore<TEntity, TId>.GetAsync(TId id) => (TEntity)await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); | |||
} | |||
internal partial class ClientStateManager | |||
{ | |||
public ReferenceStore<SocketGlobalUser, IUserModel, ulong, IUser> UserStore; | |||
public ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence> PresenceStore; | |||
private ConcurrentDictionary<ulong, ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>> _memberStores; | |||
private ConcurrentDictionary<ulong, ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>> _threadMemberStores; | |||
private SemaphoreSlim _memberStoreLock; | |||
private SemaphoreSlim _threadMemberLock; | |||
private readonly Dictionary<Type, Func<object>> _defaultModelFactory = new() | |||
{ | |||
{ typeof(IUserModel), () => new SocketUser.CacheModel() }, | |||
{ typeof(IMemberModel), () => new SocketGuildUser.CacheModel() }, | |||
{ typeof(ICurrentUserModel), () => new SocketSelfUser.CacheModel() }, | |||
{ typeof(IThreadMemberModel), () => new SocketThreadUser.CacheModel() }, | |||
{ typeof(IPresenceModel), () => new SocketPresence.CacheModel() }, | |||
{ typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() } | |||
}; | |||
public void ClearDeadReferences() | |||
{ | |||
UserStore.ClearDeadReferences(); | |||
PresenceStore.ClearDeadReferences(); | |||
} | |||
public async ValueTask InitializeAsync() | |||
{ | |||
await UserStore.InitializeAsync(); | |||
await PresenceStore.InitializeAsync(); | |||
} | |||
public ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> GetMemberStore(ulong guildId) | |||
=> TryGetMemberStore(guildId, out var store) ? store : null; | |||
public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store) | |||
=> _memberStores.TryGetValue(guildId, out store); | |||
public async ValueTask<ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>> GetMemberStoreAsync(ulong guildId) | |||
{ | |||
if (_memberStores.TryGetValue(guildId, out var store)) | |||
return store; | |||
await _memberStoreLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
store = new ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>( | |||
_cacheProvider, | |||
m => SocketGuildUser.Create(guildId, _client, m), | |||
async (id, options) => await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false), | |||
GetModel<IMemberModel>); | |||
await store.InitializeAsync(guildId).ConfigureAwait(false); | |||
_memberStores.TryAdd(guildId, store); | |||
return store; | |||
} | |||
finally | |||
{ | |||
_memberStoreLock.Release(); | |||
} | |||
} | |||
public async Task<ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>> GetThreadMemberStoreAsync(ulong threadId, ulong guildId) | |||
{ | |||
if (_threadMemberStores.TryGetValue(threadId, out var store)) | |||
return store; | |||
await _threadMemberLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
store = new ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>( | |||
_cacheProvider, | |||
m => SocketThreadUser.Create(_client, guildId, threadId, m), | |||
async (id, options) => await ThreadHelper.GetUserAsync(id, _client.GetChannel(threadId) as SocketThreadChannel, _client, options).ConfigureAwait(false), | |||
GetModel<IThreadMemberModel>); | |||
await store.InitializeAsync().ConfigureAwait(false); | |||
_threadMemberStores.TryAdd(threadId, store); | |||
return store; | |||
} | |||
finally | |||
{ | |||
_threadMemberLock.Release(); | |||
} | |||
} | |||
public ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser> GetThreadMemberStore(ulong threadId) | |||
=> _threadMemberStores.TryGetValue(threadId, out var store) ? store : null; | |||
public TModel GetModel<TModel, TFallback>() | |||
where TFallback : class, TModel, new() | |||
where TModel : class | |||
{ | |||
return GetModel<TModel>() ?? new TFallback(); | |||
} | |||
public TModel GetModel<TModel>() | |||
where TModel : class | |||
{ | |||
var type = _cacheProvider.GetModel<TModel>(); | |||
if (type != null) | |||
{ | |||
if (!type.GetInterfaces().Contains(typeof(TModel))) | |||
throw new InvalidOperationException($"Cannot use {type.Name} as a model for {typeof(TModel).Name}"); | |||
return (TModel)Activator.CreateInstance(type); | |||
} | |||
else | |||
return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; | |||
} | |||
private void CreateStores() | |||
{ | |||
UserStore = new ReferenceStore<SocketGlobalUser, IUserModel, ulong, IUser>( | |||
_cacheProvider, | |||
m => SocketGlobalUser.Create(_client, m), | |||
async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), | |||
GetModel<IUserModel>); | |||
PresenceStore = new ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence>( | |||
_cacheProvider, | |||
m => SocketPresence.Create(_client, m), | |||
(id, options) => Task.FromResult<IPresence>(null), | |||
GetModel<IPresenceModel>); | |||
_memberStores = new(); | |||
_threadMemberStores = new(); | |||
_threadMemberLock = new(1, 1); | |||
_memberStoreLock = new(1,1); | |||
} | |||
} | |||
} |
@@ -5,7 +5,7 @@ using System.Linq; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class ClientState | |||
internal partial class ClientStateManager | |||
{ | |||
private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | |||
private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | |||
@@ -30,8 +30,17 @@ namespace Discord.WebSocket | |||
_groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | |||
.ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | |||
public ClientState(int guildCount, int dmChannelCount) | |||
internal bool AllowSyncWaits | |||
=> _client.AllowSynchronousWaiting; | |||
private readonly ICacheProvider _cacheProvider; | |||
private readonly DiscordSocketClient _client; | |||
public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) | |||
{ | |||
_client = client; | |||
_cacheProvider = client.CacheProvider; | |||
double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | |||
double estimatedUsersCount = guildCount * AverageUsersPerGuild; | |||
_channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | |||
@@ -40,6 +49,8 @@ namespace Discord.WebSocket | |||
_users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | |||
_groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); | |||
_commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); | |||
CreateStores(); | |||
} | |||
internal SocketChannel GetChannel(ulong id) | |||
@@ -121,22 +132,6 @@ namespace Discord.WebSocket | |||
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() | |||
{ | |||
foreach (var guild in _guilds.Values) |
@@ -200,7 +200,7 @@ namespace Discord.WebSocket | |||
return _shards[id]; | |||
return null; | |||
} | |||
private int GetShardIdFor(ulong guildId) | |||
public int GetShardIdFor(ulong guildId) | |||
=> (int)((guildId >> 22) % (uint)_totalShards); | |||
public int GetShardIdFor(IGuild guild) | |||
=> GetShardIdFor(guild?.Id ?? 0); | |||
@@ -25,6 +25,17 @@ namespace Discord.WebSocket | |||
/// </example> | |||
public class DiscordSocketConfig : DiscordRestConfig | |||
{ | |||
/// <summary> | |||
/// Gets or sets the cache provider to use. | |||
/// </summary> | |||
public ICacheProvider CacheProvider { get; set; } | |||
/// <summary> | |||
/// Gets or sets whether or not non-async cache lookups would wait for the task to complete | |||
/// synchronously or to throw. | |||
/// </summary> | |||
public bool AllowSynchronousWaiting { get; set; } = false; | |||
/// <summary> | |||
/// Returns the encoding gateway should use. | |||
/// </summary> | |||
@@ -37,7 +37,7 @@ namespace Discord.WebSocket | |||
: base(discord, id, guild) | |||
{ | |||
} | |||
internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal new static SocketCategoryChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); | |||
entity.Update(state, model); | |||
@@ -29,7 +29,7 @@ namespace Discord.WebSocket | |||
} | |||
/// <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 | |||
{ | |||
@@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||
_ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), | |||
}; | |||
} | |||
internal abstract void Update(ClientState state, Model model); | |||
internal abstract void Update(ClientStateManager state, Model model); | |||
#endregion | |||
#region User | |||
@@ -35,25 +35,25 @@ namespace Discord.WebSocket | |||
{ | |||
Recipient = recipient; | |||
} | |||
internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) | |||
internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
Recipient.Update(state, model.Recipients.Value[0]); | |||
Recipient.Update(model.Recipients.Value[0]); | |||
} | |||
internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, ulong channelId, API.User recipient) | |||
internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, ulong channelId, API.User recipient) | |||
{ | |||
var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); | |||
entity.Update(state, recipient); | |||
return entity; | |||
} | |||
internal void Update(ClientState state, API.User recipient) | |||
internal void Update(ClientStateManager state, API.User recipient) | |||
{ | |||
Recipient.Update(state, recipient); | |||
Recipient.Update(recipient); | |||
} | |||
/// <inheritdoc /> | |||
@@ -55,13 +55,13 @@ namespace Discord.WebSocket | |||
_voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(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); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
if (model.Name.IsSpecified) | |||
Name = model.Name.Value; | |||
@@ -73,11 +73,11 @@ namespace Discord.WebSocket | |||
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)); | |||
for (int i = 0; i < models.Length; i++) | |||
users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); | |||
users[models[i].Id] = SocketGroupUser.Create(this, models[i]); | |||
_users = users; | |||
} | |||
@@ -265,8 +265,7 @@ namespace Discord.WebSocket | |||
return user; | |||
else | |||
{ | |||
var privateUser = SocketGroupUser.Create(this, Discord.State, model); | |||
privateUser.GlobalUser.AddRef(); | |||
var privateUser = SocketGroupUser.Create(this, model); | |||
_users[privateUser.Id] = privateUser; | |||
return privateUser; | |||
} | |||
@@ -275,7 +274,6 @@ namespace Discord.WebSocket | |||
{ | |||
if (_users.TryRemove(id, out SocketGroupUser user)) | |||
{ | |||
user.GlobalUser.RemoveRef(Discord); | |||
return user; | |||
} | |||
return null; | |||
@@ -283,7 +281,7 @@ namespace Discord.WebSocket | |||
#endregion | |||
#region Voice States | |||
internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) | |||
internal SocketVoiceState AddOrUpdateVoiceState(ClientStateManager state, VoiceStateModel model) | |||
{ | |||
var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | |||
var voiceState = SocketVoiceState.Create(voiceChannel, model); | |||
@@ -49,7 +49,7 @@ namespace Discord.WebSocket | |||
{ | |||
Guild = guild; | |||
} | |||
internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal static SocketGuildChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
return model.Type switch | |||
{ | |||
@@ -63,7 +63,7 @@ namespace Discord.WebSocket | |||
}; | |||
} | |||
/// <inheritdoc /> | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
Name = model.Name.Value; | |||
Position = model.Position.GetValueOrDefault(0); | |||
@@ -21,7 +21,7 @@ namespace Discord.WebSocket | |||
:base(discord, id, guild) | |||
{ | |||
} | |||
internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal new static SocketNewsChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); | |||
entity.Update(state, model); | |||
@@ -47,7 +47,7 @@ namespace Discord.WebSocket | |||
internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | |||
: base(discord, id, guild) { } | |||
internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal new static SocketStageChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketStageChannel(guild.Discord, model.Id, guild); | |||
entity.Update(state, model); | |||
@@ -64,13 +64,13 @@ namespace Discord.WebSocket | |||
if (Discord.MessageCacheSize > 0) | |||
_messages = new MessageCache(Discord); | |||
} | |||
internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal new static SocketTextChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketTextChannel(guild.Discord, model.Id, guild); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
base.Update(state, model); | |||
CategoryId = model.CategoryId; | |||
@@ -123,7 +123,7 @@ namespace Discord.WebSocket | |||
{ | |||
var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); | |||
var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.State, model); | |||
var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.StateManager, model); | |||
if(Discord.AlwaysDownloadUsers && Discord.HasGatewayIntent(GatewayIntents.GuildMembers)) | |||
await thread.DownloadUsersAsync(); | |||
@@ -118,7 +118,7 @@ namespace Discord.WebSocket | |||
CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); | |||
} | |||
internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal new static SocketThreadChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var parent = guild.GetChannel(model.CategoryId.Value); | |||
var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); | |||
@@ -126,7 +126,7 @@ namespace Discord.WebSocket | |||
return entity; | |||
} | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
base.Update(state, model); | |||
@@ -171,7 +171,6 @@ namespace Discord.WebSocket | |||
else | |||
{ | |||
member = SocketThreadUser.Create(Guild, this, model, guildMember); | |||
member.GlobalUser.AddRef(); | |||
_members[member.Id] = member; | |||
} | |||
return member; | |||
@@ -44,14 +44,14 @@ namespace Discord.WebSocket | |||
: base(discord, id, guild) | |||
{ | |||
} | |||
internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) | |||
internal new static SocketVoiceChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
/// <inheritdoc /> | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
base.Update(state, model); | |||
Bitrate = model.Bitrate.Value; | |||
@@ -14,11 +14,11 @@ using ChannelModel = Discord.API.Channel; | |||
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | |||
using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | |||
using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; | |||
using MemberModel = Discord.API.GuildMember; | |||
using MemberModel = Discord.IMemberModel; | |||
using Model = Discord.API.Guild; | |||
using PresenceModel = Discord.API.Presence; | |||
using RoleModel = Discord.API.Role; | |||
using UserModel = Discord.API.User; | |||
using UserModel = Discord.IUserModel; | |||
using VoiceStateModel = Discord.API.VoiceState; | |||
using StickerModel = Discord.API.Sticker; | |||
using EventModel = Discord.API.GuildScheduledEvent; | |||
@@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | |||
private TaskCompletionSource<AudioClient> _audioConnectPromise; | |||
private ConcurrentDictionary<ulong, SocketGuildChannel> _channels; | |||
private ConcurrentDictionary<ulong, SocketGuildUser> _members; | |||
//private ConcurrentDictionary<ulong, SocketGuildUser> _members; | |||
private ConcurrentDictionary<ulong, SocketRole> _roles; | |||
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | |||
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | |||
@@ -305,7 +305,7 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// Gets the current logged-in user. | |||
/// </summary> | |||
public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; | |||
public SocketGuildUser CurrentUser => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(Discord.CurrentUser.Id) : null; | |||
/// <summary> | |||
/// Gets the built-in role containing all users in this guild. | |||
/// </summary> | |||
@@ -324,7 +324,7 @@ namespace Discord.WebSocket | |||
get | |||
{ | |||
var channels = _channels; | |||
var state = Discord.State; | |||
var state = Discord.StateManager; | |||
return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); | |||
} | |||
} | |||
@@ -356,7 +356,7 @@ namespace Discord.WebSocket | |||
/// <returns> | |||
/// A collection of guild users found within this guild. | |||
/// </returns> | |||
public IReadOnlyCollection<SocketGuildUser> Users => _members.ToReadOnlyCollection(); | |||
public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray<SocketGuildUser>.Empty; | |||
/// <summary> | |||
/// Gets a collection of all roles in this guild. | |||
/// </summary> | |||
@@ -382,13 +382,13 @@ namespace Discord.WebSocket | |||
_audioLock = new SemaphoreSlim(1, 1); | |||
_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); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal void Update(ClientState state, ExtendedModel model) | |||
internal void Update(ClientStateManager state, ExtendedModel model) | |||
{ | |||
IsAvailable = !(model.Unavailable ?? false); | |||
if (!IsAvailable) | |||
@@ -397,8 +397,6 @@ namespace Discord.WebSocket | |||
_events = new ConcurrentDictionary<ulong, SocketGuildEvent>(); | |||
if (_channels == null) | |||
_channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); | |||
if (_members == null) | |||
_members = new ConcurrentDictionary<ulong, SocketGuildUser>(); | |||
if (_roles == null) | |||
_roles = new ConcurrentDictionary<ulong, SocketRole>(); | |||
/*if (Emojis == null) | |||
@@ -431,25 +429,6 @@ namespace Discord.WebSocket | |||
_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)); | |||
{ | |||
for (int i = 0; i < model.VoiceStates.Length; i++) | |||
@@ -473,6 +452,10 @@ namespace Discord.WebSocket | |||
} | |||
_events = events; | |||
DownloadedMemberCount = model.Members.Length; | |||
MemberCount = model.MemberCount; | |||
_syncPromise = new TaskCompletionSource<bool>(); | |||
_downloaderPromise = new TaskCompletionSource<bool>(); | |||
@@ -480,7 +463,7 @@ namespace Discord.WebSocket | |||
/*if (!model.Large) | |||
_ = _downloaderPromise.TrySetResultAsync(true);*/ | |||
} | |||
internal void Update(ClientState state, Model model) | |||
internal void Update(ClientStateManager state, Model model) | |||
{ | |||
AFKChannelId = model.AFKChannelId; | |||
if (model.WidgetChannelId.IsSpecified) | |||
@@ -561,31 +544,18 @@ namespace Discord.WebSocket | |||
else | |||
_stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | |||
} | |||
/*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related | |||
internal async ValueTask UpdateCacheAsync(ExtendedModel model) | |||
{ | |||
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]); | |||
members.TryAdd(member.Id, member); | |||
} | |||
DownloadedMemberCount = members.Count; | |||
await Discord.StateManager.PresenceStore.BulkAddOrUpdateAsync(model.Presences); | |||
for (int i = 0; i < model.Presences.Length; i++) | |||
{ | |||
if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | |||
member.Update(state, model.Presences[i], true); | |||
} | |||
} | |||
_members = members; | |||
await Discord.StateManager.UserStore.BulkAddOrUpdateAsync(model.Members.Select(x => x.User)); | |||
var _ = _syncPromise.TrySetResultAsync(true); | |||
//if (!model.Large) | |||
// _ = _downloaderPromise.TrySetResultAsync(true); | |||
}*/ | |||
if(Discord.StateManager.TryGetMemberStore(Id, out var store)) | |||
store.BulkAddOrUpdate(model.Members); | |||
} | |||
internal void Update(ClientState state, EmojiUpdateModel model) | |||
internal void Update(ClientStateManager state, EmojiUpdateModel model) | |||
{ | |||
var emotes = ImmutableArray.CreateBuilder<GuildEmote>(model.Emojis.Length); | |||
for (int i = 0; i < model.Emojis.Length; i++) | |||
@@ -682,7 +652,7 @@ namespace Discord.WebSocket | |||
/// </returns> | |||
public SocketGuildChannel GetChannel(ulong id) | |||
{ | |||
var channel = Discord.State.GetChannel(id) as SocketGuildChannel; | |||
var channel = Discord.StateManager.GetChannel(id) as SocketGuildChannel; | |||
if (channel?.Guild.Id == Id) | |||
return channel; | |||
return null; | |||
@@ -799,7 +769,7 @@ namespace Discord.WebSocket | |||
public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | |||
=> GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); | |||
internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) | |||
internal SocketGuildChannel AddChannel(ClientStateManager state, ChannelModel model) | |||
{ | |||
var channel = SocketGuildChannel.Create(this, state, model); | |||
_channels.TryAdd(model.Id, channel); | |||
@@ -807,26 +777,26 @@ namespace Discord.WebSocket | |||
return channel; | |||
} | |||
internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) | |||
internal SocketGuildChannel AddOrUpdateChannel(ClientStateManager state, ChannelModel model) | |||
{ | |||
if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) | |||
channel.Update(Discord.State, model); | |||
channel.Update(Discord.StateManager, model); | |||
else | |||
{ | |||
channel = SocketGuildChannel.Create(this, Discord.State, model); | |||
channel = SocketGuildChannel.Create(this, Discord.StateManager, model); | |||
_channels[channel.Id] = channel; | |||
state.AddChannel(channel); | |||
} | |||
return channel; | |||
} | |||
internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) | |||
internal SocketGuildChannel RemoveChannel(ClientStateManager state, ulong id) | |||
{ | |||
if (_channels.TryRemove(id, out var _)) | |||
return state.RemoveChannel(id) as SocketGuildChannel; | |||
return null; | |||
} | |||
internal void PurgeChannelCache(ClientState state) | |||
internal void PurgeChannelCache(ClientStateManager state) | |||
{ | |||
foreach (var channelId in _channels) | |||
state.RemoveChannel(channelId.Key); | |||
@@ -880,7 +850,7 @@ namespace Discord.WebSocket | |||
foreach (var command in commands) | |||
{ | |||
Discord.State.AddCommand(command); | |||
Discord.StateManager.AddCommand(command); | |||
} | |||
return commands.ToImmutableArray(); | |||
@@ -898,7 +868,7 @@ namespace Discord.WebSocket | |||
/// </returns> | |||
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) | |||
return command; | |||
@@ -913,7 +883,7 @@ namespace Discord.WebSocket | |||
command = SocketApplicationCommand.Create(Discord, model, Id); | |||
Discord.State.AddCommand(command); | |||
Discord.StateManager.AddCommand(command); | |||
return command; | |||
} | |||
@@ -930,7 +900,7 @@ namespace Discord.WebSocket | |||
{ | |||
var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); | |||
var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); | |||
var entity = Discord.StateManager.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); | |||
entity.Update(model); | |||
@@ -952,11 +922,11 @@ namespace Discord.WebSocket | |||
var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); | |||
Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); | |||
Discord.StateManager.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); | |||
foreach(var entity in entities) | |||
{ | |||
Discord.State.AddCommand(entity); | |||
Discord.StateManager.AddCommand(entity); | |||
} | |||
return entities.ToImmutableArray(); | |||
@@ -1020,7 +990,7 @@ namespace Discord.WebSocket | |||
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); | |||
internal SocketRole AddRole(RoleModel model) | |||
{ | |||
var role = SocketRole.Create(this, Discord.State, model); | |||
var role = SocketRole.Create(this, Discord.StateManager, model); | |||
_roles[model.Id] = role; | |||
return role; | |||
} | |||
@@ -1034,7 +1004,7 @@ namespace Discord.WebSocket | |||
internal SocketRole AddOrUpdateRole(RoleModel model) | |||
{ | |||
if (_roles.TryGetValue(model.Id, out SocketRole role)) | |||
_roles[model.Id].Update(Discord.State, model); | |||
_roles[model.Id].Update(Discord.StateManager, model); | |||
else | |||
role = AddRole(model); | |||
@@ -1089,60 +1059,43 @@ namespace Discord.WebSocket | |||
/// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. | |||
/// </returns> | |||
public SocketGuildUser GetUser(ulong id) | |||
{ | |||
if (_members.TryGetValue(id, out SocketGuildUser member)) | |||
return member; | |||
return null; | |||
} | |||
=> Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(id) : null; | |||
/// <inheritdoc /> | |||
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); | |||
internal SocketGuildUser AddOrUpdateUser(UserModel model) | |||
{ | |||
if (_members.TryGetValue(model.Id, out SocketGuildUser member)) | |||
member.GlobalUser?.Update(Discord.State, model); | |||
SocketGuildUser member; | |||
if ((member = GetUser(model.Id)) != null) | |||
member.Update(model); | |||
else | |||
{ | |||
member = SocketGuildUser.Create(this, Discord.State, model); | |||
member.GlobalUser.AddRef(); | |||
_members[member.Id] = member; | |||
member = SocketGuildUser.Create(Id, Discord, model); | |||
DownloadedMemberCount++; | |||
} | |||
return member; | |||
} | |||
internal SocketGuildUser AddOrUpdateUser(MemberModel model) | |||
{ | |||
if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) | |||
member.Update(Discord.State, model); | |||
else | |||
{ | |||
member = SocketGuildUser.Create(this, Discord.State, model); | |||
member.GlobalUser.AddRef(); | |||
_members[member.Id] = member; | |||
DownloadedMemberCount++; | |||
} | |||
return member; | |||
} | |||
internal SocketGuildUser AddOrUpdateUser(PresenceModel model) | |||
{ | |||
if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) | |||
member.Update(Discord.State, model, false); | |||
SocketGuildUser member; | |||
if ((member = GetUser(model.Id)) != null) | |||
member.Update(model); | |||
else | |||
{ | |||
member = SocketGuildUser.Create(this, Discord.State, model); | |||
member.GlobalUser.AddRef(); | |||
_members[member.Id] = member; | |||
member = SocketGuildUser.Create(Id, Discord, model); | |||
DownloadedMemberCount++; | |||
} | |||
return member; | |||
} | |||
internal SocketGuildUser RemoveUser(ulong id) | |||
{ | |||
if (_members.TryRemove(id, out SocketGuildUser member)) | |||
SocketGuildUser member; | |||
if ((member = GetUser(id)) != null) | |||
{ | |||
DownloadedMemberCount--; | |||
member.GlobalUser.RemoveRef(Discord); | |||
if (Discord.StateManager.TryGetMemberStore(Id, out var store)) | |||
store.Remove(id); | |||
return member; | |||
} | |||
return null; | |||
@@ -1158,18 +1111,17 @@ namespace Discord.WebSocket | |||
/// <param name="predicate">The predicate used to select which users to clear.</param> | |||
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); | |||
if(Discord.StateManager.TryGetMemberStore(Id, out var store)) | |||
foreach (var member in membersToPurge) | |||
store.Remove(member.Id); | |||
_downloaderPromise = new TaskCompletionSource<bool>(); | |||
DownloadedMemberCount = _members.Count; | |||
DownloadedMemberCount = membersToKeep.Count(); | |||
} | |||
/// <summary> | |||
@@ -1536,7 +1488,7 @@ namespace Discord.WebSocket | |||
#endregion | |||
#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 before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; | |||
@@ -89,13 +89,13 @@ namespace Discord.WebSocket | |||
if(guildUser != null) | |||
{ | |||
if(model.Creator.IsSpecified) | |||
guildUser.Update(Discord.State, model.Creator.Value); | |||
guildUser.Update(model.Creator.Value); | |||
Creator = guildUser; | |||
} | |||
else if (guildUser == null && model.Creator.IsSpecified) | |||
{ | |||
guildUser = SocketGuildUser.Create(Guild, Discord.State, model.Creator.Value); | |||
guildUser = SocketGuildUser.Create(Guild.Id, Discord, model.Creator.Value); | |||
Creator = guildUser; | |||
} | |||
} | |||
@@ -56,18 +56,18 @@ namespace Discord.WebSocket | |||
if (Channel is SocketGuildChannel channel) | |||
{ | |||
if (model.Message.Value.WebhookId.IsSpecified) | |||
author = SocketWebhookUser.Create(channel.Guild, Discord.State, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); | |||
author = SocketWebhookUser.Create(channel.Guild, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); | |||
else if (model.Message.Value.Author.IsSpecified) | |||
author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); | |||
} | |||
else if (model.Message.Value.Author.IsSpecified) | |||
author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); | |||
Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value); | |||
Message = SocketUserMessage.Create(Discord, Discord.StateManager, author, Channel, model.Message.Value); | |||
} | |||
else | |||
{ | |||
Message.Update(Discord.State, model.Message.Value); | |||
Message.Update(Discord.StateManager, model.Message.Value); | |||
} | |||
} | |||
} | |||
@@ -29,7 +29,7 @@ namespace Discord.WebSocket | |||
{ | |||
foreach (var user in resolved.Users.Value) | |||
{ | |||
var socketUser = discord.GetOrCreateUser(discord.State, user.Value); | |||
var socketUser = discord.GetOrCreateUser(discord.StateManager, user.Value); | |||
Users.Add(ulong.Parse(user.Key), socketUser); | |||
} | |||
@@ -50,11 +50,11 @@ namespace Discord.WebSocket | |||
: discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); | |||
socketChannel = guild != null | |||
? SocketGuildChannel.Create(guild, discord.State, channelModel) | |||
: (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); | |||
? SocketGuildChannel.Create(guild, discord.StateManager, channelModel) | |||
: (SocketChannel)SocketChannel.CreatePrivate(discord, discord.StateManager, channelModel); | |||
} | |||
discord.State.AddChannel(socketChannel); | |||
discord.StateManager.AddChannel(socketChannel); | |||
Channels.Add(ulong.Parse(channel.Key), socketChannel); | |||
} | |||
} | |||
@@ -88,7 +88,7 @@ namespace Discord.WebSocket | |||
if (guild != null) | |||
{ | |||
if (msg.Value.WebhookId.IsSpecified) | |||
author = SocketWebhookUser.Create(guild, discord.State, msg.Value.Author.Value, msg.Value.WebhookId.Value); | |||
author = SocketWebhookUser.Create(guild, msg.Value.Author.Value, msg.Value.WebhookId.Value); | |||
else | |||
author = guild.GetUser(msg.Value.Author.Value.Id); | |||
} | |||
@@ -99,11 +99,11 @@ namespace Discord.WebSocket | |||
{ | |||
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); | |||
} | |||
} | |||
@@ -129,7 +129,7 @@ namespace Discord.WebSocket | |||
Author = author; | |||
Source = source; | |||
} | |||
internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) | |||
internal static SocketMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) | |||
{ | |||
if (model.Type == MessageType.Default || | |||
model.Type == MessageType.Reply || | |||
@@ -140,7 +140,7 @@ namespace Discord.WebSocket | |||
else | |||
return SocketSystemMessage.Create(discord, state, author, channel, model); | |||
} | |||
internal virtual void Update(ClientState state, Model model) | |||
internal virtual void Update(ClientStateManager state, Model model) | |||
{ | |||
Type = model.Type; | |||
@@ -252,7 +252,7 @@ namespace Discord.WebSocket | |||
if (user != null) | |||
newMentions.Add(user); | |||
else | |||
newMentions.Add(SocketUnknownUser.Create(Discord, state, val)); | |||
newMentions.Add(SocketUnknownUser.Create(Discord, val)); | |||
} | |||
} | |||
_userMentions = newMentions.ToImmutable(); | |||
@@ -264,7 +264,7 @@ namespace Discord.WebSocket | |||
Interaction = new MessageInteraction<SocketUser>(model.Interaction.Value.Id, | |||
model.Interaction.Value.Type, | |||
model.Interaction.Value.Name, | |||
SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User)); | |||
SocketGlobalUser.Create(Discord, model.Interaction.Value.User)); | |||
} | |||
if (model.Flags.IsSpecified) | |||
@@ -13,13 +13,13 @@ namespace Discord.WebSocket | |||
: base(discord, id, channel, author, MessageSource.System) | |||
{ | |||
} | |||
internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) | |||
internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) | |||
{ | |||
var entity = new SocketSystemMessage(discord, model.Id, channel, author); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
base.Update(state, model); | |||
} | |||
@@ -53,14 +53,14 @@ namespace Discord.WebSocket | |||
: base(discord, id, channel, author, source) | |||
{ | |||
} | |||
internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) | |||
internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) | |||
{ | |||
var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal override void Update(ClientState state, Model model) | |||
internal override void Update(ClientStateManager state, Model model) | |||
{ | |||
base.Update(state, model); | |||
@@ -122,14 +122,14 @@ namespace Discord.WebSocket | |||
if (guild != null) | |||
{ | |||
if (webhookId != null) | |||
refMsgAuthor = SocketWebhookUser.Create(guild, state, refMsg.Author.Value, webhookId.Value); | |||
refMsgAuthor = SocketWebhookUser.Create(guild, refMsg.Author.Value, webhookId.Value); | |||
else | |||
refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); | |||
} | |||
else | |||
refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); | |||
if (refMsgAuthor == null) | |||
refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); | |||
refMsgAuthor = SocketUnknownUser.Create(Discord, refMsg.Author.Value); | |||
} | |||
else | |||
// Message author wasn't specified in the payload, so create a completely anonymous unknown user | |||
@@ -67,13 +67,13 @@ namespace Discord.WebSocket | |||
{ | |||
Guild = guild; | |||
} | |||
internal static SocketRole Create(SocketGuild guild, ClientState state, Model model) | |||
internal static SocketRole Create(SocketGuild guild, ClientStateManager state, Model model) | |||
{ | |||
var entity = new SocketRole(guild, model.Id); | |||
entity.Update(state, model); | |||
return entity; | |||
} | |||
internal void Update(ClientState state, Model model) | |||
internal void Update(ClientStateManager state, Model model) | |||
{ | |||
Name = model.Name; | |||
IsHoisted = model.Hoist; | |||
@@ -1,51 +1,36 @@ | |||
using System; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
using Model = Discord.API.User; | |||
using Model = Discord.IUserModel; | |||
namespace Discord.WebSocket | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
internal class SocketGlobalUser : SocketUser | |||
internal class SocketGlobalUser : SocketUser, IDisposable | |||
{ | |||
public override bool IsBot { get; internal set; } | |||
public override string Username { get; internal set; } | |||
public override ushort DiscriminatorValue { get; internal set; } | |||
public override string AvatarId { get; internal set; } | |||
internal override SocketPresence Presence { get; set; } | |||
public override bool IsWebhook => false; | |||
internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } | |||
private readonly object _lockObj = new object(); | |||
private ushort _references; | |||
private SocketGlobalUser(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientState state, Model model) | |||
internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketGlobalUser(discord, model.Id); | |||
entity.Update(state, model); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal void AddRef() | |||
{ | |||
checked | |||
{ | |||
lock (_lockObj) | |||
_references++; | |||
} | |||
} | |||
internal void RemoveRef(DiscordSocketClient discord) | |||
~SocketGlobalUser() => Dispose(); | |||
public override void Dispose() | |||
{ | |||
lock (_lockObj) | |||
{ | |||
if (--_references <= 0) | |||
discord.RemoveUser(Id); | |||
} | |||
GC.SuppressFinalize(this); | |||
Discord.StateManager.UserStore.RemoveReference(Id); | |||
} | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | |||
@@ -18,38 +18,33 @@ namespace Discord.WebSocket | |||
/// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. | |||
/// </returns> | |||
public SocketGroupChannel Channel { get; } | |||
/// <inheritdoc /> | |||
internal override SocketGlobalUser GlobalUser { get; set; } | |||
/// <inheritdoc /> | |||
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||
/// <inheritdoc /> | |||
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||
/// <inheritdoc /> | |||
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||
/// <inheritdoc /> | |||
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||
/// <inheritdoc /> | |||
internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||
/// <inheritdoc /> | |||
public override bool IsWebhook => false; | |||
internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) | |||
: base(channel.Discord, globalUser.Id) | |||
internal SocketGroupUser(SocketGroupChannel channel, ulong userId) | |||
: base(channel.Discord, userId) | |||
{ | |||
Channel = channel; | |||
GlobalUser = globalUser; | |||
} | |||
internal static SocketGroupUser Create(SocketGroupChannel channel, ClientState state, Model model) | |||
internal static SocketGroupUser Create(SocketGroupChannel channel, Model model) | |||
{ | |||
var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); | |||
entity.Update(state, model); | |||
var entity = new SocketGroupUser(channel, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; | |||
internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | |||
public override void Dispose() | |||
{ | |||
GC.SuppressFinalize(this); | |||
if (GlobalUser.IsValueCreated) | |||
GlobalUser.Value.Dispose(); | |||
} | |||
~SocketGroupUser() => Dispose(); | |||
#endregion | |||
#region IVoiceState | |||
@@ -6,9 +6,9 @@ using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using UserModel = Discord.API.User; | |||
using MemberModel = Discord.API.GuildMember; | |||
using PresenceModel = Discord.API.Presence; | |||
using UserModel = Discord.IUserModel; | |||
using MemberModel = Discord.IMemberModel; | |||
using PresenceModel = Discord.IPresenceModel; | |||
namespace Discord.WebSocket | |||
{ | |||
@@ -16,19 +16,23 @@ namespace Discord.WebSocket | |||
/// Represents a WebSocket-based guild user. | |||
/// </summary> | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketGuildUser : SocketUser, IGuildUser | |||
public class SocketGuildUser : SocketUser, IGuildUser, ICached<MemberModel>, IDisposable | |||
{ | |||
#region SocketGuildUser | |||
private long? _premiumSinceTicks; | |||
private long? _timedOutTicks; | |||
private long? _joinedAtTicks; | |||
private ImmutableArray<ulong> _roleIds; | |||
private ulong _guildId; | |||
internal override SocketGlobalUser GlobalUser { get; set; } | |||
/// <summary> | |||
/// Gets the guild the user is in. | |||
/// </summary> | |||
public SocketGuild Guild { get; } | |||
public Lazy<SocketGuild> Guild { get; } // TODO: convert to LazyCached once guilds are cached. | |||
/// <summary> | |||
/// Gets the ID of the guild that the user is in. | |||
/// </summary> | |||
public ulong GuildId => _guildId; | |||
/// <inheritdoc /> | |||
public string DisplayName => Nickname ?? Username; | |||
/// <inheritdoc /> | |||
@@ -38,17 +42,16 @@ namespace Discord.WebSocket | |||
/// <inheritdoc/> | |||
public string GuildAvatarId { get; private set; } | |||
/// <inheritdoc /> | |||
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||
public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } | |||
/// <inheritdoc /> | |||
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||
public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } | |||
/// <inheritdoc /> | |||
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||
public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } | |||
/// <inheritdoc /> | |||
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||
public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } | |||
/// <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 /> | |||
public override bool IsWebhook => false; | |||
@@ -71,14 +74,13 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public bool? IsPending { get; private set; } | |||
/// <inheritdoc /> | |||
public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); | |||
/// <summary> | |||
/// Returns a collection of roles that the user possesses. | |||
/// </summary> | |||
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> | |||
/// Returns the voice channel the user is in, or <c>null</c> if none. | |||
/// </summary> | |||
@@ -92,8 +94,8 @@ namespace Discord.WebSocket | |||
/// A <see cref="SocketVoiceState" /> representing the user's voice status; <c>null</c> if the user is not | |||
/// connected to a voice channel. | |||
/// </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 /> | |||
public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); | |||
/// <inheritdoc /> | |||
@@ -119,13 +121,13 @@ namespace Discord.WebSocket | |||
{ | |||
get | |||
{ | |||
if (Guild.OwnerId == Id) | |||
if (Guild.Value.OwnerId == Id) | |||
return int.MaxValue; | |||
int maxPos = 0; | |||
for (int i = 0; i < _roleIds.Length; i++) | |||
{ | |||
var role = Guild.GetRole(_roleIds[i]); | |||
var role = Guild.Value.GetRole(_roleIds[i]); | |||
if (role != null && role.Position > maxPos) | |||
maxPos = role.Position; | |||
} | |||
@@ -133,79 +135,43 @@ namespace Discord.WebSocket | |||
} | |||
} | |||
internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) | |||
: base(guild.Discord, globalUser.Id) | |||
internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client) | |||
: base(client, userId) | |||
{ | |||
Guild = guild; | |||
GlobalUser = globalUser; | |||
_guildId = guildId; | |||
Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||
} | |||
internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) | |||
internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) | |||
{ | |||
var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); | |||
entity.Update(state, model); | |||
entity.UpdateRoles(new ulong[0]); | |||
var entity = new SocketGuildUser(guildId, model.Id, client); | |||
if (entity.Update(model)) | |||
client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(entity.ToModel()); | |||
entity.UpdateRoles(Array.Empty<ulong>()); | |||
return entity; | |||
} | |||
internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel model) | |||
internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) | |||
{ | |||
var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | |||
entity.Update(state, model); | |||
if (!model.Roles.IsSpecified) | |||
entity.UpdateRoles(new ulong[0]); | |||
var entity = new SocketGuildUser(guildId, model.Id, client); | |||
entity.Update(model); | |||
client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(model); | |||
return entity; | |||
} | |||
internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) | |||
{ | |||
var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | |||
entity.Update(state, model, false); | |||
if (!model.Roles.IsSpecified) | |||
entity.UpdateRoles(new ulong[0]); | |||
return entity; | |||
} | |||
internal void Update(ClientState state, MemberModel model) | |||
{ | |||
base.Update(state, model.User); | |||
if (model.JoinedAt.IsSpecified) | |||
_joinedAtTicks = model.JoinedAt.Value.UtcTicks; | |||
if (model.Nick.IsSpecified) | |||
Nickname = model.Nick.Value; | |||
if (model.Avatar.IsSpecified) | |||
GuildAvatarId = model.Avatar.Value; | |||
if (model.Roles.IsSpecified) | |||
UpdateRoles(model.Roles.Value); | |||
if (model.PremiumSince.IsSpecified) | |||
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | |||
if (model.TimedOutUntil.IsSpecified) | |||
_timedOutTicks = model.TimedOutUntil.Value?.UtcTicks; | |||
if (model.Pending.IsSpecified) | |||
IsPending = model.Pending.Value; | |||
} | |||
internal void Update(ClientState state, PresenceModel model, bool updatePresence) | |||
{ | |||
if (updatePresence) | |||
{ | |||
Update(model); | |||
} | |||
if (model.Nick.IsSpecified) | |||
Nickname = model.Nick.Value; | |||
if (model.Roles.IsSpecified) | |||
UpdateRoles(model.Roles.Value); | |||
if (model.PremiumSince.IsSpecified) | |||
_premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | |||
} | |||
internal override void Update(PresenceModel model) | |||
internal void Update(MemberModel model) | |||
{ | |||
Presence ??= new SocketPresence(); | |||
Presence.Update(model); | |||
GlobalUser.Update(model); | |||
_joinedAtTicks = model.JoinedAt.HasValue ? model.JoinedAt.Value.UtcTicks : null; | |||
Nickname = model.Nickname; | |||
GuildAvatarId = model.GuildAvatar; | |||
UpdateRoles(model.Roles); | |||
if (model.PremiumSince.HasValue) | |||
_premiumSinceTicks = model.PremiumSince.Value.UtcTicks; | |||
if (model.CommunicationsDisabledUntil.HasValue) | |||
_timedOutTicks = model.CommunicationsDisabledUntil.Value.UtcTicks; | |||
IsPending = model.IsPending.GetValueOrDefault(false); | |||
} | |||
private void UpdateRoles(ulong[] roleIds) | |||
{ | |||
var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | |||
roles.Add(Guild.Id); | |||
roles.Add(_guildId); | |||
for (int i = 0; i < roleIds.Length; i++) | |||
roles.Add(roleIds[i]); | |||
_roleIds = roles.ToImmutable(); | |||
@@ -249,7 +215,7 @@ namespace Discord.WebSocket | |||
=> UserHelper.RemoveTimeOutAsync(this, Discord, options); | |||
/// <inheritdoc /> | |||
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 /> | |||
public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | |||
@@ -259,23 +225,30 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | |||
=> CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); | |||
=> CDN.GetGuildUserAvatarUrl(Id, _guildId, GuildAvatarId, size, format); | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | |||
internal new SocketGuildUser Clone() | |||
internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; | |||
public override void Dispose() | |||
{ | |||
var clone = MemberwiseClone() as SocketGuildUser; | |||
clone.GlobalUser = GlobalUser.Clone(); | |||
return clone; | |||
if (IsFreed) | |||
return; | |||
GC.SuppressFinalize(this); | |||
Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); | |||
IsFreed = true; | |||
} | |||
~SocketGuildUser() => Dispose(); | |||
#endregion | |||
#region IGuildUser | |||
/// <inheritdoc /> | |||
IGuild IGuildUser.Guild => Guild; | |||
IGuild IGuildUser.Guild => Guild.Value; | |||
/// <inheritdoc /> | |||
ulong IGuildUser.GuildId => Guild.Id; | |||
ulong IGuildUser.GuildId => _guildId; | |||
/// <inheritdoc /> | |||
IReadOnlyCollection<ulong> IGuildUser.RoleIds => _roleIds; | |||
@@ -283,5 +256,50 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; | |||
#endregion | |||
#region Cache | |||
internal new class CacheModel : MemberModel | |||
{ | |||
public ulong Id { get; set; } | |||
public string Nickname { get; set; } | |||
public string GuildAvatar { get; set; } | |||
public ulong[] Roles { get; set; } | |||
public DateTimeOffset? JoinedAt { get; set; } | |||
public DateTimeOffset? PremiumSince { get; set; } | |||
public bool IsDeaf { get; set; } | |||
public bool IsMute { get; set; } | |||
public bool? IsPending { get; set; } | |||
public DateTimeOffset? CommunicationsDisabledUntil { get; set; } | |||
} | |||
internal new MemberModel ToModel() | |||
{ | |||
var model = Discord.StateManager.GetModel<MemberModel, CacheModel>(); | |||
model.Id = Id; | |||
model.Nickname = Nickname; | |||
model.GuildAvatar = GuildAvatarId; | |||
model.Roles = _roleIds.ToArray(); | |||
model.JoinedAt = JoinedAt; | |||
model.PremiumSince = PremiumSince; | |||
model.IsDeaf = IsDeafened; | |||
model.IsMute = IsMuted; | |||
model.IsPending = IsPending; | |||
model.CommunicationsDisabledUntil = TimedOutUntil; | |||
return model; | |||
} | |||
MemberModel ICached<MemberModel>.ToModel() | |||
=> ToModel(); | |||
void ICached<MemberModel>.Update(MemberModel model) => Update(model); | |||
#endregion | |||
} | |||
} |
@@ -3,7 +3,7 @@ using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
using Model = Discord.API.Presence; | |||
using Model = Discord.IPresenceModel; | |||
namespace Discord.WebSocket | |||
{ | |||
@@ -11,8 +11,13 @@ namespace Discord.WebSocket | |||
/// Represents the WebSocket user's presence status. This may include their online status and their activity. | |||
/// </summary> | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketPresence : IPresence | |||
public class SocketPresence : IPresence, ICached<Model> | |||
{ | |||
internal ulong UserId; | |||
internal ulong? GuildId; | |||
internal bool IsFreed; | |||
internal DiscordSocketClient Discord; | |||
/// <inheritdoc /> | |||
public UserStatus Status { get; private set; } | |||
/// <inheritdoc /> | |||
@@ -20,17 +25,24 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public IReadOnlyCollection<IActivity> Activities { get; private set; } | |||
internal SocketPresence() { } | |||
internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | |||
public static SocketPresence Default | |||
=> new SocketPresence(null, UserStatus.Offline, null, null); | |||
internal SocketPresence(DiscordSocketClient discord) | |||
{ | |||
Discord = discord; | |||
} | |||
internal SocketPresence(DiscordSocketClient discord, UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | |||
: this(discord) | |||
{ | |||
Status = status; | |||
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | |||
Activities = activities ?? ImmutableList<IActivity>.Empty; | |||
} | |||
internal static SocketPresence Create(Model model) | |||
internal static SocketPresence Create(DiscordSocketClient client, Model model) | |||
{ | |||
var entity = new SocketPresence(); | |||
var entity = new SocketPresence(client); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
@@ -38,8 +50,10 @@ namespace Discord.WebSocket | |||
internal void Update(Model model) | |||
{ | |||
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; | |||
UserId = model.UserId; | |||
GuildId = model.GuildId; | |||
} | |||
/// <summary> | |||
@@ -76,9 +90,9 @@ namespace Discord.WebSocket | |||
/// <returns> | |||
/// A list of all <see cref="IActivity"/> that this user currently has available. | |||
/// </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; | |||
var list = new List<IActivity>(); | |||
foreach (var activity in activities) | |||
@@ -96,5 +110,122 @@ namespace Discord.WebSocket | |||
private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | |||
internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | |||
~SocketPresence() => Dispose(); | |||
public void Dispose() | |||
{ | |||
if (IsFreed) | |||
return; | |||
GC.SuppressFinalize(this); | |||
if(Discord != null) | |||
{ | |||
Discord.StateManager.PresenceStore.RemoveReference(UserId); | |||
IsFreed = true; | |||
} | |||
} | |||
#region Cache | |||
internal class CacheModel : Model | |||
{ | |||
public UserStatus Status { get; set; } | |||
public ClientType[] ActiveClients { get; set; } | |||
public IActivityModel[] Activities { get; set; } | |||
public ulong UserId { get; set; } | |||
public ulong? GuildId { get; set; } | |||
ulong IEntityModel<ulong>.Id | |||
{ | |||
get => UserId; | |||
set => UserId = value; | |||
} | |||
} | |||
internal class ActivityCacheModel : IActivityModel | |||
{ | |||
public string Id { get; set; } | |||
public string Url { get; set; } | |||
public string Name { get; set; } | |||
public ActivityType Type { get; set; } | |||
public string Details { get; set; } | |||
public string State { get; set; } | |||
public ActivityProperties Flags { get; set; } | |||
public DateTimeOffset CreatedAt { get; set; } | |||
public IEmojiModel Emoji { get; set; } | |||
public ulong? ApplicationId { get; set; } | |||
public string SyncId { get; set; } | |||
public string SessionId { get; set; } | |||
public string LargeImage { get; set; } | |||
public string LargeText { get; set; } | |||
public string SmallImage { get; set; } | |||
public string SmallText { get; set; } | |||
public string PartyId { get; set; } | |||
public long[] PartySize { get; set; } | |||
public string JoinSecret { get; set; } | |||
public string SpectateSecret { get; set; } | |||
public string MatchSecret { get; set; } | |||
public DateTimeOffset? TimestampStart { get; set; } | |||
public DateTimeOffset? TimestampEnd { get; set; } | |||
} | |||
private class EmojiCacheModel : IEmojiModel | |||
{ | |||
public ulong? Id { get; set; } | |||
public string Name { get; set; } | |||
public ulong[] Roles { get; set; } | |||
public bool RequireColons { get; set; } | |||
public bool IsManaged { get; set; } | |||
public bool IsAnimated { get; set; } | |||
public bool IsAvailable { get; set; } | |||
public ulong? CreatorId { get; set; } | |||
} | |||
internal Model ToModel() | |||
{ | |||
var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||
model.Status = Status; | |||
model.ActiveClients = ActiveClients.ToArray(); | |||
model.UserId = UserId; | |||
model.GuildId = GuildId; | |||
model.Activities = Activities.Select(x => | |||
{ | |||
switch (x) | |||
{ | |||
case Game game: | |||
switch (game) | |||
{ | |||
case RichGame richGame: | |||
return richGame.ToModel<ActivityCacheModel>(); | |||
case SpotifyGame spotify: | |||
return spotify.ToModel<ActivityCacheModel>(); | |||
case CustomStatusGame custom: | |||
return custom.ToModel<ActivityCacheModel, EmojiCacheModel>(); | |||
case StreamingGame stream: | |||
return stream.ToModel<ActivityCacheModel>(); | |||
} | |||
break; | |||
} | |||
return new ActivityCacheModel | |||
{ | |||
Name = x.Name, | |||
Details = x.Details, | |||
Flags = x.Flags, | |||
Type = x.Type | |||
}; | |||
}).ToArray(); | |||
return model; | |||
} | |||
Model ICached<Model>.ToModel() => ToModel(); | |||
void ICached<Model>.Update(Model model) => Update(model); | |||
bool ICached.IsFreed => IsFreed; | |||
#endregion | |||
} | |||
} |
@@ -2,7 +2,8 @@ using Discord.Rest; | |||
using System; | |||
using System.Diagnostics; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.User; | |||
using Model = Discord.ICurrentUserModel; | |||
using UserModel = Discord.IUserModel; | |||
namespace Discord.WebSocket | |||
{ | |||
@@ -10,7 +11,7 @@ namespace Discord.WebSocket | |||
/// Represents the logged-in WebSocket-based user. | |||
/// </summary> | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketSelfUser : SocketUser, ISelfUser | |||
public class SocketSelfUser : SocketUser, ISelfUser, ICached<Model> | |||
{ | |||
/// <inheritdoc /> | |||
public string Email { get; private set; } | |||
@@ -18,18 +19,6 @@ namespace Discord.WebSocket | |||
public bool IsVerified { get; private set; } | |||
/// <inheritdoc /> | |||
public bool IsMfaEnabled { get; private set; } | |||
internal override SocketGlobalUser GlobalUser { get; set; } | |||
/// <inheritdoc /> | |||
public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||
/// <inheritdoc /> | |||
public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||
/// <inheritdoc /> | |||
public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||
/// <inheritdoc /> | |||
public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||
/// <inheritdoc /> | |||
internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||
/// <inheritdoc /> | |||
public UserProperties Flags { get; internal set; } | |||
/// <inheritdoc /> | |||
@@ -40,48 +29,52 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public override bool IsWebhook => false; | |||
internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) | |||
: base(discord, globalUser.Id) | |||
internal SocketSelfUser(DiscordSocketClient discord, ulong userId) | |||
: base(discord, userId) | |||
{ | |||
GlobalUser = globalUser; | |||
} | |||
internal static SocketSelfUser Create(DiscordSocketClient discord, ClientState state, Model model) | |||
internal static SocketSelfUser Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); | |||
entity.Update(state, model); | |||
var entity = new SocketSelfUser(discord, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal override bool Update(ClientState state, Model model) | |||
internal override bool Update(UserModel model) | |||
{ | |||
bool hasGlobalChanges = base.Update(state, model); | |||
if (model.Email.IsSpecified) | |||
bool hasGlobalChanges = base.Update(model); | |||
if (model is not Model currentUserModel) | |||
throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); | |||
if(currentUserModel.Email != Email) | |||
{ | |||
Email = model.Email.Value; | |||
Email = currentUserModel.Email; | |||
hasGlobalChanges = true; | |||
} | |||
if (model.Verified.IsSpecified) | |||
if (currentUserModel.IsVerified.HasValue) | |||
{ | |||
IsVerified = model.Verified.Value; | |||
IsVerified = currentUserModel.IsVerified.Value; | |||
hasGlobalChanges = true; | |||
} | |||
if (model.MfaEnabled.IsSpecified) | |||
if (currentUserModel.IsMfaEnabled.HasValue) | |||
{ | |||
IsMfaEnabled = model.MfaEnabled.Value; | |||
IsMfaEnabled = currentUserModel.IsMfaEnabled.Value; | |||
hasGlobalChanges = true; | |||
} | |||
if (model.Flags.IsSpecified && model.Flags.Value != Flags) | |||
if (currentUserModel.Flags != Flags) | |||
{ | |||
Flags = (UserProperties)model.Flags.Value; | |||
Flags = currentUserModel.Flags; | |||
hasGlobalChanges = true; | |||
} | |||
if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType) | |||
if (currentUserModel.PremiumType != PremiumType) | |||
{ | |||
PremiumType = model.PremiumType.Value; | |||
PremiumType = currentUserModel.PremiumType; | |||
hasGlobalChanges = true; | |||
} | |||
if (model.Locale.IsSpecified && model.Locale.Value != Locale) | |||
if (currentUserModel.Locale != Locale) | |||
{ | |||
Locale = model.Locale.Value; | |||
Locale = currentUserModel.Locale; | |||
hasGlobalChanges = true; | |||
} | |||
return hasGlobalChanges; | |||
@@ -93,5 +86,63 @@ namespace Discord.WebSocket | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | |||
internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | |||
public override void Dispose() | |||
{ | |||
if (IsFreed) | |||
return; | |||
GC.SuppressFinalize(this); | |||
Discord.StateManager.UserStore.RemoveReference(Id); | |||
IsFreed = true; | |||
} | |||
#region Cache | |||
internal new class CacheModel : Model | |||
{ | |||
public bool? IsVerified { get; set; } | |||
public string Email { get; set; } | |||
public bool? IsMfaEnabled { get; set; } | |||
public UserProperties Flags { get; set; } | |||
public PremiumType PremiumType { get; set; } | |||
public string Locale { get; set; } | |||
public UserProperties PublicFlags { get; set; } | |||
public string Username { get; set; } | |||
public string Discriminator { get; set; } | |||
public bool? IsBot { get; set; } | |||
public string Avatar { get; set; } | |||
public ulong Id { get; set; } | |||
} | |||
internal new Model ToModel() | |||
{ | |||
var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||
model.Avatar = AvatarId; | |||
model.Discriminator = Discriminator; | |||
model.Email = Email; | |||
model.Flags = Flags; | |||
model.IsBot = IsBot; | |||
model.IsMfaEnabled = IsMfaEnabled; | |||
model.Locale = Locale; | |||
model.PremiumType = PremiumType; | |||
model.PublicFlags = PublicFlags ?? UserProperties.None; | |||
model.Username = Username; | |||
model.Id = Id; | |||
return model; | |||
} | |||
Model ICached<Model>.ToModel() => ToModel(); | |||
void ICached<Model>.Update(Model model) => Update(model); | |||
#endregion | |||
} | |||
} |
@@ -2,7 +2,7 @@ using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.ThreadMember; | |||
using Model = Discord.IThreadMemberModel; | |||
using System.Collections.Immutable; | |||
namespace Discord.WebSocket | |||
@@ -10,12 +10,12 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// Represents a thread user received over the gateway. | |||
/// </summary> | |||
public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser | |||
public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached<Model> | |||
{ | |||
/// <summary> | |||
/// Gets the <see cref="SocketThreadChannel"/> this user is in. | |||
/// </summary> | |||
public SocketThreadChannel Thread { get; private set; } | |||
public Lazy<SocketThreadChannel> Thread { get; private set; } | |||
/// <inheritdoc/> | |||
public DateTimeOffset ThreadJoinedAt { get; private set; } | |||
@@ -23,126 +23,142 @@ namespace Discord.WebSocket | |||
/// <summary> | |||
/// Gets the guild this user is in. | |||
/// </summary> | |||
public SocketGuild Guild { get; private set; } | |||
public Lazy<SocketGuild> Guild { get; private set; } | |||
/// <inheritdoc/> | |||
public DateTimeOffset? JoinedAt | |||
=> GuildUser.JoinedAt; | |||
=> GuildUser.Value.JoinedAt; | |||
/// <inheritdoc/> | |||
public string DisplayName | |||
=> GuildUser.Nickname ?? GuildUser.Username; | |||
=> GuildUser.Value.Nickname ?? GuildUser.Value.Username; | |||
/// <inheritdoc/> | |||
public string Nickname | |||
=> GuildUser.Nickname; | |||
=> GuildUser.Value.Nickname; | |||
/// <inheritdoc/> | |||
public DateTimeOffset? PremiumSince | |||
=> GuildUser.PremiumSince; | |||
=> GuildUser.Value.PremiumSince; | |||
/// <inheritdoc/> | |||
public DateTimeOffset? TimedOutUntil | |||
=> GuildUser.TimedOutUntil; | |||
=> GuildUser.Value.TimedOutUntil; | |||
/// <inheritdoc/> | |||
public bool? IsPending | |||
=> GuildUser.IsPending; | |||
=> GuildUser.Value.IsPending; | |||
/// <inheritdoc /> | |||
public int Hierarchy | |||
=> GuildUser.Hierarchy; | |||
=> GuildUser.Value.Hierarchy; | |||
/// <inheritdoc/> | |||
public override string AvatarId | |||
{ | |||
get => GuildUser.AvatarId; | |||
internal set => GuildUser.AvatarId = value; | |||
get => GuildUser.Value.AvatarId; | |||
internal set => GuildUser.Value.AvatarId = value; | |||
} | |||
/// <inheritdoc/> | |||
public string DisplayAvatarId => GuildAvatarId ?? AvatarId; | |||
/// <inheritdoc/> | |||
public string GuildAvatarId | |||
=> GuildUser.GuildAvatarId; | |||
=> GuildUser.Value.GuildAvatarId; | |||
/// <inheritdoc/> | |||
public override ushort DiscriminatorValue | |||
{ | |||
get => GuildUser.DiscriminatorValue; | |||
internal set => GuildUser.DiscriminatorValue = value; | |||
get => GuildUser.Value.DiscriminatorValue; | |||
internal set => GuildUser.Value.DiscriminatorValue = value; | |||
} | |||
/// <inheritdoc/> | |||
public override bool IsBot | |||
{ | |||
get => GuildUser.IsBot; | |||
internal set => GuildUser.IsBot = value; | |||
get => GuildUser.Value.IsBot; | |||
internal set => GuildUser.Value.IsBot = value; | |||
} | |||
/// <inheritdoc/> | |||
public override bool IsWebhook | |||
=> GuildUser.IsWebhook; | |||
=> GuildUser.Value.IsWebhook; | |||
/// <inheritdoc/> | |||
public override string Username | |||
{ | |||
get => GuildUser.Username; | |||
internal set => GuildUser.Username = value; | |||
get => GuildUser.Value.Username; | |||
internal set => GuildUser.Value.Username = value; | |||
} | |||
/// <inheritdoc/> | |||
public bool IsDeafened | |||
=> GuildUser.IsDeafened; | |||
=> GuildUser.Value.IsDeafened; | |||
/// <inheritdoc/> | |||
public bool IsMuted | |||
=> GuildUser.IsMuted; | |||
=> GuildUser.Value.IsMuted; | |||
/// <inheritdoc/> | |||
public bool IsSelfDeafened | |||
=> GuildUser.IsSelfDeafened; | |||
=> GuildUser.Value.IsSelfDeafened; | |||
/// <inheritdoc/> | |||
public bool IsSelfMuted | |||
=> GuildUser.IsSelfMuted; | |||
=> GuildUser.Value.IsSelfMuted; | |||
/// <inheritdoc/> | |||
public bool IsSuppressed | |||
=> GuildUser.IsSuppressed; | |||
=> GuildUser.Value.IsSuppressed; | |||
/// <inheritdoc/> | |||
public IVoiceChannel VoiceChannel | |||
=> GuildUser.VoiceChannel; | |||
=> GuildUser.Value.VoiceChannel; | |||
/// <inheritdoc/> | |||
public string VoiceSessionId | |||
=> GuildUser.VoiceSessionId; | |||
=> GuildUser.Value.VoiceSessionId; | |||
/// <inheritdoc/> | |||
public bool IsStreaming | |||
=> GuildUser.IsStreaming; | |||
=> GuildUser.Value.IsStreaming; | |||
/// <inheritdoc/> | |||
public bool IsVideoing | |||
=> GuildUser.IsVideoing; | |||
=> GuildUser.Value.IsVideoing; | |||
/// <inheritdoc/> | |||
public DateTimeOffset? RequestToSpeakTimestamp | |||
=> GuildUser.RequestToSpeakTimestamp; | |||
=> GuildUser.Value.RequestToSpeakTimestamp; | |||
private Lazy<SocketGuildUser> GuildUser { get; set; } | |||
private SocketGuildUser GuildUser { get; set; } | |||
private ulong _threadId; | |||
private ulong _guildId; | |||
internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId) | |||
: base(guild.Discord, userId) | |||
internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threadId, ulong userId) | |||
: base(client, userId) | |||
{ | |||
Thread = thread; | |||
Guild = guild; | |||
GuildUser = member; | |||
_guildId = guildId; | |||
_threadId = threadId; | |||
GuildUser = new(() => client.StateManager.TryGetMemberStore(guildId, out var store) ? store.Get(userId) : null); | |||
Thread = new(() => client.GetChannel(threadId) as SocketThreadChannel); | |||
Guild = new(() => client.GetGuild(guildId)); | |||
} | |||
internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) | |||
{ | |||
var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); | |||
var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model) | |||
{ | |||
var entity = new SocketThreadUser(client, guildId, threadId, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
@@ -150,89 +166,116 @@ namespace Discord.WebSocket | |||
internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | |||
{ | |||
// this is used for creating the owner of the thread. | |||
var entity = new SocketThreadUser(guild, thread, owner, owner.Id); | |||
entity.Update(new Model | |||
{ | |||
JoinTimestamp = thread.CreatedAt, | |||
}); | |||
var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, owner.Id); | |||
entity.ThreadJoinedAt = thread.CreatedAt; | |||
return entity; | |||
} | |||
internal void Update(Model model) | |||
{ | |||
ThreadJoinedAt = model.JoinTimestamp; | |||
ThreadJoinedAt = model.JoinedAt; | |||
} | |||
/// <inheritdoc/> | |||
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); | |||
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel); | |||
/// <inheritdoc/> | |||
public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); | |||
public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.Value.KickAsync(reason, options); | |||
/// <inheritdoc/> | |||
public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); | |||
public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.Value.ModifyAsync(func, options); | |||
/// <inheritdoc/> | |||
public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); | |||
public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(roleId, options); | |||
/// <inheritdoc/> | |||
public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); | |||
public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(role, options); | |||
/// <inheritdoc/> | |||
public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); | |||
public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roleIds, options); | |||
/// <inheritdoc/> | |||
public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); | |||
public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roles, options); | |||
/// <inheritdoc/> | |||
public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); | |||
public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(roleId, options); | |||
/// <inheritdoc/> | |||
public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); | |||
public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(role, options); | |||
/// <inheritdoc/> | |||
public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); | |||
public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roleIds, options); | |||
/// <inheritdoc/> | |||
public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); | |||
public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roles, options); | |||
/// <inheritdoc/> | |||
public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options); | |||
public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.Value.SetTimeOutAsync(span, options); | |||
/// <inheritdoc/> | |||
public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); | |||
public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options); | |||
/// <inheritdoc/> | |||
IThreadChannel IThreadUser.Thread => Thread; | |||
IThreadChannel IThreadUser.Thread => Thread.Value; | |||
/// <inheritdoc/> | |||
IGuild IThreadUser.Guild => Guild; | |||
IGuild IThreadUser.Guild => Guild.Value; | |||
/// <inheritdoc/> | |||
IGuild IGuildUser.Guild => Guild; | |||
IGuild IGuildUser.Guild => Guild.Value; | |||
/// <inheritdoc/> | |||
ulong IGuildUser.GuildId => Guild.Id; | |||
ulong IGuildUser.GuildId => Guild.Value.Id; | |||
/// <inheritdoc/> | |||
GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; | |||
GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions; | |||
/// <inheritdoc/> | |||
IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); | |||
IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Value.Roles.Select(x => x.Id).ToImmutableArray(); | |||
/// <inheritdoc /> | |||
string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size); | |||
string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size); | |||
/// <inheritdoc /> | |||
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); | |||
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); | |||
internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } | |||
internal override LazyCached<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } | |||
public override void Dispose() | |||
{ | |||
if (IsFreed) | |||
return; | |||
GC.SuppressFinalize(this); | |||
Discord.StateManager.GetThreadMemberStore(_threadId)?.RemoveReference(Id); | |||
IsFreed = true; | |||
} | |||
internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } | |||
/// <summary> | |||
/// Gets the guild user of this thread user. | |||
/// </summary> | |||
/// <param name="user"></param> | |||
public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; | |||
public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value; | |||
#region Cache | |||
internal new class CacheModel : Model | |||
{ | |||
public ulong Id { get; set; } | |||
public ulong? ThreadId { get; set; } | |||
public DateTimeOffset JoinedAt { get; set; } | |||
} | |||
internal new Model ToModel() | |||
{ | |||
var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||
model.Id = Id; | |||
model.ThreadId = _threadId; | |||
model.JoinedAt = ThreadJoinedAt; | |||
return model; | |||
} | |||
Model ICached<Model>.ToModel() => ToModel(); | |||
void ICached<Model>.Update(Model model) => Update(model); | |||
#endregion | |||
} | |||
} |
@@ -26,22 +26,22 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public override bool IsWebhook => false; | |||
/// <inheritdoc /> | |||
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } | |||
/// <inheritdoc /> | |||
/// <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 LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } } | |||
internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } } | |||
internal SocketUnknownUser(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) | |||
internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketUnknownUser(discord, model.Id); | |||
entity.Update(state, model); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
public override void Dispose() { } | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; | |||
internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; | |||
} | |||
@@ -6,8 +6,8 @@ using System.Globalization; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Discord.Rest; | |||
using Model = Discord.API.User; | |||
using PresenceModel = Discord.API.Presence; | |||
using Model = Discord.IUserModel; | |||
using PresenceModel = Discord.IPresenceModel; | |||
namespace Discord.WebSocket | |||
{ | |||
@@ -15,23 +15,23 @@ namespace Discord.WebSocket | |||
/// Represents a WebSocket-based user. | |||
/// </summary> | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public abstract class SocketUser : SocketEntity<ulong>, IUser | |||
public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model> | |||
{ | |||
/// <inheritdoc /> | |||
public abstract bool IsBot { get; internal set; } | |||
public virtual bool IsBot { get; internal set; } | |||
/// <inheritdoc /> | |||
public abstract string Username { get; internal set; } | |||
public virtual string Username { get; internal set; } | |||
/// <inheritdoc /> | |||
public abstract ushort DiscriminatorValue { get; internal set; } | |||
public virtual ushort DiscriminatorValue { get; internal set; } | |||
/// <inheritdoc /> | |||
public abstract string AvatarId { get; internal set; } | |||
public virtual string AvatarId { get; internal set; } | |||
/// <inheritdoc /> | |||
public abstract bool IsWebhook { get; } | |||
public virtual bool IsWebhook { get; } | |||
/// <inheritdoc /> | |||
public UserProperties? PublicFlags { get; private set; } | |||
internal abstract SocketGlobalUser GlobalUser { get; set; } | |||
internal abstract SocketPresence Presence { get; set; } | |||
internal virtual LazyCached<SocketGlobalUser> GlobalUser { get; set; } | |||
internal virtual LazyCached<SocketPresence> Presence { get; set; } | |||
internal bool IsFreed { get; set; } | |||
/// <inheritdoc /> | |||
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | |||
/// <inheritdoc /> | |||
@@ -39,11 +39,11 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public string Mention => MentionUtils.MentionUser(Id); | |||
/// <inheritdoc /> | |||
public UserStatus Status => Presence.Status; | |||
public UserStatus Status => Presence.Value.Status; | |||
/// <inheritdoc /> | |||
public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||
public IReadOnlyCollection<ClientType> ActiveClients => Presence.Value?.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||
/// <inheritdoc /> | |||
public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||
public IReadOnlyCollection<IActivity> Activities => Presence.Value?.Activities ?? ImmutableList<IActivity>.Empty; | |||
/// <summary> | |||
/// Gets mutual guilds shared with this user. | |||
/// </summary> | |||
@@ -56,48 +56,50 @@ namespace Discord.WebSocket | |||
internal SocketUser(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
Presence = new LazyCached<SocketPresence>(id, discord.StateManager.PresenceStore); | |||
GlobalUser = new LazyCached<SocketGlobalUser>(id, discord.StateManager.UserStore); | |||
} | |||
internal virtual bool Update(ClientState state, Model model) | |||
internal virtual bool Update(Model model) | |||
{ | |||
Presence ??= new SocketPresence(); | |||
bool hasChanges = false; | |||
if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) | |||
if (model.Avatar != AvatarId) | |||
{ | |||
AvatarId = model.Avatar.Value; | |||
AvatarId = model.Avatar; | |||
hasChanges = true; | |||
} | |||
if (model.Discriminator.IsSpecified) | |||
if (model.Discriminator != null) | |||
{ | |||
var newVal = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | |||
var newVal = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); | |||
if (newVal != DiscriminatorValue) | |||
{ | |||
DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | |||
DiscriminatorValue = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); | |||
hasChanges = true; | |||
} | |||
} | |||
if (model.Bot.IsSpecified && model.Bot.Value != IsBot) | |||
if (model.IsBot.HasValue && model.IsBot.Value != IsBot) | |||
{ | |||
IsBot = model.Bot.Value; | |||
IsBot = model.IsBot.Value; | |||
hasChanges = true; | |||
} | |||
if (model.Username.IsSpecified && model.Username.Value != Username) | |||
if (model.Username != Username) | |||
{ | |||
Username = model.Username.Value; | |||
Username = model.Username; | |||
hasChanges = true; | |||
} | |||
if (model.PublicFlags.IsSpecified && model.PublicFlags.Value != PublicFlags) | |||
if(model is ICurrentUserModel currentUserModel) | |||
{ | |||
PublicFlags = model.PublicFlags.Value; | |||
hasChanges = true; | |||
if (currentUserModel.PublicFlags != PublicFlags) | |||
{ | |||
PublicFlags = currentUserModel.PublicFlags; | |||
hasChanges = true; | |||
} | |||
} | |||
return hasChanges; | |||
} | |||
internal virtual void Update(PresenceModel model) | |||
{ | |||
Presence ??= new SocketPresence(); | |||
Presence.Update(model); | |||
} | |||
public abstract void Dispose(); | |||
/// <inheritdoc /> | |||
public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | |||
@@ -120,5 +122,37 @@ namespace Discord.WebSocket | |||
public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | |||
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | |||
internal SocketUser Clone() => MemberwiseClone() as SocketUser; | |||
#region Cache | |||
internal class CacheModel : Model | |||
{ | |||
public string Username { get; set; } | |||
public string Discriminator { get; set; } | |||
public bool? IsBot { get; set; } | |||
public string Avatar { get; set; } | |||
public ulong Id { get; set; } | |||
} | |||
internal Model ToModel() | |||
{ | |||
var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||
model.Avatar = AvatarId; | |||
model.Discriminator = Discriminator; | |||
model.Id = Id; | |||
model.IsBot = IsBot; | |||
model.Username = Username; | |||
return model; | |||
} | |||
Model ICached<Model>.ToModel() | |||
=> ToModel(); | |||
void ICached<Model>.Update(Model model) => Update(model); | |||
bool ICached.IsFreed => IsFreed; | |||
#endregion | |||
} | |||
} |
@@ -33,8 +33,8 @@ namespace Discord.WebSocket | |||
/// <inheritdoc /> | |||
public override bool IsWebhook => true; | |||
/// <inheritdoc /> | |||
internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } | |||
internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | |||
internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } } | |||
internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } } | |||
internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | |||
: base(guild.Discord, id) | |||
@@ -42,16 +42,17 @@ namespace Discord.WebSocket | |||
Guild = guild; | |||
WebhookId = webhookId; | |||
} | |||
internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) | |||
internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId) | |||
{ | |||
var entity = new SocketWebhookUser(guild, model.Id, webhookId); | |||
entity.Update(state, model); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; | |||
internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; | |||
#endregion | |||
public override void Dispose() { } | |||
#endregion | |||
#region IGuildUser | |||
/// <inheritdoc /> | |||
@@ -0,0 +1,56 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Text.RegularExpressions; | |||
namespace Discord.WebSocket | |||
{ | |||
internal static class CacheModelExtensions | |||
{ | |||
public static TDest ToSpecifiedModel<TId, TDest>(this IEntityModel<TId> source, TDest dest) | |||
where TId : IEquatable<TId> | |||
where TDest : IEntityModel<TId> | |||
{ | |||
if (source == null || dest == null) | |||
throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); | |||
// get the shared model interface | |||
var sourceType = source.GetType(); | |||
var destType = dest.GetType(); | |||
if (sourceType == destType) | |||
return (TDest)source; | |||
List<Type> sharedInterfaceModels = new(); | |||
foreach (var intf in sourceType.GetInterfaces()) | |||
{ | |||
if (destType.GetInterface(intf.Name) != null && intf.Name.Contains("Model")) | |||
sharedInterfaceModels.Add(intf); | |||
} | |||
if (sharedInterfaceModels.Count == 0) | |||
throw new NotSupportedException($"cannot find common shared model interface between {sourceType.Name} and {destType.Name}"); | |||
foreach (var interfaceType in sharedInterfaceModels) | |||
{ | |||
var intfName = interfaceType.GenericTypeArguments.Length == 0 ? interfaceType.FullName : | |||
$"{interfaceType.Namespace}.{Regex.Replace(interfaceType.Name, @"`\d+?$", "")}<{string.Join(", ", interfaceType.GenericTypeArguments.Select(x => x.FullName))}>"; | |||
foreach (var prop in interfaceType.GetProperties()) | |||
{ | |||
var sProp = sourceType.GetProperty($"{intfName}.{prop.Name}", BindingFlags.NonPublic | BindingFlags.Instance) ?? sourceType.GetProperty(prop.Name); | |||
var dProp = destType.GetProperty($"{intfName}.{prop.Name}", BindingFlags.NonPublic | BindingFlags.Instance) ?? destType.GetProperty(prop.Name); | |||
if (sProp == null || dProp == null) | |||
throw new NotSupportedException($"Couldn't find common interface property {prop.Name}"); | |||
dProp.SetValue(dest, sProp.GetValue(source)); | |||
} | |||
} | |||
return dest; | |||
} | |||
} | |||
} |
@@ -7,86 +7,97 @@ namespace Discord.WebSocket | |||
{ | |||
internal static class EntityExtensions | |||
{ | |||
public static IActivity ToEntity(this API.Game model) | |||
public static IActivity ToEntity(this IActivityModel model) | |||
{ | |||
#region Custom Status Game | |||
if (model.Id.IsSpecified && model.Id.Value == "custom") | |||
if (model.Id != null && model.Id == "custom") | |||
{ | |||
return new CustomStatusGame() | |||
{ | |||
Type = ActivityType.CustomStatus, | |||
Name = model.Name, | |||
State = model.State.IsSpecified ? model.State.Value : null, | |||
Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, | |||
CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), | |||
State = model.State, | |||
Emote = model.Emoji?.ToIEmote(), | |||
CreatedAt = model.CreatedAt, | |||
}; | |||
} | |||
#endregion | |||
#region Spotify Game | |||
if (model.SyncId.IsSpecified) | |||
if (model.SyncId != null) | |||
{ | |||
var assets = model.Assets.GetValueOrDefault()?.ToEntity(); | |||
string albumText = assets?[1]?.Text; | |||
string albumArtId = assets?[1]?.ImageId?.Replace("spotify:", ""); | |||
var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; | |||
string albumText = model.LargeText; | |||
string albumArtId = model.LargeImage?.Replace("spotify:", ""); | |||
return new SpotifyGame | |||
{ | |||
Name = model.Name, | |||
SessionId = model.SessionId.GetValueOrDefault(), | |||
TrackId = model.SyncId.Value, | |||
TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value), | |||
SessionId = model.SessionId, | |||
TrackId = model.SyncId, | |||
TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId), | |||
AlbumTitle = albumText, | |||
TrackTitle = model.Details.GetValueOrDefault(), | |||
Artists = model.State.GetValueOrDefault()?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), | |||
StartedAt = timestamps?.Start, | |||
EndsAt = timestamps?.End, | |||
Duration = timestamps?.End - timestamps?.Start, | |||
TrackTitle = model.Details, | |||
Artists = model.State?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), | |||
StartedAt = model.TimestampStart, | |||
EndsAt = model.TimestampEnd, | |||
Duration = model.TimestampEnd - model.TimestampStart, | |||
AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, | |||
Type = ActivityType.Listening, | |||
Flags = model.Flags.GetValueOrDefault(), | |||
Flags = model.Flags, | |||
AlbumArt = model.LargeImage, | |||
}; | |||
} | |||
#endregion | |||
#region Rich Game | |||
if (model.ApplicationId.IsSpecified) | |||
if (model.ApplicationId.HasValue) | |||
{ | |||
ulong appId = model.ApplicationId.Value; | |||
var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); | |||
return new RichGame | |||
{ | |||
ApplicationId = appId, | |||
Name = model.Name, | |||
Details = model.Details.GetValueOrDefault(), | |||
State = model.State.GetValueOrDefault(), | |||
SmallAsset = assets?[0], | |||
LargeAsset = assets?[1], | |||
Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, | |||
Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, | |||
Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null, | |||
Flags = model.Flags.GetValueOrDefault() | |||
Details = model.Details, | |||
State = model.State, | |||
SmallAsset = new GameAsset | |||
{ | |||
Text = model.SmallText, | |||
ImageId = model.SmallImage, | |||
ApplicationId = appId, | |||
}, | |||
LargeAsset = new GameAsset | |||
{ | |||
Text = model.LargeText, | |||
ApplicationId = appId, | |||
ImageId = model.LargeImage | |||
}, | |||
Party = model.PartyId != null ? new GameParty | |||
{ | |||
Id = model.PartyId, | |||
Capacity = model.PartySize?.Length > 1 ? model.PartySize[1] : 0, | |||
Members = model.PartySize?.Length > 0 ? model.PartySize[0] : 0 | |||
} : null, | |||
Secrets = model.JoinSecret != null || model.SpectateSecret != null || model.MatchSecret != null ? new GameSecrets(model.MatchSecret, model.JoinSecret, model.SpectateSecret) : null, | |||
Timestamps = model.TimestampStart.HasValue || model.TimestampEnd.HasValue ? new GameTimestamps(model.TimestampStart, model.TimestampEnd) : null, | |||
Flags = model.Flags | |||
}; | |||
} | |||
#endregion | |||
#region Stream Game | |||
if (model.StreamUrl.IsSpecified) | |||
if (model.Url != null) | |||
{ | |||
return new StreamingGame( | |||
model.Name, | |||
model.StreamUrl.Value) | |||
model.Url) | |||
{ | |||
Flags = model.Flags.GetValueOrDefault(), | |||
Details = model.Details.GetValueOrDefault() | |||
Flags = model.Flags, | |||
Details = model.Details | |||
}; | |||
} | |||
#endregion | |||
#region Normal Game | |||
return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing, | |||
model.Flags.IsSpecified ? model.Flags.Value : ActivityProperties.None, | |||
model.Details.GetValueOrDefault()); | |||
return new Game(model.Name, model.Type, model.Flags, model.Details); | |||
#endregion | |||
} | |||
@@ -19,13 +19,13 @@ namespace Discord.Interactions | |||
/// <param name="client">The underlying client.</param> | |||
/// <param name="interaction">The underlying interaction.</param> | |||
public ShardedInteractionContext (DiscordShardedClient client, TInteraction interaction) | |||
: base(client.GetShard(GetShardId(client, ( interaction.User as SocketGuildUser )?.Guild)), interaction) | |||
: base(client.GetShard(GetShardId(client, (interaction.User as SocketGuildUser )?.GuildId)), interaction) | |||
{ | |||
Client = client; | |||
} | |||
private static int GetShardId (DiscordShardedClient client, IGuild guild) | |||
=> guild == null ? 0 : client.GetShardIdFor(guild); | |||
private static int GetShardId(DiscordShardedClient client, ulong? guildId) | |||
=> guildId.HasValue ? client.GetShardIdFor(guildId.Value) : 0; | |||
} | |||
/// <summary> | |||
@@ -50,7 +50,7 @@ namespace Discord.Interactions | |||
{ | |||
Client = client; | |||
Channel = interaction.Channel; | |||
Guild = (interaction.User as SocketGuildUser)?.Guild; | |||
Guild = (interaction.User as SocketGuildUser)?.Guild.Value; | |||
User = interaction.User; | |||
Interaction = interaction; | |||
} | |||