| @@ -8,6 +8,10 @@ namespace Discord | |||||
| { | { | ||||
| internal interface ICached<TType> | internal interface ICached<TType> | ||||
| { | { | ||||
| void Update(TType model); | |||||
| TType ToModel(); | TType ToModel(); | ||||
| TResult ToModel<TResult>() where TResult : TType, new(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -6,7 +6,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IPresenceModel | |||||
| public interface IPresenceModel : IEntityModel<ulong> | |||||
| { | { | ||||
| ulong UserId { get; set; } | ulong UserId { get; set; } | ||||
| ulong? GuildId { get; set; } | ulong? GuildId { get; set; } | ||||
| @@ -6,10 +6,9 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IMemberModel | |||||
| public interface IMemberModel : IEntityModel<ulong> | |||||
| { | { | ||||
| IUserModel User { get; set; } | |||||
| //IUserModel User { get; set; } | |||||
| string Nickname { get; set; } | string Nickname { get; set; } | ||||
| string GuildAvatar { get; set; } | string GuildAvatar { get; set; } | ||||
| ulong[] Roles { get; set; } | ulong[] Roles { get; set; } | ||||
| @@ -0,0 +1,15 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IThreadMemberModel : IEntityModel<ulong> | |||||
| { | |||||
| ulong? ThreadId { get; set; } | |||||
| ulong? UserId { get; set; } | |||||
| DateTimeOffset JoinedAt { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -6,9 +6,8 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IUserModel | |||||
| public interface IUserModel : IEntityModel<ulong> | |||||
| { | { | ||||
| ulong Id { get; set; } | |||||
| string Username { get; set; } | string Username { get; set; } | ||||
| string Discriminator { get; set; } | string Discriminator { get; set; } | ||||
| bool? IsBot { get; set; } | bool? IsBot { get; set; } | ||||
| @@ -63,8 +63,8 @@ namespace Discord.API | |||||
| get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); | get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); | ||||
| } | } | ||||
| IUserModel IMemberModel.User { | |||||
| get => User; set => throw new NotSupportedException(); | |||||
| ulong IEntityModel<ulong>.Id { | |||||
| get => User.Id; set => throw new NotSupportedException(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -49,5 +49,8 @@ namespace Discord.API | |||||
| IActivityModel[] IPresenceModel.Activities { | IActivityModel[] IPresenceModel.Activities { | ||||
| get => Activities.ToArray(); set => throw new NotSupportedException(); | 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 | namespace Discord.API | ||||
| { | { | ||||
| internal class ThreadMember | |||||
| internal class ThreadMember : IThreadMemberModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public Optional<ulong> Id { get; set; } | |||||
| public Optional<ulong> ThreadId { get; set; } | |||||
| [JsonProperty("user_id")] | [JsonProperty("user_id")] | ||||
| public Optional<ulong> UserId { get; set; } | public Optional<ulong> UserId { get; set; } | ||||
| @@ -14,7 +14,9 @@ namespace Discord.API | |||||
| [JsonProperty("join_timestamp")] | [JsonProperty("join_timestamp")] | ||||
| public DateTimeOffset JoinTimestamp { get; set; } | public DateTimeOffset JoinTimestamp { get; set; } | ||||
| [JsonProperty("flags")] | |||||
| public int Flags { get; set; } // No enum type (yet?) | |||||
| ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } | |||||
| ulong? IThreadMemberModel.UserId { get => UserId.ToNullable(); set => throw new NotSupportedException(); } | |||||
| DateTimeOffset IThreadMemberModel.JoinedAt { get => JoinTimestamp; set => throw new NotSupportedException(); } | |||||
| ulong IEntityModel<ulong>.Id { get => UserId.GetValueOrDefault(0); set => throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -43,10 +43,10 @@ namespace Discord.API | |||||
| get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); | get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); | ||||
| } | } | ||||
| ulong IUserModel.Id | |||||
| ulong IEntityModel<ulong>.Id | |||||
| { | { | ||||
| get => Id; | get => Id; | ||||
| set => throw new NotSupportedException(); | set => throw new NotSupportedException(); | ||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -9,74 +9,74 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| public class DefaultConcurrentCacheProvider : ICacheProvider | public class DefaultConcurrentCacheProvider : ICacheProvider | ||||
| { | { | ||||
| private readonly ConcurrentDictionary<ulong, IUserModel> _users; | |||||
| private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IMemberModel>> _members; | |||||
| private readonly ConcurrentDictionary<ulong, IPresenceModel> _presense; | |||||
| private readonly ConcurrentDictionary<Type, object> _storeCache = new(); | |||||
| private readonly ConcurrentDictionary<object, object> _subStoreCache = new(); | |||||
| private ValueTask CompletedValueTask => new ValueTask(Task.CompletedTask).Preserve(); | |||||
| public DefaultConcurrentCacheProvider(int defaultConcurrency, int defaultCapacity) | |||||
| private class DefaultEntityStore<TModel, TId> : IEntityStore<TModel, TId> | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | { | ||||
| _users = new(defaultConcurrency, defaultCapacity); | |||||
| _members = new(defaultConcurrency, defaultCapacity); | |||||
| _presense = new(defaultConcurrency, defaultCapacity); | |||||
| } | |||||
| private ConcurrentDictionary<TId, TModel> _cache; | |||||
| public ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode mode) | |||||
| { | |||||
| _users.AddOrUpdate(model.Id, model, (_, __) => model); | |||||
| return CompletedValueTask; | |||||
| } | |||||
| public ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode mode) | |||||
| { | |||||
| var guildMemberCache = _members.GetOrAdd(guildId, (_) => new ConcurrentDictionary<ulong, IMemberModel>()); | |||||
| guildMemberCache.AddOrUpdate(model.User.Id, model, (_, __) => model); | |||||
| return CompletedValueTask; | |||||
| } | |||||
| public ValueTask<IMemberModel> GetMemberAsync(ulong id, ulong guildId, CacheRunMode mode) | |||||
| => new ValueTask<IMemberModel>(_members.FirstOrDefault(x => x.Key == guildId).Value?.FirstOrDefault(x => x.Key == id).Value); | |||||
| public DefaultEntityStore(ConcurrentDictionary<TId, TModel> cache) | |||||
| { | |||||
| _cache = cache; | |||||
| } | |||||
| public ValueTask<IEnumerable<IMemberModel>> GetMembersAsync(ulong guildId, CacheRunMode mode) | |||||
| { | |||||
| if(_members.TryGetValue(guildId, out var inner)) | |||||
| return new ValueTask<IEnumerable<IMemberModel>>(inner.ToArray().Select(x => x.Value)); // ToArray here is important before .Select due to concurrency | |||||
| return new ValueTask<IEnumerable<IMemberModel>>(Array.Empty<IMemberModel>()); | |||||
| } | |||||
| public ValueTask<IUserModel> GetUserAsync(ulong id, CacheRunMode mode) | |||||
| { | |||||
| if (_users.TryGetValue(id, out var result)) | |||||
| return new ValueTask<IUserModel>(result); | |||||
| return new ValueTask<IUserModel>((IUserModel)null); | |||||
| } | |||||
| public ValueTask<IEnumerable<IUserModel>> GetUsersAsync(CacheRunMode mode) | |||||
| => new ValueTask<IEnumerable<IUserModel>>(_users.ToArray().Select(x => x.Value)); | |||||
| public ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode mode) | |||||
| { | |||||
| if (_members.TryGetValue(guildId, out var inner)) | |||||
| inner.TryRemove(id, out var _); | |||||
| return CompletedValueTask; | |||||
| } | |||||
| public ValueTask RemoveUserAsync(ulong id, CacheRunMode mode) | |||||
| { | |||||
| _members.TryRemove(id, out var _); | |||||
| return CompletedValueTask; | |||||
| } | |||||
| public ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode) | |||||
| { | |||||
| _cache.AddOrUpdate(model.Id, model, (_, __) => model); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IPresenceModel> GetPresenceAsync(ulong userId, CacheRunMode runmode) | |||||
| { | |||||
| if (_presense.TryGetValue(userId, out var presense)) | |||||
| return new ValueTask<IPresenceModel>(presense); | |||||
| return new ValueTask<IPresenceModel>((IPresenceModel)null); | |||||
| public ValueTask AddOrUpdateBatchAsync(IEnumerable<TModel> models, CacheRunMode runmode) | |||||
| { | |||||
| foreach (var model in models) | |||||
| _cache.AddOrUpdate(model.Id, model, (_, __) => model); | |||||
| return default; | |||||
| } | |||||
| public IAsyncEnumerable<TModel> GetAllAsync(CacheRunMode runmode) | |||||
| { | |||||
| var coll = _cache.Select(x => x.Value).GetEnumerator(); | |||||
| return AsyncEnumerable.Create((_) => AsyncEnumerator.Create( | |||||
| () => new ValueTask<bool>(coll.MoveNext()), | |||||
| () => coll.Current, | |||||
| () => new ValueTask())); | |||||
| } | |||||
| public ValueTask<TModel> GetAsync(TId id, CacheRunMode runmode) | |||||
| { | |||||
| if (_cache.TryGetValue(id, out var model)) | |||||
| return new ValueTask<TModel>(model); | |||||
| return default; | |||||
| } | |||||
| public ValueTask RemoveAsync(TId id, CacheRunMode runmode) | |||||
| { | |||||
| _cache.TryRemove(id, out _); | |||||
| return default; | |||||
| } | |||||
| public ValueTask PurgeAllAsync(CacheRunMode runmode) | |||||
| { | |||||
| _cache.Clear(); | |||||
| return default; | |||||
| } | |||||
| } | } | ||||
| public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel presense, CacheRunMode runmode) | |||||
| public virtual ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>() | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | { | ||||
| _presense.AddOrUpdate(userId, presense, (_, __) => presense); | |||||
| return CompletedValueTask; | |||||
| 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 ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode) | |||||
| public virtual ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId) | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | { | ||||
| _presense.TryRemove(userId, out var _); | |||||
| return CompletedValueTask; | |||||
| var store = _subStoreCache.GetOrAdd(parentId, (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>())); | |||||
| return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -8,30 +8,24 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| public interface ICacheProvider | public interface ICacheProvider | ||||
| { | { | ||||
| #region Users | |||||
| ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>() | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId>; | |||||
| ValueTask<IUserModel> GetUserAsync(ulong id, CacheRunMode runmode); | |||||
| ValueTask<IEnumerable<IUserModel>> GetUsersAsync(CacheRunMode runmode); | |||||
| ValueTask AddOrUpdateUserAsync(IUserModel model, CacheRunMode runmode); | |||||
| ValueTask RemoveUserAsync(ulong id, CacheRunMode runmode); | |||||
| #endregion | |||||
| #region Members | |||||
| ValueTask<IMemberModel> GetMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); | |||||
| ValueTask<IEnumerable<IMemberModel>> GetMembersAsync(ulong guildId, CacheRunMode runmode); | |||||
| ValueTask AddOrUpdateMemberAsync(IMemberModel model, ulong guildId, CacheRunMode runmode); | |||||
| ValueTask RemoveMemberAsync(ulong id, ulong guildId, CacheRunMode runmode); | |||||
| #endregion | |||||
| #region Presence | |||||
| ValueTask<IPresenceModel> GetPresenceAsync(ulong userId, CacheRunMode runmode); | |||||
| ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresenceModel model, CacheRunMode runmode); | |||||
| ValueTask RemovePresenseAsync(ulong userId, CacheRunMode runmode); | |||||
| ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId) | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId>; | |||||
| } | |||||
| #endregion | |||||
| public interface IEntityStore<TModel, TId> | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | |||||
| ValueTask<TModel> GetAsync(TId id, CacheRunMode runmode); | |||||
| IAsyncEnumerable<TModel> GetAllAsync(CacheRunMode runmode); | |||||
| ValueTask AddOrUpdateAsync(TModel model, CacheRunMode runmode); | |||||
| ValueTask AddOrUpdateBatchAsync(IEnumerable<TModel> models, CacheRunMode runmode); | |||||
| ValueTask RemoveAsync(TId id, CacheRunMode runmode); | |||||
| ValueTask PurgeAllAsync(CacheRunMode runmode); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,163 +1,342 @@ | |||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Runtime.CompilerServices; | |||||
| using System.Text; | using System.Text; | ||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| internal class CacheWeakReference<T> : WeakReference | |||||
| internal class CacheReference<TType> where TType : class | |||||
| { | { | ||||
| public new T Target { get => (T)base.Target; set => base.Target = value; } | |||||
| public CacheWeakReference(T target) | |||||
| : base(target, false) | |||||
| public WeakReference<TType> Reference { get; } | |||||
| public bool CanRelease | |||||
| => !Reference.TryGetTarget(out _) || _referenceCount <= 0; | |||||
| private int _referenceCount; | |||||
| private readonly object _lock = new object(); | |||||
| public CacheReference(TType value) | |||||
| { | { | ||||
| Reference = new(value); | |||||
| _referenceCount = 1; | |||||
| } | |||||
| public bool TryObtainReference(out TType reference) | |||||
| { | |||||
| if (Reference.TryGetTarget(out reference)) | |||||
| { | |||||
| Interlocked.Increment(ref _referenceCount); | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | } | ||||
| public bool TryGetTarget(out T target) | |||||
| public void ReleaseReference() | |||||
| { | { | ||||
| target = Target; | |||||
| return IsAlive; | |||||
| lock (_lock) | |||||
| { | |||||
| if (_referenceCount > 0) | |||||
| _referenceCount--; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| internal partial class ClientStateManager | |||||
| internal class ReferenceStore<TEntity, TModel, TId, ISharedEntity> | |||||
| where TEntity : class, ICached<TModel>, ISharedEntity | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| where ISharedEntity : class | |||||
| { | { | ||||
| private readonly ConcurrentDictionary<ulong, CacheWeakReference<SocketGlobalUser>> _userReferences = new(); | |||||
| private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), CacheWeakReference<SocketGuildUser>> _memberReferences = new(); | |||||
| #region Helpers | |||||
| private void EnsureSync(ValueTask vt) | |||||
| private readonly ICacheProvider _cacheProvider; | |||||
| private readonly ConcurrentDictionary<TId, CacheReference<TEntity>> _references = new(); | |||||
| private IEntityStore<TModel, TId> _store; | |||||
| private Func<TModel, TEntity> _entityBuilder; | |||||
| private Func<TId, RequestOptions, Task<ISharedEntity>> _restLookup; | |||||
| private readonly bool _allowSyncWaits; | |||||
| private readonly object _lock = new(); | |||||
| public ReferenceStore(ICacheProvider cacheProvider, Func<TModel, TEntity> entityBuilder, Func<TId, RequestOptions, Task<ISharedEntity>> restLookup, bool allowSyncWaits) | |||||
| { | { | ||||
| if (!vt.IsCompleted) | |||||
| throw new NotSupportedException($"Cannot use async context for value task lookup"); | |||||
| _allowSyncWaits = allowSyncWaits; | |||||
| _cacheProvider = cacheProvider; | |||||
| _entityBuilder = entityBuilder; | |||||
| _restLookup = restLookup; | |||||
| } | } | ||||
| #endregion | |||||
| internal void ClearDeadReferences() | |||||
| { | |||||
| lock (_lock) | |||||
| { | |||||
| var references = _references.Where(x => x.Value.CanRelease).ToArray(); | |||||
| foreach (var reference in references) | |||||
| _references.TryRemove(reference.Key, out _); | |||||
| } | |||||
| } | |||||
| #region Global users | |||||
| internal void RemoveReferencedGlobalUser(ulong id) | |||||
| private TResult RunOrThrowValueTask<TResult>(ValueTask<TResult> t) | |||||
| { | { | ||||
| Console.WriteLine("Global user untracked"); | |||||
| _userReferences.TryRemove(id, out _); | |||||
| if (_allowSyncWaits) | |||||
| { | |||||
| return t.GetAwaiter().GetResult(); | |||||
| } | |||||
| else if (t.IsCompleted) | |||||
| return t.Result; | |||||
| else | |||||
| throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); | |||||
| } | } | ||||
| private void TrackGlobalUser(ulong id, SocketGlobalUser user) | |||||
| private void RunOrThrowValueTask(ValueTask t) | |||||
| { | { | ||||
| if (user != null) | |||||
| if (_allowSyncWaits) | |||||
| { | { | ||||
| _userReferences.TryAdd(id, new CacheWeakReference<SocketGlobalUser>(user)); | |||||
| t.GetAwaiter().GetResult(); | |||||
| } | } | ||||
| else if (!t.IsCompleted) | |||||
| throw new InvalidOperationException("Cannot run asynchronous value task in synchronous context"); | |||||
| } | } | ||||
| internal ValueTask<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | |||||
| => _state.GetUserAsync(id, mode.ToBehavior(), options); | |||||
| 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); | |||||
| } | |||||
| internal SocketGlobalUser GetUser(ulong id) | |||||
| private bool TryGetReference(TId id, out TEntity entity) | |||||
| { | { | ||||
| if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) | |||||
| return user; | |||||
| entity = null; | |||||
| return _references.TryGetValue(id, out var reference) && reference.TryObtainReference(out entity); | |||||
| } | |||||
| public TEntity Get(TId id) | |||||
| { | |||||
| if(TryGetReference(id, out var entity)) | |||||
| { | |||||
| return entity; | |||||
| } | |||||
| user = (SocketGlobalUser)_state.GetUserAsync(id, StateBehavior.SyncOnly).Result; | |||||
| var model = RunOrThrowValueTask(_store.GetAsync(id, CacheRunMode.Sync)); | |||||
| if(user != null) | |||||
| TrackGlobalUser(id, user); | |||||
| if (model != null) | |||||
| { | |||||
| entity = _entityBuilder(model); | |||||
| _references.TryAdd(id, new CacheReference<TEntity>(entity)); | |||||
| return entity; | |||||
| } | |||||
| return user; | |||||
| return null; | |||||
| } | } | ||||
| internal SocketGlobalUser GetOrAddUser(ulong id, Func<ulong, SocketGlobalUser> userFactory) | |||||
| public async ValueTask<ISharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null) | |||||
| { | { | ||||
| if (_userReferences.TryGetValue(id, out var userRef) && userRef.TryGetTarget(out var user)) | |||||
| return user; | |||||
| if (TryGetReference(id, out var entity)) | |||||
| { | |||||
| return entity; | |||||
| } | |||||
| var model = await _store.GetAsync(id, CacheRunMode.Async).ConfigureAwait(false); | |||||
| user = GetUser(id); | |||||
| if (model != null) | |||||
| { | |||||
| entity = _entityBuilder(model); | |||||
| _references.TryAdd(id, new CacheReference<TEntity>(entity)); | |||||
| return entity; | |||||
| } | |||||
| if (user == null) | |||||
| if(mode == CacheMode.AllowDownload) | |||||
| { | { | ||||
| user ??= userFactory(id); | |||||
| _state.AddOrUpdateUserAsync(user); | |||||
| TrackGlobalUser(id, user); | |||||
| return await _restLookup(id, options).ConfigureAwait(false); | |||||
| } | } | ||||
| return user; | |||||
| return null; | |||||
| } | } | ||||
| internal void RemoveUser(ulong id) | |||||
| public IEnumerable<TEntity> GetAll() | |||||
| { | { | ||||
| _state.RemoveUserAsync(id); | |||||
| var models = RunOrThrowValueTask(_store.GetAllAsync(CacheRunMode.Sync).ToArrayAsync()); | |||||
| return models.Select(x => | |||||
| { | |||||
| var entity = _entityBuilder(x); | |||||
| _references.TryAdd(x.Id, new CacheReference<TEntity>(entity)); | |||||
| return entity; | |||||
| }); | |||||
| } | } | ||||
| #endregion | |||||
| #region GuildUsers | |||||
| private void TrackMember(ulong userId, ulong guildId, SocketGuildUser user) | |||||
| public async IAsyncEnumerable<TEntity> GetAllAsync() | |||||
| { | { | ||||
| if(user != null) | |||||
| await foreach(var model in _store.GetAllAsync(CacheRunMode.Async)) | |||||
| { | { | ||||
| _memberReferences.TryAdd((guildId, userId), new CacheWeakReference<SocketGuildUser>(user)); | |||||
| var entity = _entityBuilder(model); | |||||
| _references.TryAdd(model.Id, new CacheReference<TEntity>(entity)); | |||||
| yield return entity; | |||||
| } | } | ||||
| } | } | ||||
| internal void RemovedReferencedMember(ulong userId, ulong guildId) | |||||
| => _memberReferences.TryRemove((guildId, userId), out _); | |||||
| internal ValueTask<IGuildUser> GetMemberAsync(ulong userId, ulong guildId, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | |||||
| => _state.GetMemberAsync(guildId, userId, mode.ToBehavior(), options); | |||||
| 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); | |||||
| return _entityBuilder(model); | |||||
| } | |||||
| internal SocketGuildUser GetMember(ulong userId, ulong guildId) | |||||
| public void AddOrUpdate(TModel model) | |||||
| { | { | ||||
| if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) | |||||
| return member; | |||||
| member = (SocketGuildUser)_state.GetMemberAsync(guildId, userId, StateBehavior.SyncOnly).Result; | |||||
| if(member != null) | |||||
| TrackMember(userId, guildId, member); | |||||
| return member; | |||||
| RunOrThrowValueTask(_store.AddOrUpdateAsync(model, CacheRunMode.Sync)); | |||||
| if (TryGetReference(model.Id, out var reference)) | |||||
| reference.Update(model); | |||||
| } | } | ||||
| internal SocketGuildUser GetOrAddMember(ulong userId, ulong guildId, Func<ulong, ulong, SocketGuildUser> memberFactory) | |||||
| public ValueTask AddOrUpdateAsync(TModel model) | |||||
| { | { | ||||
| if (_memberReferences.TryGetValue((guildId, userId), out var memberRef) && memberRef.TryGetTarget(out var member)) | |||||
| return member; | |||||
| if (TryGetReference(model.Id, out var reference)) | |||||
| reference.Update(model); | |||||
| return _store.AddOrUpdateAsync(model, CacheRunMode.Async); | |||||
| } | |||||
| member = GetMember(userId, guildId); | |||||
| public void Remove(TId id) | |||||
| { | |||||
| RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); | |||||
| _references.TryRemove(id, out _); | |||||
| } | |||||
| if (member == null) | |||||
| { | |||||
| member ??= memberFactory(userId, guildId); | |||||
| TrackMember(userId, guildId, member); | |||||
| Task.Run(async () => await _state.AddOrUpdateMemberAsync(guildId, member)); // can run async, think of this as fire and forget. | |||||
| } | |||||
| public ValueTask RemoveAsync(TId id) | |||||
| { | |||||
| _references.TryRemove(id, out _); | |||||
| return _store.RemoveAsync(id, CacheRunMode.Async); | |||||
| } | |||||
| return member; | |||||
| public void Purge() | |||||
| { | |||||
| RunOrThrowValueTask(_store.PurgeAllAsync(CacheRunMode.Sync)); | |||||
| _references.Clear(); | |||||
| } | } | ||||
| internal IEnumerable<IGuildUser> GetMembers(ulong guildId) | |||||
| => _state.GetMembersAsync(guildId, StateBehavior.SyncOnly).Result; | |||||
| public ValueTask PurgeAsync() | |||||
| { | |||||
| _references.Clear(); | |||||
| return _store.PurgeAllAsync(CacheRunMode.Async); | |||||
| } | |||||
| } | |||||
| internal void AddOrUpdateMember(ulong guildId, SocketGuildUser user) | |||||
| => EnsureSync(_state.AddOrUpdateMemberAsync(guildId, user)); | |||||
| internal partial class ClientStateManager | |||||
| { | |||||
| public ReferenceStore<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; | |||||
| internal void RemoveMember(ulong userId, ulong guildId) | |||||
| => EnsureSync(_state.RemoveMemberAsync(guildId, userId)); | |||||
| private SemaphoreSlim _memberStoreLock; | |||||
| private SemaphoreSlim _threadMemberLock; | |||||
| #endregion | |||||
| private void CreateStores() | |||||
| { | |||||
| UserStore = new ReferenceStore<SocketGlobalUser, IUserModel, ulong, IUser>( | |||||
| _cacheProvider, | |||||
| m => SocketGlobalUser.Create(_client, m), | |||||
| async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), | |||||
| AllowSyncWaits); | |||||
| PresenceStore = new ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence>( | |||||
| _cacheProvider, | |||||
| m => SocketPresence.Create(m), | |||||
| (id, options) => Task.FromResult<IPresence>(null), | |||||
| AllowSyncWaits); | |||||
| _memberStores = new(); | |||||
| _threadMemberStores = new(); | |||||
| _threadMemberLock = new(1, 1); | |||||
| _memberStoreLock = new(1,1); | |||||
| } | |||||
| #region Presence | |||||
| internal void AddOrUpdatePresence(SocketPresence presence) | |||||
| public void ClearDeadReferences() | |||||
| { | { | ||||
| EnsureSync(_state.AddOrUpdatePresenseAsync(presence.UserId, presence, StateBehavior.SyncOnly)); | |||||
| UserStore.ClearDeadReferences(); | |||||
| PresenceStore.ClearDeadReferences(); | |||||
| } | } | ||||
| internal SocketPresence GetPresence(ulong userId) | |||||
| public async ValueTask InitializeAsync() | |||||
| { | { | ||||
| if (_state.GetPresenceAsync(userId, StateBehavior.SyncOnly).Result is not SocketPresence socketPresence) | |||||
| throw new NotSupportedException("Cannot use non-socket entity for presence"); | |||||
| await UserStore.InitializeAsync(); | |||||
| await PresenceStore.InitializeAsync(); | |||||
| } | |||||
| public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store) | |||||
| => _memberStores.TryGetValue(guildId, out store); | |||||
| return socketPresence; | |||||
| 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), | |||||
| AllowSyncWaits); | |||||
| 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), | |||||
| AllowSyncWaits); | |||||
| await store.InitializeAsync().ConfigureAwait(false); | |||||
| _threadMemberStores.TryAdd(threadId, store); | |||||
| return store; | |||||
| } | |||||
| finally | |||||
| { | |||||
| _threadMemberLock.Release(); | |||||
| } | |||||
| } | } | ||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -30,11 +30,17 @@ namespace Discord.WebSocket | |||||
| _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | ||||
| .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | ||||
| private readonly IStateProvider _state; | |||||
| internal bool AllowSyncWaits | |||||
| => _client.AllowSynchronousWaiting; | |||||
| public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCount) | |||||
| private readonly ICacheProvider _cacheProvider; | |||||
| private readonly DiscordSocketClient _client; | |||||
| public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) | |||||
| { | { | ||||
| _state = state; | |||||
| _client = client; | |||||
| _cacheProvider = client.CacheProvider; | |||||
| double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | ||||
| double estimatedUsersCount = guildCount * AverageUsersPerGuild; | double estimatedUsersCount = guildCount * AverageUsersPerGuild; | ||||
| _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | ||||
| @@ -43,6 +49,8 @@ namespace Discord.WebSocket | |||||
| _users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | _users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | ||||
| _groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); | _groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); | ||||
| _commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); | _commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); | ||||
| CreateStores(); | |||||
| } | } | ||||
| internal SocketChannel GetChannel(ulong id) | internal SocketChannel GetChannel(ulong id) | ||||
| @@ -70,16 +70,17 @@ namespace Discord.WebSocket | |||||
| internal int TotalShards { get; private set; } | internal int TotalShards { get; private set; } | ||||
| internal int MessageCacheSize { get; private set; } | internal int MessageCacheSize { get; private set; } | ||||
| internal int LargeThreshold { get; private set; } | internal int LargeThreshold { get; private set; } | ||||
| internal ICacheProvider CacheProvider { get; private set; } | |||||
| internal ClientStateManager StateManager { get; private set; } | internal ClientStateManager StateManager { get; private set; } | ||||
| internal UdpSocketProvider UdpSocketProvider { get; private set; } | internal UdpSocketProvider UdpSocketProvider { get; private set; } | ||||
| internal WebSocketProvider WebSocketProvider { get; private set; } | internal WebSocketProvider WebSocketProvider { get; private set; } | ||||
| internal IStateProvider StateProvider { get; private set; } | |||||
| internal bool AlwaysDownloadUsers { get; private set; } | internal bool AlwaysDownloadUsers { get; private set; } | ||||
| internal int? HandlerTimeout { get; private set; } | internal int? HandlerTimeout { get; private set; } | ||||
| internal bool AlwaysDownloadDefaultStickers { get; private set; } | internal bool AlwaysDownloadDefaultStickers { get; private set; } | ||||
| internal bool AlwaysResolveStickers { get; private set; } | internal bool AlwaysResolveStickers { get; private set; } | ||||
| internal bool LogGatewayIntentWarnings { get; private set; } | internal bool LogGatewayIntentWarnings { get; private set; } | ||||
| internal bool SuppressUnknownDispatchWarnings { get; private set; } | internal bool SuppressUnknownDispatchWarnings { get; private set; } | ||||
| internal bool AllowSynchronousWaiting { get; private set; } | |||||
| internal new DiscordSocketApiClient ApiClient => base.ApiClient; | internal new DiscordSocketApiClient ApiClient => base.ApiClient; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override IReadOnlyCollection<SocketGuild> Guilds => StateManager.Guilds; | public override IReadOnlyCollection<SocketGuild> Guilds => StateManager.Guilds; | ||||
| @@ -155,6 +156,8 @@ namespace Discord.WebSocket | |||||
| LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; | LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; | ||||
| SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; | SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; | ||||
| HandlerTimeout = config.HandlerTimeout; | HandlerTimeout = config.HandlerTimeout; | ||||
| CacheProvider = config.CacheProvider ?? new DefaultConcurrentCacheProvider(); | |||||
| AllowSynchronousWaiting = config.AllowSynchronousWaiting; | |||||
| Rest = new DiscordSocketRestClient(config, ApiClient); | Rest = new DiscordSocketRestClient(config, ApiClient); | ||||
| _heartbeatTimes = new ConcurrentQueue<long>(); | _heartbeatTimes = new ConcurrentQueue<long>(); | ||||
| _gatewayIntents = config.GatewayIntents; | _gatewayIntents = config.GatewayIntents; | ||||
| @@ -166,7 +169,6 @@ namespace Discord.WebSocket | |||||
| OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); | OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); | ||||
| _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); | _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); | ||||
| _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); | _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); | ||||
| StateProvider = config.StateProvider ?? new DefaultStateProvider(_gatewayLogger, config.CacheProvider ?? new DefaultConcurrentCacheProvider(5, 50), this, config.DefaultStateBehavior); | |||||
| _nextAudioId = 1; | _nextAudioId = 1; | ||||
| _shardedClient = shardedClient; | _shardedClient = shardedClient; | ||||
| @@ -206,10 +208,14 @@ namespace Discord.WebSocket | |||||
| #region State | #region State | ||||
| public ValueTask<IUser> GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) | public ValueTask<IUser> GetUserAsync(ulong id, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) | ||||
| => StateManager.GetUserAsync(id, cacheMode, options); | |||||
| => StateManager.UserStore.GetAsync(id, cacheMode, options); | |||||
| public ValueTask<IGuildUser> GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) | public ValueTask<IGuildUser> GetGuildUserAsync(ulong userId, ulong guildId, CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null) | ||||
| => StateManager.GetMemberAsync(userId, guildId, cacheMode, options); | |||||
| { | |||||
| if (StateManager.TryGetMemberStore(guildId, out var store)) | |||||
| return store.GetAsync(userId, cacheMode, options); | |||||
| return ValueTask.FromResult<IGuildUser>(null); | |||||
| } | |||||
| #endregion | #endregion | ||||
| @@ -409,7 +415,7 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override SocketUser GetUser(ulong id) | public override SocketUser GetUser(ulong id) | ||||
| => StateManager.GetUser(id); | |||||
| => StateManager.UserStore.Get(id); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override SocketUser GetUser(string username, string discriminator) | public override SocketUser GetUser(string username, string discriminator) | ||||
| => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); | => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); | ||||
| @@ -496,23 +502,18 @@ namespace Discord.WebSocket | |||||
| public void PurgeUserCache() => StateManager.PurgeUsers(); | public void PurgeUserCache() => StateManager.PurgeUsers(); | ||||
| internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model) | internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model) | ||||
| { | { | ||||
| return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model)); | |||||
| return state.UserStore.GetOrAdd(model.Id, x => model); | |||||
| } | } | ||||
| internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model) | internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model) | ||||
| { | { | ||||
| return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model); | |||||
| return state.UserStore.Get(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, model); | |||||
| } | } | ||||
| internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model) | internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model) | ||||
| { | { | ||||
| return state.GetOrAddUser(model.Id, x => | |||||
| { | |||||
| var user = SocketGlobalUser.Create(this, state, model); | |||||
| user.GlobalUser.AddRef(); | |||||
| return user; | |||||
| }); | |||||
| return state.UserStore.GetOrAdd(model.Id, x => model); | |||||
| } | } | ||||
| internal void RemoveUser(ulong id) | internal void RemoveUser(ulong id) | ||||
| => StateManager.RemoveUser(id); | |||||
| => StateManager.UserStore.Remove(id); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override async Task<SocketSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | public override async Task<SocketSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | ||||
| @@ -689,7 +690,7 @@ namespace Discord.WebSocket | |||||
| if (CurrentUser == null) | if (CurrentUser == null) | ||||
| return; | return; | ||||
| var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | ||||
| StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); | |||||
| await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); | |||||
| var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); | var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); | ||||
| @@ -813,6 +814,7 @@ namespace Discord.WebSocket | |||||
| int latency = (int)(Environment.TickCount - time); | int latency = (int)(Environment.TickCount - time); | ||||
| int before = Latency; | int before = Latency; | ||||
| Latency = latency; | Latency = latency; | ||||
| StateManager?.ClearDeadReferences(); | |||||
| await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); | await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -859,21 +861,26 @@ namespace Discord.WebSocket | |||||
| await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | ||||
| var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | ||||
| var state = new ClientStateManager(StateProvider, data.Guilds.Length, data.PrivateChannels.Length); | |||||
| var state = new ClientStateManager(this, data.Guilds.Length, data.PrivateChannels.Length); | |||||
| StateManager = state; | StateManager = state; | ||||
| await StateManager.InitializeAsync().ConfigureAwait(false); | |||||
| var currentUser = SocketSelfUser.Create(this, state, data.User); | |||||
| var currentUser = SocketSelfUser.Create(this, data.User); | |||||
| Rest.CreateRestSelfUser(data.User); | Rest.CreateRestSelfUser(data.User); | ||||
| var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | ||||
| StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); | |||||
| await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); | |||||
| ApiClient.CurrentUserId = currentUser.Id; | ApiClient.CurrentUserId = currentUser.Id; | ||||
| ApiClient.CurrentApplicationId = data.Application.Id; | ApiClient.CurrentApplicationId = data.Application.Id; | ||||
| Rest.CurrentUser = RestSelfUser.Create(this, data.User); | Rest.CurrentUser = RestSelfUser.Create(this, data.User); | ||||
| int unavailableGuilds = 0; | int unavailableGuilds = 0; | ||||
| for (int i = 0; i < data.Guilds.Length; i++) | for (int i = 0; i < data.Guilds.Length; i++) | ||||
| { | { | ||||
| var model = data.Guilds[i]; | var model = data.Guilds[i]; | ||||
| var guild = AddGuild(model, state); | |||||
| var guild = await AddGuildAsync(model).ConfigureAwait(false); | |||||
| if (!guild.IsAvailable) | if (!guild.IsAvailable) | ||||
| unavailableGuilds++; | unavailableGuilds++; | ||||
| else | else | ||||
| @@ -950,6 +957,7 @@ namespace Discord.WebSocket | |||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| guild.Update(StateManager, data); | guild.Update(StateManager, data); | ||||
| await guild.UpdateCacheAsync(data).ConfigureAwait(false); | |||||
| if (_unavailableGuildCount != 0) | if (_unavailableGuildCount != 0) | ||||
| _unavailableGuildCount--; | _unavailableGuildCount--; | ||||
| @@ -971,7 +979,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); | ||||
| var guild = AddGuild(data, StateManager); | |||||
| var guild = await AddGuildAsync(data).ConfigureAwait(false); | |||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); | await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); | ||||
| @@ -1290,13 +1298,13 @@ namespace Discord.WebSocket | |||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| var before = user.Clone(); | var before = user.Clone(); | ||||
| if (user.GlobalUser.Update(StateManager, data.User)) | |||||
| if (user.GlobalUser.Value.Update(data.User)) // TODO: update cache only and have lazy like support for events. | |||||
| { | { | ||||
| //Global data was updated, trigger UserUpdated | //Global data was updated, trigger UserUpdated | ||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); | |||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser.Value, user).ConfigureAwait(false); | |||||
| } | } | ||||
| user.Update(StateManager, data); | |||||
| user.Update(data); | |||||
| var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => null); | var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => null); | ||||
| await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); | await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); | ||||
| @@ -1332,12 +1340,12 @@ namespace Discord.WebSocket | |||||
| return; | return; | ||||
| } | } | ||||
| user ??= StateManager.GetUser(data.User.Id); | |||||
| user ??= (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); | |||||
| if (user != null) | if (user != null) | ||||
| user.Update(StateManager, data.User); | |||||
| user.Update(data.User); | |||||
| else | else | ||||
| user = StateManager.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, StateManager, data.User)); | |||||
| user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User); | |||||
| await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); | await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -1957,8 +1965,8 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| var globalBefore = user.GlobalUser.Clone(); | |||||
| if (user.GlobalUser.Update(StateManager, data.User)) | |||||
| var globalBefore = user.GlobalUser.Value.Clone(); | |||||
| if (user.GlobalUser.Value.Update(StateManager, data.User)) | |||||
| { | { | ||||
| //Global data was updated, trigger UserUpdated | //Global data was updated, trigger UserUpdated | ||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | ||||
| @@ -1978,7 +1986,7 @@ namespace Discord.WebSocket | |||||
| var before = user.Presence?.Value?.Clone(); | var before = user.Presence?.Value?.Clone(); | ||||
| user.Update(StateManager, data.User); | user.Update(StateManager, data.User); | ||||
| var after = SocketPresence.Create(data); | var after = SocketPresence.Create(data); | ||||
| StateManager.AddOrUpdatePresence(after); | |||||
| StateManager.AddOrUpdatePresence(data); | |||||
| await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); | await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); | ||||
| } | } | ||||
| break; | break; | ||||
| @@ -2324,7 +2332,7 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| SocketUser user = data.User.IsSpecified | SocketUser user = data.User.IsSpecified | ||||
| ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, StateManager, data.User.Value)) | |||||
| ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value) | |||||
| : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. | : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. | ||||
| SocketChannel channel = null; | SocketChannel channel = null; | ||||
| @@ -2579,9 +2587,9 @@ namespace Discord.WebSocket | |||||
| entity.Update(StateManager, thread); | entity.Update(StateManager, thread); | ||||
| } | } | ||||
| foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id)) | |||||
| foreach(var member in data.Members.Where(x => x.ThreadId.Value == entity.Id)) | |||||
| { | { | ||||
| var guildMember = guild.GetUser(member.Id.Value); | |||||
| var guildMember = guild.GetUser(member.ThreadId.Value); | |||||
| entity.AddOrUpdateThreadMember(member, guildMember); | entity.AddOrUpdateThreadMember(member, guildMember); | ||||
| } | } | ||||
| @@ -2594,11 +2602,11 @@ namespace Discord.WebSocket | |||||
| var data = (payload as JToken).ToObject<ThreadMember>(_serializer); | var data = (payload as JToken).ToObject<ThreadMember>(_serializer); | ||||
| var thread = (SocketThreadChannel)StateManager.GetChannel(data.Id.Value); | |||||
| var thread = (SocketThreadChannel)StateManager.GetChannel(data.ThreadId.Value); | |||||
| if (thread == null) | if (thread == null) | ||||
| { | { | ||||
| await UnknownChannelAsync(type, data.Id.Value); | |||||
| await UnknownChannelAsync(type, data.ThreadId.Value); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -2948,10 +2956,11 @@ namespace Discord.WebSocket | |||||
| await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); | await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); | ||||
| } | } | ||||
| internal SocketGuild AddGuild(ExtendedGuild model, ClientStateManager state) | |||||
| internal async Task<SocketGuild> AddGuildAsync(ExtendedGuild model) | |||||
| { | { | ||||
| var guild = SocketGuild.Create(this, state, model); | |||||
| state.AddGuild(guild); | |||||
| await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false); | |||||
| var guild = SocketGuild.Create(this, StateManager, model); | |||||
| StateManager.AddGuild(guild); | |||||
| if (model.Large) | if (model.Large) | ||||
| _largeGuilds.Enqueue(model.Id); | _largeGuilds.Enqueue(model.Id); | ||||
| return guild; | return guild; | ||||
| @@ -2977,19 +2986,12 @@ namespace Discord.WebSocket | |||||
| internal ISocketPrivateChannel RemovePrivateChannel(ulong id) | internal ISocketPrivateChannel RemovePrivateChannel(ulong id) | ||||
| { | { | ||||
| var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; | var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; | ||||
| if (channel != null) | |||||
| { | |||||
| foreach (var recipient in channel.Recipients) | |||||
| recipient.GlobalUser.RemoveRef(this); | |||||
| } | |||||
| return channel; | return channel; | ||||
| } | } | ||||
| internal void RemoveDMChannels() | internal void RemoveDMChannels() | ||||
| { | { | ||||
| var channels = StateManager.DMChannels; | var channels = StateManager.DMChannels; | ||||
| StateManager.PurgeDMChannels(); | StateManager.PurgeDMChannels(); | ||||
| foreach (var channel in channels) | |||||
| channel.Recipient.GlobalUser.RemoveRef(this); | |||||
| } | } | ||||
| internal void EnsureGatewayIntent(GatewayIntents intents) | internal void EnsureGatewayIntent(GatewayIntents intents) | ||||
| @@ -29,7 +29,12 @@ namespace Discord.WebSocket | |||||
| /// Gets or sets the cache provider to use | /// Gets or sets the cache provider to use | ||||
| /// </summary> | /// </summary> | ||||
| public ICacheProvider CacheProvider { get; set; } | public ICacheProvider CacheProvider { get; set; } | ||||
| public IStateProvider StateProvider { 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> | /// <summary> | ||||
| /// Returns the encoding gateway should use. | /// Returns the encoding gateway should use. | ||||
| @@ -199,11 +204,6 @@ namespace Discord.WebSocket | |||||
| /// </summary> | /// </summary> | ||||
| public bool SuppressUnknownDispatchWarnings { get; set; } = true; | public bool SuppressUnknownDispatchWarnings { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Gets or sets the default state behavior clients will use. | |||||
| /// </summary> | |||||
| public StateBehavior DefaultStateBehavior { get; set; } = StateBehavior.Default; | |||||
| /// <summary> | /// <summary> | ||||
| /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -452,17 +452,8 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| _events = events; | _events = events; | ||||
| for (int i = 0; i < model.Members.Length; i++) | |||||
| { | |||||
| Discord.StateManager.AddOrUpdateMember(Id, SocketGuildUser.Create(Id, Discord, model.Members[i])); | |||||
| } | |||||
| DownloadedMemberCount = model.Members.Length; | DownloadedMemberCount = model.Members.Length; | ||||
| for (int i = 0; i < model.Presences.Length; i++) | |||||
| { | |||||
| Discord.StateManager.AddOrUpdatePresence(SocketPresence.Create(model.Presences[i])); | |||||
| } | |||||
| MemberCount = model.MemberCount; | MemberCount = model.MemberCount; | ||||
| @@ -553,29 +544,12 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | ||||
| } | } | ||||
| /*internal void Update(ClientStateManager state, GuildSyncModel model) //TODO remove? userbot related | |||||
| { | |||||
| var members = new ConcurrentDictionary<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); | |||||
| { | |||||
| for (int i = 0; i < model.Members.Length; i++) | |||||
| { | |||||
| var member = SocketGuildUser.Create(this, state, model.Members[i]); | |||||
| members.TryAdd(member.Id, member); | |||||
| } | |||||
| DownloadedMemberCount = members.Count; | |||||
| for (int i = 0; i < model.Presences.Length; i++) | |||||
| { | |||||
| if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | |||||
| member.Update(state, model.Presences[i], true); | |||||
| } | |||||
| } | |||||
| _members = members; | |||||
| var _ = _syncPromise.TrySetResultAsync(true); | |||||
| //if (!model.Large) | |||||
| // _ = _downloaderPromise.TrySetResultAsync(true); | |||||
| }*/ | |||||
| internal async ValueTask UpdateCacheAsync(ExtendedModel model) | |||||
| { | |||||
| await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false); | |||||
| await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false); | |||||
| } | |||||
| internal void Update(ClientStateManager state, EmojiUpdateModel model) | internal void Update(ClientStateManager state, EmojiUpdateModel model) | ||||
| { | { | ||||
| @@ -12,45 +12,27 @@ namespace Discord.WebSocket | |||||
| public override string Username { get; internal set; } | public override string Username { get; internal set; } | ||||
| public override ushort DiscriminatorValue { get; internal set; } | public override ushort DiscriminatorValue { get; internal set; } | ||||
| public override string AvatarId { get; internal set; } | public override string AvatarId { get; internal set; } | ||||
| public override bool IsWebhook => false; | 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) | private SocketGlobalUser(DiscordSocketClient discord, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| } | } | ||||
| internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model) | |||||
| { | { | ||||
| var entity = new SocketGlobalUser(discord, model.Id); | var entity = new SocketGlobalUser(discord, model.Id); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void AddRef() | |||||
| { | |||||
| checked | |||||
| { | |||||
| lock (_lockObj) | |||||
| _references++; | |||||
| } | |||||
| } | |||||
| internal void RemoveRef(DiscordSocketClient discord) | |||||
| ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| public override void Dispose() | |||||
| { | { | ||||
| lock (_lockObj) | |||||
| { | |||||
| if (--_references <= 0) | |||||
| discord.RemoveUser(Id); | |||||
| } | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| } | } | ||||
| ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | ||||
| internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | ||||
| } | } | ||||
| @@ -18,38 +18,33 @@ namespace Discord.WebSocket | |||||
| /// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. | /// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. | ||||
| /// </returns> | /// </returns> | ||||
| public SocketGroupChannel Channel { get; } | 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 Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | 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; | Channel = channel; | ||||
| GlobalUser = globalUser; | |||||
| } | } | ||||
| internal static SocketGroupUser Create(SocketGroupChannel channel, ClientStateManager state, Model model) | |||||
| internal static SocketGroupUser Create(SocketGroupChannel channel, Model model) | |||||
| { | { | ||||
| var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); | |||||
| entity.Update(state, model); | |||||
| var entity = new SocketGroupUser(channel, model.Id); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; | ||||
| internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | ||||
| public override void Dispose() | |||||
| { | |||||
| GC.SuppressFinalize(this); | |||||
| if (GlobalUser.IsValueCreated) | |||||
| GlobalUser.Value.Dispose(); | |||||
| } | |||||
| ~SocketGroupUser() => Dispose(); | |||||
| #endregion | #endregion | ||||
| #region IVoiceState | #region IVoiceState | ||||
| @@ -25,7 +25,6 @@ namespace Discord.WebSocket | |||||
| private ImmutableArray<ulong> _roleIds; | private ImmutableArray<ulong> _roleIds; | ||||
| private ulong _guildId; | private ulong _guildId; | ||||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild the user is in. | /// Gets the guild the user is in. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -43,13 +42,13 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string GuildAvatarId { get; private set; } | public string GuildAvatarId { get; private set; } | ||||
| /// <inheritdoc /> | /// <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 /> | /// <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 /> | /// <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 /> | /// <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 /> | /// <inheritdoc /> | ||||
| public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this)); | public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this)); | ||||
| @@ -137,32 +136,29 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| internal SocketGuildUser(ulong guildId, SocketGlobalUser globalUser, DiscordSocketClient client) | |||||
| : base(client, globalUser.Id) | |||||
| internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client) | |||||
| : base(client, userId) | |||||
| { | { | ||||
| _guildId = guildId; | _guildId = guildId; | ||||
| Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); | Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); | ||||
| GlobalUser = globalUser; | |||||
| } | } | ||||
| internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) | internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) | ||||
| { | { | ||||
| var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, (Discord.API.User)model), client); | |||||
| if (entity.Update(client.StateManager, model)) | |||||
| client.StateManager.AddOrUpdateMember(guildId, entity); | |||||
| var entity = new SocketGuildUser(guildId, model.Id, client); | |||||
| if (entity.Update(model)) | |||||
| client.StateManager.AddOrUpdateMember(guildId, entity.ToModel()); | |||||
| entity.UpdateRoles(Array.Empty<ulong>()); | entity.UpdateRoles(Array.Empty<ulong>()); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) | internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) | ||||
| { | { | ||||
| var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, model.User), client); | |||||
| entity.Update(client.StateManager, model); | |||||
| client.StateManager.AddOrUpdateMember(guildId, entity); | |||||
| var entity = new SocketGuildUser(guildId, model.Id, client); | |||||
| entity.Update(model); | |||||
| client.StateManager.AddOrUpdateMember(guildId, model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientStateManager state, MemberModel model) | |||||
| internal void Update(MemberModel model) | |||||
| { | { | ||||
| base.Update(state, model.User); | |||||
| _joinedAtTicks = model.JoinedAt.UtcTicks; | _joinedAtTicks = model.JoinedAt.UtcTicks; | ||||
| Nickname = model.Nickname; | Nickname = model.Nickname; | ||||
| GuildAvatarId = model.GuildAvatar; | GuildAvatarId = model.GuildAvatar; | ||||
| @@ -234,12 +230,7 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | ||||
| internal new SocketGuildUser Clone() | |||||
| { | |||||
| var clone = MemberwiseClone() as SocketGuildUser; | |||||
| clone.GlobalUser = GlobalUser.Clone(); | |||||
| return clone; | |||||
| } | |||||
| internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; | |||||
| #endregion | #endregion | ||||
| @@ -260,8 +251,7 @@ namespace Discord.WebSocket | |||||
| private struct CacheModel : MemberModel | private struct CacheModel : MemberModel | ||||
| { | { | ||||
| public UserModel User { get; set; } | |||||
| public ulong Id { get; set; } | |||||
| public string Nickname { get; set; } | public string Nickname { get; set; } | ||||
| public string GuildAvatar { get; set; } | public string GuildAvatar { get; set; } | ||||
| @@ -280,15 +270,14 @@ namespace Discord.WebSocket | |||||
| public DateTimeOffset? CommunicationsDisabledUntil { get; set; } | public DateTimeOffset? CommunicationsDisabledUntil { get; set; } | ||||
| } | } | ||||
| internal new MemberModel ToModel() | |||||
| => ToModel<CacheModel>(); | |||||
| MemberModel ICached<MemberModel>.ToModel() | |||||
| => ToMemberModel(); | |||||
| internal MemberModel ToMemberModel() | |||||
| internal new TModel ToModel<TModel>() where TModel : MemberModel, new() | |||||
| { | { | ||||
| return new CacheModel | |||||
| return new TModel | |||||
| { | { | ||||
| User = ((ICached<UserModel>)this).ToModel(), | |||||
| Id = Id, | |||||
| CommunicationsDisabledUntil = TimedOutUntil, | CommunicationsDisabledUntil = TimedOutUntil, | ||||
| GuildAvatar = GuildAvatarId, | GuildAvatar = GuildAvatarId, | ||||
| IsDeaf = IsDeafened, | IsDeaf = IsDeafened, | ||||
| @@ -301,7 +290,19 @@ namespace Discord.WebSocket | |||||
| }; | }; | ||||
| } | } | ||||
| public void Dispose() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); | |||||
| MemberModel ICached<MemberModel>.ToModel() | |||||
| => ToModel(); | |||||
| TResult ICached<MemberModel>.ToModel<TResult>() | |||||
| => ToModel<TResult>(); | |||||
| void ICached<MemberModel>.Update(MemberModel model) => Update(model); | |||||
| public override void Dispose() | |||||
| { | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.RemovedReferencedMember(Id, _guildId); | |||||
| } | |||||
| ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); | ~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId); | ||||
| #endregion | #endregion | ||||
| @@ -114,6 +114,12 @@ namespace Discord.WebSocket | |||||
| public ulong UserId { get; set; } | public ulong UserId { get; set; } | ||||
| public ulong? GuildId { get; set; } | public ulong? GuildId { get; set; } | ||||
| ulong IEntityModel<ulong>.Id | |||||
| { | |||||
| get => UserId; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| } | } | ||||
| private struct ActivityCacheModel : IActivityModel | private struct ActivityCacheModel : IActivityModel | ||||
| @@ -156,8 +162,11 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| internal Model ToModel() | internal Model ToModel() | ||||
| => ToModel<CacheModel>(); | |||||
| internal TModel ToModel<TModel>() where TModel : Model, new() | |||||
| { | { | ||||
| return new CacheModel | |||||
| return new TModel | |||||
| { | { | ||||
| Status = Status, | Status = Status, | ||||
| ActiveClients = ActiveClients.ToArray(), | ActiveClients = ActiveClients.ToArray(), | ||||
| @@ -194,6 +203,8 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| Model ICached<Model>.ToModel() => ToModel(); | Model ICached<Model>.ToModel() => ToModel(); | ||||
| TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| #endregion | #endregion | ||||
| } | } | ||||
| @@ -19,18 +19,17 @@ namespace Discord.WebSocket | |||||
| public bool IsVerified { get; private set; } | public bool IsVerified { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public bool IsMfaEnabled { get; private set; } | public bool IsMfaEnabled { get; private set; } | ||||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||||
| /// <inheritdoc /> | /// <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 /> | /// <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 /> | /// <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 /> | /// <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 /> | /// <inheritdoc /> | ||||
| internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Value.Presence; } set { GlobalUser.Value.Presence = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserProperties Flags { get; internal set; } | public UserProperties Flags { get; internal set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -41,20 +40,20 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) | |||||
| : base(discord, globalUser.Id) | |||||
| internal SocketSelfUser(DiscordSocketClient discord, ulong userId) | |||||
| : base(discord, userId) | |||||
| { | { | ||||
| GlobalUser = globalUser; | |||||
| } | } | ||||
| internal static SocketSelfUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| internal static SocketSelfUser Create(DiscordSocketClient discord, Model model) | |||||
| { | { | ||||
| var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); | |||||
| entity.Update(state, model); | |||||
| var entity = new SocketSelfUser(discord, model.Id); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override bool Update(ClientStateManager state, UserModel model) | |||||
| internal override bool Update(UserModel model) | |||||
| { | { | ||||
| bool hasGlobalChanges = base.Update(state, model); | |||||
| bool hasGlobalChanges = base.Update(model); | |||||
| if (model is not Model currentUserModel) | if (model is not Model currentUserModel) | ||||
| throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); | throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); | ||||
| @@ -98,9 +97,13 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | ||||
| internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | ||||
| public override void Dispose() | |||||
| { | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||||
| } | |||||
| #region Cache | #region Cache | ||||
| private struct CacheModel : Model | private struct CacheModel : Model | ||||
| { | { | ||||
| public bool? IsVerified { get; set; } | public bool? IsVerified { get; set; } | ||||
| @@ -128,9 +131,12 @@ namespace Discord.WebSocket | |||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| } | } | ||||
| Model ICached<Model>.ToModel() | |||||
| internal new Model ToModel() | |||||
| => ToModel<CacheModel>(); | |||||
| internal new TModel ToModel<TModel>() where TModel : Model, new() | |||||
| { | { | ||||
| return new CacheModel | |||||
| return new TModel | |||||
| { | { | ||||
| Avatar = AvatarId, | Avatar = AvatarId, | ||||
| Discriminator = Discriminator, | Discriminator = Discriminator, | ||||
| @@ -147,6 +153,9 @@ namespace Discord.WebSocket | |||||
| }; | }; | ||||
| } | } | ||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| #endregion | #endregion | ||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.ThreadMember; | |||||
| using Model = Discord.IThreadMemberModel; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| @@ -10,12 +10,12 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents a thread user received over the gateway. | /// Represents a thread user received over the gateway. | ||||
| /// </summary> | /// </summary> | ||||
| public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser | |||||
| public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached<Model> | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the <see cref="SocketThreadChannel"/> this user is in. | /// Gets the <see cref="SocketThreadChannel"/> this user is in. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketThreadChannel Thread { get; private set; } | |||||
| public Lazy<SocketThreadChannel> Thread { get; private set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset ThreadJoinedAt { get; private set; } | public DateTimeOffset ThreadJoinedAt { get; private set; } | ||||
| @@ -23,126 +23,142 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild this user is in. | /// Gets the guild this user is in. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketGuild Guild { get; private set; } | |||||
| public Lazy<SocketGuild> Guild { get; private set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? JoinedAt | public DateTimeOffset? JoinedAt | ||||
| => GuildUser.JoinedAt; | |||||
| => GuildUser.Value.JoinedAt; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string DisplayName | public string DisplayName | ||||
| => GuildUser.Nickname ?? GuildUser.Username; | |||||
| => GuildUser.Value.Nickname ?? GuildUser.Value.Username; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string Nickname | public string Nickname | ||||
| => GuildUser.Nickname; | |||||
| => GuildUser.Value.Nickname; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? PremiumSince | public DateTimeOffset? PremiumSince | ||||
| => GuildUser.PremiumSince; | |||||
| => GuildUser.Value.PremiumSince; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? TimedOutUntil | public DateTimeOffset? TimedOutUntil | ||||
| => GuildUser.TimedOutUntil; | |||||
| => GuildUser.Value.TimedOutUntil; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool? IsPending | public bool? IsPending | ||||
| => GuildUser.IsPending; | |||||
| => GuildUser.Value.IsPending; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public int Hierarchy | public int Hierarchy | ||||
| => GuildUser.Hierarchy; | |||||
| => GuildUser.Value.Hierarchy; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override string AvatarId | public override string AvatarId | ||||
| { | { | ||||
| get => GuildUser.AvatarId; | |||||
| internal set => GuildUser.AvatarId = value; | |||||
| get => GuildUser.Value.AvatarId; | |||||
| internal set => GuildUser.Value.AvatarId = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string DisplayAvatarId => GuildAvatarId ?? AvatarId; | public string DisplayAvatarId => GuildAvatarId ?? AvatarId; | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string GuildAvatarId | public string GuildAvatarId | ||||
| => GuildUser.GuildAvatarId; | |||||
| => GuildUser.Value.GuildAvatarId; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override ushort DiscriminatorValue | public override ushort DiscriminatorValue | ||||
| { | { | ||||
| get => GuildUser.DiscriminatorValue; | |||||
| internal set => GuildUser.DiscriminatorValue = value; | |||||
| get => GuildUser.Value.DiscriminatorValue; | |||||
| internal set => GuildUser.Value.DiscriminatorValue = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool IsBot | public override bool IsBot | ||||
| { | { | ||||
| get => GuildUser.IsBot; | |||||
| internal set => GuildUser.IsBot = value; | |||||
| get => GuildUser.Value.IsBot; | |||||
| internal set => GuildUser.Value.IsBot = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool IsWebhook | public override bool IsWebhook | ||||
| => GuildUser.IsWebhook; | |||||
| => GuildUser.Value.IsWebhook; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override string Username | public override string Username | ||||
| { | { | ||||
| get => GuildUser.Username; | |||||
| internal set => GuildUser.Username = value; | |||||
| get => GuildUser.Value.Username; | |||||
| internal set => GuildUser.Value.Username = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsDeafened | public bool IsDeafened | ||||
| => GuildUser.IsDeafened; | |||||
| => GuildUser.Value.IsDeafened; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsMuted | public bool IsMuted | ||||
| => GuildUser.IsMuted; | |||||
| => GuildUser.Value.IsMuted; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsSelfDeafened | public bool IsSelfDeafened | ||||
| => GuildUser.IsSelfDeafened; | |||||
| => GuildUser.Value.IsSelfDeafened; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsSelfMuted | public bool IsSelfMuted | ||||
| => GuildUser.IsSelfMuted; | |||||
| => GuildUser.Value.IsSelfMuted; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsSuppressed | public bool IsSuppressed | ||||
| => GuildUser.IsSuppressed; | |||||
| => GuildUser.Value.IsSuppressed; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public IVoiceChannel VoiceChannel | public IVoiceChannel VoiceChannel | ||||
| => GuildUser.VoiceChannel; | |||||
| => GuildUser.Value.VoiceChannel; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string VoiceSessionId | public string VoiceSessionId | ||||
| => GuildUser.VoiceSessionId; | |||||
| => GuildUser.Value.VoiceSessionId; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsStreaming | public bool IsStreaming | ||||
| => GuildUser.IsStreaming; | |||||
| => GuildUser.Value.IsStreaming; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsVideoing | public bool IsVideoing | ||||
| => GuildUser.IsVideoing; | |||||
| => GuildUser.Value.IsVideoing; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? RequestToSpeakTimestamp | 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) | internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) | ||||
| { | { | ||||
| var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); | |||||
| var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.UserId.Value); | |||||
| entity.Update(model); | |||||
| return entity; | |||||
| } | |||||
| internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model) | |||||
| { | |||||
| var entity = new SocketThreadUser(client, guildId, threadId, model.UserId.Value); | |||||
| entity.Update(model); | entity.Update(model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| @@ -150,89 +166,117 @@ namespace Discord.WebSocket | |||||
| internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | ||||
| { | { | ||||
| // this is used for creating the owner of the thread. | // 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; | return entity; | ||||
| } | } | ||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| ThreadJoinedAt = model.JoinTimestamp; | |||||
| ThreadJoinedAt = model.JoinedAt; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); | |||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel); | |||||
| /// <inheritdoc/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <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/> | /// <inheritdoc/> | ||||
| public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); | |||||
| public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IThreadChannel IThreadUser.Thread => Thread; | |||||
| IThreadChannel IThreadUser.Thread => Thread.Value; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IGuild IThreadUser.Guild => Guild; | |||||
| IGuild IThreadUser.Guild => Guild.Value; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IGuild IGuildUser.Guild => Guild; | |||||
| IGuild IGuildUser.Guild => Guild.Value; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| ulong IGuildUser.GuildId => Guild.Id; | |||||
| ulong IGuildUser.GuildId => Guild.Value.Id; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; | |||||
| GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions; | |||||
| /// <inheritdoc/> | /// <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 /> | /// <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 /> | /// <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 Lazy<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } | |||||
| internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } | |||||
| public override void Dispose() | |||||
| { | |||||
| GC.SuppressFinalize(this); | |||||
| } | |||||
| internal override Lazy<SocketPresence> Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild user of this thread user. | /// Gets the guild user of this thread user. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="user"></param> | /// <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 | |||||
| private class CacheModel : Model | |||||
| { | |||||
| public ulong? ThreadId { get; set; } | |||||
| public ulong? UserId { get; set; } | |||||
| public DateTimeOffset JoinedAt { get; set; } | |||||
| ulong IEntityModel<ulong>.Id { get => UserId.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| } | |||||
| internal new Model ToModel() => ToModel<CacheModel>(); | |||||
| internal new TModel ToModel<TModel>() where TModel : Model, new() | |||||
| { | |||||
| return new TModel | |||||
| { | |||||
| JoinedAt = ThreadJoinedAt, | |||||
| ThreadId = _threadId, | |||||
| UserId = Id | |||||
| }; | |||||
| } | |||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -27,21 +27,21 @@ namespace Discord.WebSocket | |||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | ||||
| /// <inheritdoc /> | |||||
| /// <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 Lazy<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } } | |||||
| internal SocketUnknownUser(DiscordSocketClient discord, ulong id) | internal SocketUnknownUser(DiscordSocketClient discord, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| } | } | ||||
| internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model) | |||||
| { | { | ||||
| var entity = new SocketUnknownUser(discord, model.Id); | var entity = new SocketUnknownUser(discord, model.Id); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| public override void Dispose() { } | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; | ||||
| internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; | internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; | ||||
| } | } | ||||
| @@ -18,18 +18,18 @@ namespace Discord.WebSocket | |||||
| public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable | public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable | ||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract bool IsBot { get; internal set; } | |||||
| public virtual bool IsBot { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract string Username { get; internal set; } | |||||
| public virtual string Username { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract ushort DiscriminatorValue { get; internal set; } | |||||
| public virtual ushort DiscriminatorValue { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract string AvatarId { get; internal set; } | |||||
| public virtual string AvatarId { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract bool IsWebhook { get; } | |||||
| public virtual bool IsWebhook { get; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserProperties? PublicFlags { get; private set; } | public UserProperties? PublicFlags { get; private set; } | ||||
| internal abstract SocketGlobalUser GlobalUser { get; set; } | |||||
| internal virtual Lazy<SocketGlobalUser> GlobalUser { get; set; } | |||||
| internal virtual Lazy<SocketPresence> Presence { get; set; } | internal virtual Lazy<SocketPresence> Presence { get; set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -57,9 +57,10 @@ namespace Discord.WebSocket | |||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| } | } | ||||
| internal virtual bool Update(ClientStateManager state, Model model) | |||||
| internal virtual bool Update(Model model) | |||||
| { | { | ||||
| Presence ??= new Lazy<SocketPresence>(() => state.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||||
| Presence ??= new Lazy<SocketPresence>(() => Discord.StateManager.GetPresence(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||||
| GlobalUser ??= new Lazy<SocketGlobalUser>(() => Discord.StateManager.GetUser(Id), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||||
| bool hasChanges = false; | bool hasChanges = false; | ||||
| if (model.Avatar != AvatarId) | if (model.Avatar != AvatarId) | ||||
| { | { | ||||
| @@ -98,6 +99,8 @@ namespace Discord.WebSocket | |||||
| return hasChanges; | return hasChanges; | ||||
| } | } | ||||
| public abstract void Dispose(); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | ||||
| => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | ||||
| @@ -117,8 +120,6 @@ namespace Discord.WebSocket | |||||
| /// The full name of the user. | /// The full name of the user. | ||||
| /// </returns> | /// </returns> | ||||
| public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | ||||
| ~SocketUser() => GlobalUser?.Dispose(); | |||||
| public void Dispose() => GlobalUser?.Dispose(); | |||||
| private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
| internal SocketUser Clone() => MemberwiseClone() as SocketUser; | internal SocketUser Clone() => MemberwiseClone() as SocketUser; | ||||
| @@ -136,12 +137,9 @@ namespace Discord.WebSocket | |||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| } | } | ||||
| Model ICached<Model>.ToModel() | |||||
| => ToModel(); | |||||
| internal Model ToModel() | |||||
| internal TModel ToModel<TModel>() where TModel : Model, new() | |||||
| { | { | ||||
| return new CacheModel | |||||
| return new TModel | |||||
| { | { | ||||
| Avatar = AvatarId, | Avatar = AvatarId, | ||||
| Discriminator = Discriminator, | Discriminator = Discriminator, | ||||
| @@ -151,6 +149,17 @@ namespace Discord.WebSocket | |||||
| }; | }; | ||||
| } | } | ||||
| internal Model ToModel() | |||||
| => ToModel<CacheModel>(); | |||||
| Model ICached<Model>.ToModel() | |||||
| => ToModel(); | |||||
| TResult ICached<Model>.ToModel<TResult>() | |||||
| => ToModel<TResult>(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| #endregion | #endregion | ||||
| } | } | ||||
| } | } | ||||
| @@ -34,7 +34,7 @@ namespace Discord.WebSocket | |||||
| public override bool IsWebhook => true; | public override bool IsWebhook => true; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | internal override Lazy<SocketPresence> Presence { get { return new Lazy<SocketPresence>(() => new SocketPresence(UserStatus.Offline, null, null)); } set { } } | ||||
| internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | |||||
| internal override Lazy<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } } | |||||
| internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | ||||
| : base(guild.Discord, id) | : base(guild.Discord, id) | ||||
| @@ -42,16 +42,17 @@ namespace Discord.WebSocket | |||||
| Guild = guild; | Guild = guild; | ||||
| WebhookId = webhookId; | WebhookId = webhookId; | ||||
| } | } | ||||
| internal static SocketWebhookUser Create(SocketGuild guild, ClientStateManager state, Model model, ulong webhookId) | |||||
| internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId) | |||||
| { | { | ||||
| var entity = new SocketWebhookUser(guild, model.Id, webhookId); | var entity = new SocketWebhookUser(guild, model.Id, webhookId); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; | ||||
| internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; | internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; | ||||
| #endregion | |||||
| public override void Dispose() { } | |||||
| #endregion | |||||
| #region IGuildUser | #region IGuildUser | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -1,21 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| internal static class StateExtensions | |||||
| { | |||||
| public static StateBehavior ToBehavior(this CacheMode mode) | |||||
| { | |||||
| return mode switch | |||||
| { | |||||
| CacheMode.AllowDownload => StateBehavior.AllowDownload, | |||||
| CacheMode.CacheOnly => StateBehavior.CacheOnly, | |||||
| _ => StateBehavior.AllowDownload | |||||
| }; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,252 +0,0 @@ | |||||
| using Discord.Logging; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| internal class DefaultStateProvider : IStateProvider | |||||
| { | |||||
| private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | |||||
| private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | |||||
| private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth | |||||
| private readonly ICacheProvider _cache; | |||||
| private readonly StateBehavior _defaultBehavior; | |||||
| private readonly DiscordSocketClient _client; | |||||
| private readonly Logger _logger; | |||||
| public DefaultStateProvider(Logger logger, ICacheProvider cacheProvider, DiscordSocketClient client, StateBehavior stateBehavior) | |||||
| { | |||||
| _cache = cacheProvider; | |||||
| _client = client; | |||||
| _logger = logger; | |||||
| if (stateBehavior == StateBehavior.Default) | |||||
| throw new ArgumentException("Cannot use \"default\" as the default state behavior"); | |||||
| _defaultBehavior = stateBehavior; | |||||
| } | |||||
| private void RunAsyncWithLogs(ValueTask task) | |||||
| { | |||||
| _ = Task.Run(async () => | |||||
| { | |||||
| try | |||||
| { | |||||
| await task.ConfigureAwait(false); | |||||
| } | |||||
| catch (Exception x) | |||||
| { | |||||
| await _logger.ErrorAsync("Cache provider failed", x).ConfigureAwait(false); | |||||
| } | |||||
| }); | |||||
| } | |||||
| private TType ValidateAsSocketEntity<TType>(ISnowflakeEntity entity) where TType : SocketEntity<ulong> | |||||
| { | |||||
| if(entity is not TType val) | |||||
| throw new NotSupportedException("Cannot cache non-socket entities"); | |||||
| return val; | |||||
| } | |||||
| private StateBehavior ResolveBehavior(StateBehavior behavior) | |||||
| => behavior == StateBehavior.Default ? _defaultBehavior : behavior; | |||||
| public ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user) | |||||
| { | |||||
| var socketGuildUser = ValidateAsSocketEntity<SocketGuildUser>(user); | |||||
| var model = socketGuildUser.ToMemberModel(); | |||||
| RunAsyncWithLogs(_cache.AddOrUpdateMemberAsync(model, guildId, CacheRunMode.Async)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask AddOrUpdateUserAsync(IUser user) | |||||
| { | |||||
| var socketUser = ValidateAsSocketEntity<SocketUser>(user); | |||||
| var model = socketUser.ToModel(); | |||||
| RunAsyncWithLogs(_cache.AddOrUpdateUserAsync(model, CacheRunMode.Async)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IGuildUser> GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null) | |||||
| { | |||||
| var behavior = ResolveBehavior(stateBehavior); | |||||
| var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; | |||||
| if(behavior != StateBehavior.DownloadOnly) | |||||
| { | |||||
| var memberLookupTask = _cache.GetMemberAsync(id, guildId, cacheMode); | |||||
| if (memberLookupTask.IsCompleted) | |||||
| { | |||||
| var model = memberLookupTask.Result; | |||||
| if(model != null) | |||||
| return new ValueTask<IGuildUser>(SocketGuildUser.Create(guildId, _client, model)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return new ValueTask<IGuildUser>(Task.Run(async () => // review: task.run here? | |||||
| { | |||||
| var result = await memberLookupTask; | |||||
| if (result != null) | |||||
| return (IGuildUser)SocketGuildUser.Create(guildId, _client, result); | |||||
| else if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) | |||||
| return await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false); | |||||
| return null; | |||||
| })); | |||||
| } | |||||
| } | |||||
| if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) | |||||
| return new ValueTask<IGuildUser>(_client.Rest.GetGuildUserAsync(guildId, id, options).ContinueWith(x => (IGuildUser)x.Result)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IEnumerable<IGuildUser>> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null) | |||||
| { | |||||
| var behavior = ResolveBehavior(stateBehavior); | |||||
| var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; | |||||
| if(behavior != StateBehavior.DownloadOnly) | |||||
| { | |||||
| var memberLookupTask = _cache.GetMembersAsync(guildId, cacheMode); | |||||
| if (memberLookupTask.IsCompleted) | |||||
| return new ValueTask<IEnumerable<IGuildUser>>(memberLookupTask.Result?.Select(x => SocketGuildUser.Create(guildId, _client, x))); | |||||
| else | |||||
| { | |||||
| return new ValueTask<IEnumerable<IGuildUser>>(Task.Run(async () => | |||||
| { | |||||
| var result = await memberLookupTask; | |||||
| if (result != null && result.Any()) | |||||
| return result.Select(x => (IGuildUser)SocketGuildUser.Create(guildId, _client, x)); | |||||
| if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) | |||||
| return await _client.Rest.GetGuildUsersAsync(guildId, options); | |||||
| return null; | |||||
| })); | |||||
| } | |||||
| } | |||||
| if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) | |||||
| return new ValueTask<IEnumerable<IGuildUser>>(_client.Rest.GetGuildUsersAsync(guildId, options).ContinueWith(x => x.Result.Cast<IGuildUser>())); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IUser> GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null) | |||||
| { | |||||
| var behavior = ResolveBehavior(stateBehavior); | |||||
| var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; | |||||
| if (behavior != StateBehavior.DownloadOnly) | |||||
| { | |||||
| var userLookupTask = _cache.GetUserAsync(id, cacheMode); | |||||
| if (userLookupTask.IsCompleted) | |||||
| { | |||||
| var model = userLookupTask.Result; | |||||
| if(model != null) | |||||
| return new ValueTask<IUser>(SocketGlobalUser.Create(_client, null, model)); | |||||
| } | |||||
| else | |||||
| { | |||||
| return new ValueTask<IUser>(Task.Run<IUser>(async () => | |||||
| { | |||||
| var result = await userLookupTask; | |||||
| if (result != null) | |||||
| return SocketGlobalUser.Create(_client, null, result); | |||||
| if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) | |||||
| return await _client.Rest.GetUserAsync(id, options); | |||||
| return null; | |||||
| })); | |||||
| } | |||||
| } | |||||
| if (behavior == StateBehavior.AllowDownload || behavior == StateBehavior.DownloadOnly) | |||||
| return new ValueTask<IUser>(_client.Rest.GetUserAsync(id, options).ContinueWith(x => (IUser)x.Result)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask<IEnumerable<IUser>> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null) | |||||
| { | |||||
| var behavior = ResolveBehavior(stateBehavior); | |||||
| var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; | |||||
| if(behavior != StateBehavior.DownloadOnly) | |||||
| { | |||||
| var usersTask = _cache.GetUsersAsync(cacheMode); | |||||
| if (usersTask.IsCompleted) | |||||
| return new ValueTask<IEnumerable<IUser>>(usersTask.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x))); | |||||
| else | |||||
| { | |||||
| return new ValueTask<IEnumerable<IUser>>(usersTask.AsTask().ContinueWith(x => x.Result.Select(x => (IUser)SocketGlobalUser.Create(_client, null, x)))); | |||||
| } | |||||
| } | |||||
| // no download path | |||||
| return default; | |||||
| } | |||||
| public ValueTask RemoveMemberAsync(ulong id, ulong guildId) | |||||
| => _cache.RemoveMemberAsync(id, guildId, CacheRunMode.Async); | |||||
| public ValueTask RemoveUserAsync(ulong id) | |||||
| => _cache.RemoveUserAsync(id, CacheRunMode.Async); | |||||
| public ValueTask<IPresence> GetPresenceAsync(ulong userId, StateBehavior stateBehavior) | |||||
| { | |||||
| var behavior = ResolveBehavior(stateBehavior); | |||||
| var cacheMode = behavior == StateBehavior.SyncOnly ? CacheRunMode.Sync : CacheRunMode.Async; | |||||
| if(stateBehavior != StateBehavior.DownloadOnly) | |||||
| { | |||||
| var fetchTask = _cache.GetPresenceAsync(userId, cacheMode); | |||||
| if (fetchTask.IsCompleted) | |||||
| return new ValueTask<IPresence>(SocketPresence.Create(fetchTask.Result)); | |||||
| else | |||||
| { | |||||
| return new ValueTask<IPresence>(Task.Run(async () => | |||||
| { | |||||
| var result = await fetchTask; | |||||
| if(result != null) | |||||
| return (IPresence)SocketPresence.Create(result); | |||||
| return null; | |||||
| })); | |||||
| } | |||||
| } | |||||
| // no download path | |||||
| return new ValueTask<IPresence>((IPresence)null); | |||||
| } | |||||
| public ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior) | |||||
| { | |||||
| if (presense is not SocketPresence socketPresense) | |||||
| throw new ArgumentException($"Expected socket entity but got {presense?.GetType()}"); | |||||
| var model = socketPresense.ToModel(); | |||||
| RunAsyncWithLogs(_cache.AddOrUpdatePresenseAsync(userId, model, CacheRunMode.Async)); | |||||
| return default; | |||||
| } | |||||
| public ValueTask RemovePresenseAsync(ulong userId) | |||||
| => _cache.RemovePresenseAsync(userId, CacheRunMode.Async); | |||||
| } | |||||
| } | |||||
| @@ -1,25 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| public interface IStateProvider | |||||
| { | |||||
| ValueTask<IPresence> GetPresenceAsync(ulong userId, StateBehavior stateBehavior); | |||||
| ValueTask AddOrUpdatePresenseAsync(ulong userId, IPresence presense, StateBehavior stateBehavior); | |||||
| ValueTask RemovePresenseAsync(ulong userId); | |||||
| ValueTask<IUser> GetUserAsync(ulong id, StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask<IEnumerable<IUser>> GetUsersAsync(StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask AddOrUpdateUserAsync(IUser user); | |||||
| ValueTask RemoveUserAsync(ulong id); | |||||
| ValueTask<IGuildUser> GetMemberAsync(ulong guildId, ulong id, StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask<IEnumerable<IGuildUser>> GetMembersAsync(ulong guildId, StateBehavior stateBehavior, RequestOptions options = null); | |||||
| ValueTask AddOrUpdateMemberAsync(ulong guildId, IGuildUser user); | |||||
| ValueTask RemoveMemberAsync(ulong guildId, ulong id); | |||||
| } | |||||
| } | |||||
| @@ -1,53 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| public enum StateBehavior | |||||
| { | |||||
| /// <summary> | |||||
| /// Use the default Cache Behavior of the client. | |||||
| /// </summary> | |||||
| /// <seealso cref="DiscordSocketConfig.DefaultStateBehavior"/> | |||||
| Default = 0, | |||||
| /// <summary> | |||||
| /// The entity will only be retrieved via a synchronous cache lookup. | |||||
| /// | |||||
| /// For the default <see cref="IStateProvider"/>, this is equivalent to using <see cref="CacheOnly"/> | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This flag is used to indicate that the retrieval of this entity should not leave the | |||||
| /// synchronous path of the <see cref="System.Threading.Tasks.ValueTask"/>. When true, | |||||
| /// the calling method *should* not ever leave the calling task, and never generate an async | |||||
| /// state machine. | |||||
| /// | |||||
| /// Bear in mind that the true behavior of this flag depends entirely on the <see cref="IStateProvider"/> to | |||||
| /// abide by design implications of this flag. Once Discord.Net has called out to the state provider with this | |||||
| /// flag, it is out of our control whether or not an async method is evaluated. | |||||
| /// </remarks> | |||||
| SyncOnly = 1, | |||||
| /// <summary> | |||||
| /// The entity will only be retrieved via a cache lookup - the Discord API will not be contacted to retrieve the entity. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// When using an alternative <see cref="IStateProvider"/>, usage of this flag implies that it is | |||||
| /// okay for the state provider to make an external call if the local cache missed the entity. | |||||
| /// | |||||
| /// Note that when designing an <see cref="IStateProvider"/>, this flag does not imply that the state | |||||
| /// provider itself should contact Discord for the entity; rather that if using a dual-layer caching system, | |||||
| /// it would be okay to contact an external layer, e.g. Redis, for the entity. | |||||
| /// </remarks> | |||||
| CacheOnly = 2, | |||||
| /// <summary> | |||||
| /// The entity will be downloaded from the Discord REST API if the <see cref="ICacheProvider"/> on hand cannot locate it. | |||||
| /// </summary> | |||||
| AllowDownload = 3, | |||||
| /// <summary> | |||||
| /// The entity will be downloaded from the Discord REST API. The local <see cref="ICacheProvider"/> will not be contacted to find the entity. | |||||
| /// </summary> | |||||
| DownloadOnly = 4 | |||||
| } | |||||
| } | |||||