| @@ -8,6 +8,10 @@ namespace Discord | |||
| { | |||
| internal interface ICached<TType> | |||
| { | |||
| void Update(TType model); | |||
| 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 | |||
| { | |||
| public interface IPresenceModel | |||
| public interface IPresenceModel : IEntityModel<ulong> | |||
| { | |||
| ulong UserId { get; set; } | |||
| ulong? GuildId { get; set; } | |||
| @@ -6,10 +6,9 @@ using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public interface IMemberModel | |||
| public interface IMemberModel : IEntityModel<ulong> | |||
| { | |||
| IUserModel User { get; set; } | |||
| //IUserModel User { get; set; } | |||
| string Nickname { get; set; } | |||
| string GuildAvatar { 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 | |||
| { | |||
| public interface IUserModel | |||
| public interface IUserModel : IEntityModel<ulong> | |||
| { | |||
| ulong Id { get; set; } | |||
| string Username { get; set; } | |||
| string Discriminator { get; set; } | |||
| bool? IsBot { get; set; } | |||
| @@ -63,8 +63,8 @@ namespace Discord.API | |||
| 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 { | |||
| get => Activities.ToArray(); set => throw new NotSupportedException(); | |||
| } | |||
| ulong IEntityModel<ulong>.Id { | |||
| get => User.Id; set => throw new NotSupportedException(); | |||
| } | |||
| } | |||
| } | |||
| @@ -3,10 +3,10 @@ using System; | |||
| namespace Discord.API | |||
| { | |||
| internal class ThreadMember | |||
| internal class ThreadMember : IThreadMemberModel | |||
| { | |||
| [JsonProperty("id")] | |||
| public Optional<ulong> Id { get; set; } | |||
| public Optional<ulong> ThreadId { get; set; } | |||
| [JsonProperty("user_id")] | |||
| public Optional<ulong> UserId { get; set; } | |||
| @@ -14,7 +14,9 @@ namespace Discord.API | |||
| [JsonProperty("join_timestamp")] | |||
| public DateTimeOffset JoinTimestamp { get; set; } | |||
| [JsonProperty("flags")] | |||
| public int Flags { get; set; } // No enum type (yet?) | |||
| ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } | |||
| 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(); | |||
| } | |||
| ulong IUserModel.Id | |||
| ulong IEntityModel<ulong>.Id | |||
| { | |||
| get => Id; | |||
| set => throw new NotSupportedException(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -9,74 +9,74 @@ namespace Discord.WebSocket | |||
| { | |||
| public class DefaultConcurrentCacheProvider : ICacheProvider | |||
| { | |||
| private readonly ConcurrentDictionary<ulong, IUserModel> _users; | |||
| private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IMemberModel>> _members; | |||
| private readonly ConcurrentDictionary<ulong, IPresenceModel> _presense; | |||
| private 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 | |||
| { | |||
| #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.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Runtime.CompilerServices; | |||
| using System.Text; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.WebSocket | |||
| { | |||
| internal class CacheWeakReference<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)) | |||
| .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | |||
| private readonly IStateProvider _state; | |||
| internal bool AllowSyncWaits | |||
| => _client.AllowSynchronousWaiting; | |||
| public ClientStateManager(IStateProvider state, int guildCount, int dmChannelCount) | |||
| private readonly ICacheProvider _cacheProvider; | |||
| private readonly DiscordSocketClient _client; | |||
| public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) | |||
| { | |||
| _state = state; | |||
| _client = client; | |||
| _cacheProvider = client.CacheProvider; | |||
| double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | |||
| double estimatedUsersCount = guildCount * AverageUsersPerGuild; | |||
| _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | |||
| @@ -43,6 +49,8 @@ namespace Discord.WebSocket | |||
| _users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | |||
| _groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); | |||
| _commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); | |||
| CreateStores(); | |||
| } | |||
| internal SocketChannel GetChannel(ulong id) | |||
| @@ -70,16 +70,17 @@ namespace Discord.WebSocket | |||
| internal int TotalShards { get; private set; } | |||
| internal int MessageCacheSize { get; private set; } | |||
| internal int LargeThreshold { get; private set; } | |||
| internal ICacheProvider CacheProvider { get; private set; } | |||
| internal ClientStateManager StateManager { get; private set; } | |||
| internal UdpSocketProvider UdpSocketProvider { get; private set; } | |||
| internal WebSocketProvider WebSocketProvider { get; private set; } | |||
| internal IStateProvider StateProvider { get; private set; } | |||
| internal bool AlwaysDownloadUsers { get; private set; } | |||
| internal int? HandlerTimeout { get; private set; } | |||
| internal bool AlwaysDownloadDefaultStickers { get; private set; } | |||
| internal bool AlwaysResolveStickers { get; private set; } | |||
| internal bool LogGatewayIntentWarnings { get; private set; } | |||
| internal bool SuppressUnknownDispatchWarnings { get; private set; } | |||
| internal bool AllowSynchronousWaiting { get; private set; } | |||
| internal new DiscordSocketApiClient ApiClient => base.ApiClient; | |||
| /// <inheritdoc /> | |||
| public override IReadOnlyCollection<SocketGuild> Guilds => StateManager.Guilds; | |||
| @@ -155,6 +156,8 @@ namespace Discord.WebSocket | |||
| LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; | |||
| SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; | |||
| HandlerTimeout = config.HandlerTimeout; | |||
| CacheProvider = config.CacheProvider ?? new DefaultConcurrentCacheProvider(); | |||
| AllowSynchronousWaiting = config.AllowSynchronousWaiting; | |||
| Rest = new DiscordSocketRestClient(config, ApiClient); | |||
| _heartbeatTimes = new ConcurrentQueue<long>(); | |||
| _gatewayIntents = config.GatewayIntents; | |||
| @@ -166,7 +169,6 @@ namespace Discord.WebSocket | |||
| OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); | |||
| _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); | |||
| _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); | |||
| StateProvider = config.StateProvider ?? new DefaultStateProvider(_gatewayLogger, config.CacheProvider ?? new DefaultConcurrentCacheProvider(5, 50), this, config.DefaultStateBehavior); | |||
| _nextAudioId = 1; | |||
| _shardedClient = shardedClient; | |||
| @@ -206,10 +208,14 @@ namespace Discord.WebSocket | |||
| #region State | |||
| 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) | |||
| => 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 | |||
| @@ -409,7 +415,7 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| public override SocketUser GetUser(ulong id) | |||
| => StateManager.GetUser(id); | |||
| => StateManager.UserStore.Get(id); | |||
| /// <inheritdoc /> | |||
| public override SocketUser GetUser(string username, string discriminator) | |||
| => StateManager.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); | |||
| @@ -496,23 +502,18 @@ namespace Discord.WebSocket | |||
| public void PurgeUserCache() => StateManager.PurgeUsers(); | |||
| internal SocketGlobalUser GetOrCreateUser(ClientStateManager state, IUserModel model) | |||
| { | |||
| return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model)); | |||
| return state.UserStore.GetOrAdd(model.Id, x => model); | |||
| } | |||
| internal SocketUser GetOrCreateTemporaryUser(ClientStateManager state, Discord.API.User model) | |||
| { | |||
| return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model); | |||
| return state.UserStore.Get(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, model); | |||
| } | |||
| internal SocketGlobalUser GetOrCreateSelfUser(ClientStateManager state, ICurrentUserModel model) | |||
| { | |||
| return state.GetOrAddUser(model.Id, x => | |||
| { | |||
| var user = SocketGlobalUser.Create(this, state, model); | |||
| user.GlobalUser.AddRef(); | |||
| return user; | |||
| }); | |||
| return state.UserStore.GetOrAdd(model.Id, x => model); | |||
| } | |||
| internal void RemoveUser(ulong id) | |||
| => StateManager.RemoveUser(id); | |||
| => StateManager.UserStore.Remove(id); | |||
| /// <inheritdoc/> | |||
| 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) | |||
| return; | |||
| var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | |||
| StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); | |||
| await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); | |||
| var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); | |||
| @@ -813,6 +814,7 @@ namespace Discord.WebSocket | |||
| int latency = (int)(Environment.TickCount - time); | |||
| int before = Latency; | |||
| Latency = latency; | |||
| StateManager?.ClearDeadReferences(); | |||
| await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); | |||
| } | |||
| @@ -859,21 +861,26 @@ namespace Discord.WebSocket | |||
| await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | |||
| 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; | |||
| await StateManager.InitializeAsync().ConfigureAwait(false); | |||
| var currentUser = SocketSelfUser.Create(this, state, data.User); | |||
| var currentUser = SocketSelfUser.Create(this, data.User); | |||
| Rest.CreateRestSelfUser(data.User); | |||
| var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | |||
| StateManager.AddOrUpdatePresence(new SocketPresence(Status, null, activities)); | |||
| await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false); | |||
| ApiClient.CurrentUserId = currentUser.Id; | |||
| ApiClient.CurrentApplicationId = data.Application.Id; | |||
| Rest.CurrentUser = RestSelfUser.Create(this, data.User); | |||
| int unavailableGuilds = 0; | |||
| for (int i = 0; i < data.Guilds.Length; i++) | |||
| { | |||
| var model = data.Guilds[i]; | |||
| var guild = AddGuild(model, state); | |||
| var guild = await AddGuildAsync(model).ConfigureAwait(false); | |||
| if (!guild.IsAvailable) | |||
| unavailableGuilds++; | |||
| else | |||
| @@ -950,6 +957,7 @@ namespace Discord.WebSocket | |||
| if (guild != null) | |||
| { | |||
| guild.Update(StateManager, data); | |||
| await guild.UpdateCacheAsync(data).ConfigureAwait(false); | |||
| if (_unavailableGuildCount != 0) | |||
| _unavailableGuildCount--; | |||
| @@ -971,7 +979,7 @@ namespace Discord.WebSocket | |||
| { | |||
| await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); | |||
| var guild = AddGuild(data, StateManager); | |||
| var guild = await AddGuildAsync(data).ConfigureAwait(false); | |||
| if (guild != null) | |||
| { | |||
| await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); | |||
| @@ -1290,13 +1298,13 @@ namespace Discord.WebSocket | |||
| if (user != null) | |||
| { | |||
| var before = user.Clone(); | |||
| if (user.GlobalUser.Update(StateManager, data.User)) | |||
| if (user.GlobalUser.Value.Update(data.User)) // TODO: update cache only and have lazy like support for events. | |||
| { | |||
| //Global data was updated, trigger UserUpdated | |||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser, user).ConfigureAwait(false); | |||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before.GlobalUser.Value, user).ConfigureAwait(false); | |||
| } | |||
| user.Update(StateManager, data); | |||
| user.Update(data); | |||
| var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => null); | |||
| await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); | |||
| @@ -1332,12 +1340,12 @@ namespace Discord.WebSocket | |||
| return; | |||
| } | |||
| user ??= StateManager.GetUser(data.User.Id); | |||
| user ??= (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false); | |||
| if (user != null) | |||
| user.Update(StateManager, data.User); | |||
| user.Update(data.User); | |||
| else | |||
| user = StateManager.GetOrAddUser(data.User.Id, (x) => SocketGlobalUser.Create(this, StateManager, data.User)); | |||
| user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User); | |||
| await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); | |||
| } | |||
| @@ -1957,8 +1965,8 @@ namespace Discord.WebSocket | |||
| } | |||
| else | |||
| { | |||
| var globalBefore = user.GlobalUser.Clone(); | |||
| if (user.GlobalUser.Update(StateManager, data.User)) | |||
| var globalBefore = user.GlobalUser.Value.Clone(); | |||
| if (user.GlobalUser.Value.Update(StateManager, data.User)) | |||
| { | |||
| //Global data was updated, trigger UserUpdated | |||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | |||
| @@ -1978,7 +1986,7 @@ namespace Discord.WebSocket | |||
| var before = user.Presence?.Value?.Clone(); | |||
| user.Update(StateManager, data.User); | |||
| var after = SocketPresence.Create(data); | |||
| StateManager.AddOrUpdatePresence(after); | |||
| StateManager.AddOrUpdatePresence(data); | |||
| await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); | |||
| } | |||
| break; | |||
| @@ -2324,7 +2332,7 @@ namespace Discord.WebSocket | |||
| } | |||
| SocketUser user = data.User.IsSpecified | |||
| ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, StateManager, data.User.Value)) | |||
| ? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value) | |||
| : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. | |||
| SocketChannel channel = null; | |||
| @@ -2579,9 +2587,9 @@ namespace Discord.WebSocket | |||
| entity.Update(StateManager, thread); | |||
| } | |||
| foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id)) | |||
| foreach(var member in data.Members.Where(x => x.ThreadId.Value == entity.Id)) | |||
| { | |||
| var guildMember = guild.GetUser(member.Id.Value); | |||
| var guildMember = guild.GetUser(member.ThreadId.Value); | |||
| entity.AddOrUpdateThreadMember(member, guildMember); | |||
| } | |||
| @@ -2594,11 +2602,11 @@ namespace Discord.WebSocket | |||
| 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) | |||
| { | |||
| await UnknownChannelAsync(type, data.Id.Value); | |||
| await UnknownChannelAsync(type, data.ThreadId.Value); | |||
| return; | |||
| } | |||
| @@ -2948,10 +2956,11 @@ namespace Discord.WebSocket | |||
| 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) | |||
| _largeGuilds.Enqueue(model.Id); | |||
| return guild; | |||
| @@ -2977,19 +2986,12 @@ namespace Discord.WebSocket | |||
| internal ISocketPrivateChannel RemovePrivateChannel(ulong id) | |||
| { | |||
| var channel = StateManager.RemoveChannel(id) as ISocketPrivateChannel; | |||
| if (channel != null) | |||
| { | |||
| foreach (var recipient in channel.Recipients) | |||
| recipient.GlobalUser.RemoveRef(this); | |||
| } | |||
| return channel; | |||
| } | |||
| internal void RemoveDMChannels() | |||
| { | |||
| var channels = StateManager.DMChannels; | |||
| StateManager.PurgeDMChannels(); | |||
| foreach (var channel in channels) | |||
| channel.Recipient.GlobalUser.RemoveRef(this); | |||
| } | |||
| internal void EnsureGatewayIntent(GatewayIntents intents) | |||
| @@ -29,7 +29,12 @@ namespace Discord.WebSocket | |||
| /// Gets or sets the cache provider to use | |||
| /// </summary> | |||
| 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> | |||
| /// Returns the encoding gateway should use. | |||
| @@ -199,11 +204,6 @@ namespace Discord.WebSocket | |||
| /// </summary> | |||
| 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> | |||
| /// Initializes a new instance of the <see cref="DiscordSocketConfig"/> class with the default configuration. | |||
| /// </summary> | |||
| @@ -452,17 +452,8 @@ namespace Discord.WebSocket | |||
| } | |||
| _events = events; | |||
| for (int i = 0; i < model.Members.Length; i++) | |||
| { | |||
| Discord.StateManager.AddOrUpdateMember(Id, SocketGuildUser.Create(Id, Discord, model.Members[i])); | |||
| } | |||
| DownloadedMemberCount = model.Members.Length; | |||
| for (int i = 0; i < model.Presences.Length; i++) | |||
| { | |||
| Discord.StateManager.AddOrUpdatePresence(SocketPresence.Create(model.Presences[i])); | |||
| } | |||
| MemberCount = model.MemberCount; | |||
| @@ -553,29 +544,12 @@ namespace Discord.WebSocket | |||
| else | |||
| _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) | |||
| { | |||
| @@ -12,45 +12,27 @@ namespace Discord.WebSocket | |||
| public override string Username { get; internal set; } | |||
| public override ushort DiscriminatorValue { get; internal set; } | |||
| public override string AvatarId { get; internal set; } | |||
| public override bool IsWebhook => false; | |||
| internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } | |||
| private readonly object _lockObj = new object(); | |||
| private ushort _references; | |||
| private SocketGlobalUser(DiscordSocketClient discord, ulong id) | |||
| : base(discord, id) | |||
| { | |||
| } | |||
| internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||
| internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model) | |||
| { | |||
| var entity = new SocketGlobalUser(discord, model.Id); | |||
| entity.Update(state, model); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| internal void AddRef() | |||
| { | |||
| checked | |||
| { | |||
| lock (_lockObj) | |||
| _references++; | |||
| } | |||
| } | |||
| internal void RemoveRef(DiscordSocketClient discord) | |||
| ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||
| public override void Dispose() | |||
| { | |||
| lock (_lockObj) | |||
| { | |||
| if (--_references <= 0) | |||
| discord.RemoveUser(Id); | |||
| } | |||
| GC.SuppressFinalize(this); | |||
| Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||
| } | |||
| ~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||
| public void Dispose() => Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | |||
| internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | |||
| } | |||
| @@ -18,38 +18,33 @@ namespace Discord.WebSocket | |||
| /// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. | |||
| /// </returns> | |||
| public SocketGroupChannel Channel { get; } | |||
| /// <inheritdoc /> | |||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||
| /// <inheritdoc /> | |||
| public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||
| /// <inheritdoc /> | |||
| public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||
| /// <inheritdoc /> | |||
| public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||
| /// <inheritdoc /> | |||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||
| /// <inheritdoc /> | |||
| internal override Lazy<SocketPresence> Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||
| /// <inheritdoc /> | |||
| public override bool IsWebhook => false; | |||
| internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) | |||
| : base(channel.Discord, globalUser.Id) | |||
| internal SocketGroupUser(SocketGroupChannel channel, ulong userId) | |||
| : base(channel.Discord, userId) | |||
| { | |||
| Channel = channel; | |||
| GlobalUser = globalUser; | |||
| } | |||
| internal static SocketGroupUser Create(SocketGroupChannel channel, ClientStateManager state, Model model) | |||
| internal static SocketGroupUser Create(SocketGroupChannel channel, Model model) | |||
| { | |||
| var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); | |||
| entity.Update(state, model); | |||
| var entity = new SocketGroupUser(channel, model.Id); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; | |||
| internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | |||
| public override void Dispose() | |||
| { | |||
| GC.SuppressFinalize(this); | |||
| if (GlobalUser.IsValueCreated) | |||
| GlobalUser.Value.Dispose(); | |||
| } | |||
| ~SocketGroupUser() => Dispose(); | |||
| #endregion | |||
| #region IVoiceState | |||
| @@ -25,7 +25,6 @@ namespace Discord.WebSocket | |||
| private ImmutableArray<ulong> _roleIds; | |||
| private ulong _guildId; | |||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||
| /// <summary> | |||
| /// Gets the guild the user is in. | |||
| /// </summary> | |||
| @@ -43,13 +42,13 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc/> | |||
| public string GuildAvatarId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||
| public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } | |||
| /// <inheritdoc /> | |||
| public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||
| public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } | |||
| /// <inheritdoc /> | |||
| public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||
| public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } | |||
| /// <inheritdoc /> | |||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||
| public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } | |||
| /// <inheritdoc /> | |||
| public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.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; | |||
| Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||
| GlobalUser = globalUser; | |||
| } | |||
| internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) | |||
| { | |||
| var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, (Discord.API.User)model), client); | |||
| if (entity.Update(client.StateManager, model)) | |||
| client.StateManager.AddOrUpdateMember(guildId, entity); | |||
| var entity = new SocketGuildUser(guildId, model.Id, client); | |||
| if (entity.Update(model)) | |||
| client.StateManager.AddOrUpdateMember(guildId, entity.ToModel()); | |||
| entity.UpdateRoles(Array.Empty<ulong>()); | |||
| return entity; | |||
| } | |||
| internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) | |||
| { | |||
| var entity = new SocketGuildUser(guildId, client.GetOrCreateUser(client.StateManager, model.User), client); | |||
| entity.Update(client.StateManager, model); | |||
| client.StateManager.AddOrUpdateMember(guildId, entity); | |||
| var entity = new SocketGuildUser(guildId, model.Id, client); | |||
| entity.Update(model); | |||
| client.StateManager.AddOrUpdateMember(guildId, model); | |||
| return entity; | |||
| } | |||
| internal void Update(ClientStateManager state, MemberModel model) | |||
| internal void Update(MemberModel model) | |||
| { | |||
| base.Update(state, model.User); | |||
| _joinedAtTicks = model.JoinedAt.UtcTicks; | |||
| Nickname = model.Nickname; | |||
| GuildAvatarId = model.GuildAvatar; | |||
| @@ -234,12 +230,7 @@ namespace Discord.WebSocket | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | |||
| internal new SocketGuildUser Clone() | |||
| { | |||
| var clone = MemberwiseClone() as SocketGuildUser; | |||
| clone.GlobalUser = GlobalUser.Clone(); | |||
| return clone; | |||
| } | |||
| internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; | |||
| #endregion | |||
| @@ -260,8 +251,7 @@ namespace Discord.WebSocket | |||
| private struct CacheModel : MemberModel | |||
| { | |||
| public UserModel User { get; set; } | |||
| public ulong Id { get; set; } | |||
| public string Nickname { get; set; } | |||
| public string GuildAvatar { get; set; } | |||
| @@ -280,15 +270,14 @@ namespace Discord.WebSocket | |||
| 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, | |||
| GuildAvatar = GuildAvatarId, | |||
| 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); | |||
| #endregion | |||
| @@ -114,6 +114,12 @@ namespace Discord.WebSocket | |||
| public ulong UserId { get; set; } | |||
| public ulong? GuildId { get; set; } | |||
| ulong IEntityModel<ulong>.Id | |||
| { | |||
| get => UserId; | |||
| set => throw new NotSupportedException(); | |||
| } | |||
| } | |||
| private struct ActivityCacheModel : IActivityModel | |||
| @@ -156,8 +162,11 @@ namespace Discord.WebSocket | |||
| } | |||
| internal Model ToModel() | |||
| => ToModel<CacheModel>(); | |||
| internal TModel ToModel<TModel>() where TModel : Model, new() | |||
| { | |||
| return new CacheModel | |||
| return new TModel | |||
| { | |||
| Status = Status, | |||
| ActiveClients = ActiveClients.ToArray(), | |||
| @@ -194,6 +203,8 @@ namespace Discord.WebSocket | |||
| } | |||
| Model ICached<Model>.ToModel() => ToModel(); | |||
| TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>(); | |||
| void ICached<Model>.Update(Model model) => Update(model); | |||
| #endregion | |||
| } | |||
| @@ -19,18 +19,17 @@ namespace Discord.WebSocket | |||
| public bool IsVerified { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsMfaEnabled { get; private set; } | |||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||
| /// <inheritdoc /> | |||
| public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||
| public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } | |||
| /// <inheritdoc /> | |||
| public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||
| public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } | |||
| /// <inheritdoc /> | |||
| public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||
| public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } | |||
| /// <inheritdoc /> | |||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||
| public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } | |||
| /// <inheritdoc /> | |||
| 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 /> | |||
| public UserProperties Flags { get; internal set; } | |||
| /// <inheritdoc /> | |||
| @@ -41,20 +40,20 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| public override bool IsWebhook => false; | |||
| internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) | |||
| : base(discord, globalUser.Id) | |||
| internal SocketSelfUser(DiscordSocketClient discord, ulong userId) | |||
| : base(discord, userId) | |||
| { | |||
| GlobalUser = globalUser; | |||
| } | |||
| internal static SocketSelfUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||
| internal static SocketSelfUser Create(DiscordSocketClient discord, Model model) | |||
| { | |||
| var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); | |||
| entity.Update(state, model); | |||
| var entity = new SocketSelfUser(discord, model.Id); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| internal override bool Update(ClientStateManager state, UserModel model) | |||
| internal override bool Update(UserModel model) | |||
| { | |||
| bool hasGlobalChanges = base.Update(state, model); | |||
| bool hasGlobalChanges = base.Update(model); | |||
| if (model is not Model currentUserModel) | |||
| throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); | |||
| @@ -98,9 +97,13 @@ namespace Discord.WebSocket | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | |||
| internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | |||
| public override void Dispose() | |||
| { | |||
| GC.SuppressFinalize(this); | |||
| Discord.StateManager.RemoveReferencedGlobalUser(Id); | |||
| } | |||
| #region Cache | |||
| private struct CacheModel : Model | |||
| { | |||
| public bool? IsVerified { get; set; } | |||
| @@ -128,9 +131,12 @@ namespace Discord.WebSocket | |||
| 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, | |||
| 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 | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.ThreadMember; | |||
| using Model = Discord.IThreadMemberModel; | |||
| using System.Collections.Immutable; | |||
| namespace Discord.WebSocket | |||
| @@ -10,12 +10,12 @@ namespace Discord.WebSocket | |||
| /// <summary> | |||
| /// Represents a thread user received over the gateway. | |||
| /// </summary> | |||
| public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser | |||
| public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached<Model> | |||
| { | |||
| /// <summary> | |||
| /// Gets the <see cref="SocketThreadChannel"/> this user is in. | |||
| /// </summary> | |||
| public SocketThreadChannel Thread { get; private set; } | |||
| public Lazy<SocketThreadChannel> Thread { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset ThreadJoinedAt { get; private set; } | |||
| @@ -23,126 +23,142 @@ namespace Discord.WebSocket | |||
| /// <summary> | |||
| /// Gets the guild this user is in. | |||
| /// </summary> | |||
| public SocketGuild Guild { get; private set; } | |||
| public Lazy<SocketGuild> Guild { get; private set; } | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset? JoinedAt | |||
| => GuildUser.JoinedAt; | |||
| => GuildUser.Value.JoinedAt; | |||
| /// <inheritdoc/> | |||
| public string DisplayName | |||
| => GuildUser.Nickname ?? GuildUser.Username; | |||
| => GuildUser.Value.Nickname ?? GuildUser.Value.Username; | |||
| /// <inheritdoc/> | |||
| public string Nickname | |||
| => GuildUser.Nickname; | |||
| => GuildUser.Value.Nickname; | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset? PremiumSince | |||
| => GuildUser.PremiumSince; | |||
| => GuildUser.Value.PremiumSince; | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset? TimedOutUntil | |||
| => GuildUser.TimedOutUntil; | |||
| => GuildUser.Value.TimedOutUntil; | |||
| /// <inheritdoc/> | |||
| public bool? IsPending | |||
| => GuildUser.IsPending; | |||
| => GuildUser.Value.IsPending; | |||
| /// <inheritdoc /> | |||
| public int Hierarchy | |||
| => GuildUser.Hierarchy; | |||
| => GuildUser.Value.Hierarchy; | |||
| /// <inheritdoc/> | |||
| public override string AvatarId | |||
| { | |||
| get => GuildUser.AvatarId; | |||
| internal set => GuildUser.AvatarId = value; | |||
| get => GuildUser.Value.AvatarId; | |||
| internal set => GuildUser.Value.AvatarId = value; | |||
| } | |||
| /// <inheritdoc/> | |||
| public string DisplayAvatarId => GuildAvatarId ?? AvatarId; | |||
| /// <inheritdoc/> | |||
| public string GuildAvatarId | |||
| => GuildUser.GuildAvatarId; | |||
| => GuildUser.Value.GuildAvatarId; | |||
| /// <inheritdoc/> | |||
| public override ushort DiscriminatorValue | |||
| { | |||
| get => GuildUser.DiscriminatorValue; | |||
| internal set => GuildUser.DiscriminatorValue = value; | |||
| get => GuildUser.Value.DiscriminatorValue; | |||
| internal set => GuildUser.Value.DiscriminatorValue = value; | |||
| } | |||
| /// <inheritdoc/> | |||
| public override bool IsBot | |||
| { | |||
| get => GuildUser.IsBot; | |||
| internal set => GuildUser.IsBot = value; | |||
| get => GuildUser.Value.IsBot; | |||
| internal set => GuildUser.Value.IsBot = value; | |||
| } | |||
| /// <inheritdoc/> | |||
| public override bool IsWebhook | |||
| => GuildUser.IsWebhook; | |||
| => GuildUser.Value.IsWebhook; | |||
| /// <inheritdoc/> | |||
| public override string Username | |||
| { | |||
| get => GuildUser.Username; | |||
| internal set => GuildUser.Username = value; | |||
| get => GuildUser.Value.Username; | |||
| internal set => GuildUser.Value.Username = value; | |||
| } | |||
| /// <inheritdoc/> | |||
| public bool IsDeafened | |||
| => GuildUser.IsDeafened; | |||
| => GuildUser.Value.IsDeafened; | |||
| /// <inheritdoc/> | |||
| public bool IsMuted | |||
| => GuildUser.IsMuted; | |||
| => GuildUser.Value.IsMuted; | |||
| /// <inheritdoc/> | |||
| public bool IsSelfDeafened | |||
| => GuildUser.IsSelfDeafened; | |||
| => GuildUser.Value.IsSelfDeafened; | |||
| /// <inheritdoc/> | |||
| public bool IsSelfMuted | |||
| => GuildUser.IsSelfMuted; | |||
| => GuildUser.Value.IsSelfMuted; | |||
| /// <inheritdoc/> | |||
| public bool IsSuppressed | |||
| => GuildUser.IsSuppressed; | |||
| => GuildUser.Value.IsSuppressed; | |||
| /// <inheritdoc/> | |||
| public IVoiceChannel VoiceChannel | |||
| => GuildUser.VoiceChannel; | |||
| => GuildUser.Value.VoiceChannel; | |||
| /// <inheritdoc/> | |||
| public string VoiceSessionId | |||
| => GuildUser.VoiceSessionId; | |||
| => GuildUser.Value.VoiceSessionId; | |||
| /// <inheritdoc/> | |||
| public bool IsStreaming | |||
| => GuildUser.IsStreaming; | |||
| => GuildUser.Value.IsStreaming; | |||
| /// <inheritdoc/> | |||
| public bool IsVideoing | |||
| => GuildUser.IsVideoing; | |||
| => GuildUser.Value.IsVideoing; | |||
| /// <inheritdoc/> | |||
| public DateTimeOffset? RequestToSpeakTimestamp | |||
| => GuildUser.RequestToSpeakTimestamp; | |||
| => GuildUser.Value.RequestToSpeakTimestamp; | |||
| private Lazy<SocketGuildUser> GuildUser { get; set; } | |||
| private SocketGuildUser GuildUser { get; set; } | |||
| private ulong _threadId; | |||
| private ulong _guildId; | |||
| internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId) | |||
| : base(guild.Discord, userId) | |||
| internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threadId, ulong userId) | |||
| : base(client, userId) | |||
| { | |||
| Thread = thread; | |||
| Guild = guild; | |||
| GuildUser = member; | |||
| _guildId = guildId; | |||
| _threadId = threadId; | |||
| GuildUser = new(() => client.StateManager.TryGetMemberStore(guildId, out var store) ? store.Get(userId) : null); | |||
| Thread = new(() => client.GetChannel(threadId) as SocketThreadChannel); | |||
| Guild = new(() => client.GetGuild(guildId)); | |||
| } | |||
| internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) | |||
| { | |||
| var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); | |||
| var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.UserId.Value); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model) | |||
| { | |||
| var entity = new SocketThreadUser(client, guildId, threadId, model.UserId.Value); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| @@ -150,89 +166,117 @@ namespace Discord.WebSocket | |||
| internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | |||
| { | |||
| // this is used for creating the owner of the thread. | |||
| var entity = new SocketThreadUser(guild, thread, owner, owner.Id); | |||
| entity.Update(new Model | |||
| { | |||
| JoinTimestamp = thread.CreatedAt, | |||
| }); | |||
| var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, owner.Id); | |||
| entity.ThreadJoinedAt = thread.CreatedAt; | |||
| return entity; | |||
| } | |||
| internal void Update(Model model) | |||
| { | |||
| ThreadJoinedAt = model.JoinTimestamp; | |||
| ThreadJoinedAt = model.JoinedAt; | |||
| } | |||
| /// <inheritdoc/> | |||
| public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); | |||
| public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel); | |||
| /// <inheritdoc/> | |||
| public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); | |||
| public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.Value.KickAsync(reason, options); | |||
| /// <inheritdoc/> | |||
| public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); | |||
| public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.Value.ModifyAsync(func, options); | |||
| /// <inheritdoc/> | |||
| public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); | |||
| public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(roleId, options); | |||
| /// <inheritdoc/> | |||
| public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); | |||
| public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(role, options); | |||
| /// <inheritdoc/> | |||
| public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); | |||
| public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roleIds, options); | |||
| /// <inheritdoc/> | |||
| public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); | |||
| public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roles, options); | |||
| /// <inheritdoc/> | |||
| public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); | |||
| public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(roleId, options); | |||
| /// <inheritdoc/> | |||
| public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); | |||
| public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(role, options); | |||
| /// <inheritdoc/> | |||
| public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); | |||
| public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roleIds, options); | |||
| /// <inheritdoc/> | |||
| public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); | |||
| public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roles, options); | |||
| /// <inheritdoc/> | |||
| public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options); | |||
| public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.Value.SetTimeOutAsync(span, options); | |||
| /// <inheritdoc/> | |||
| public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); | |||
| public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options); | |||
| /// <inheritdoc/> | |||
| IThreadChannel IThreadUser.Thread => Thread; | |||
| IThreadChannel IThreadUser.Thread => Thread.Value; | |||
| /// <inheritdoc/> | |||
| IGuild IThreadUser.Guild => Guild; | |||
| IGuild IThreadUser.Guild => Guild.Value; | |||
| /// <inheritdoc/> | |||
| IGuild IGuildUser.Guild => Guild; | |||
| IGuild IGuildUser.Guild => Guild.Value; | |||
| /// <inheritdoc/> | |||
| ulong IGuildUser.GuildId => Guild.Id; | |||
| ulong IGuildUser.GuildId => Guild.Value.Id; | |||
| /// <inheritdoc/> | |||
| GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; | |||
| GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions; | |||
| /// <inheritdoc/> | |||
| IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); | |||
| IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Value.Roles.Select(x => x.Id).ToImmutableArray(); | |||
| /// <inheritdoc /> | |||
| string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size); | |||
| string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size); | |||
| /// <inheritdoc /> | |||
| string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); | |||
| string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); | |||
| internal override 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> | |||
| /// Gets the guild user of this thread user. | |||
| /// </summary> | |||
| /// <param name="user"></param> | |||
| public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; | |||
| public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value; | |||
| #region Cache | |||
| 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; | |||
| /// <inheritdoc /> | |||
| 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) | |||
| : base(discord, id) | |||
| { | |||
| } | |||
| internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||
| internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model) | |||
| { | |||
| var entity = new SocketUnknownUser(discord, model.Id); | |||
| entity.Update(state, model); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| public override void Dispose() { } | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; | |||
| internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; | |||
| } | |||
| @@ -18,18 +18,18 @@ namespace Discord.WebSocket | |||
| public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable | |||
| { | |||
| /// <inheritdoc /> | |||
| public abstract bool IsBot { get; internal set; } | |||
| public virtual bool IsBot { get; internal set; } | |||
| /// <inheritdoc /> | |||
| public abstract string Username { get; internal set; } | |||
| public virtual string Username { get; internal set; } | |||
| /// <inheritdoc /> | |||
| public abstract ushort DiscriminatorValue { get; internal set; } | |||
| public virtual ushort DiscriminatorValue { get; internal set; } | |||
| /// <inheritdoc /> | |||
| public abstract string AvatarId { get; internal set; } | |||
| public virtual string AvatarId { get; internal set; } | |||
| /// <inheritdoc /> | |||
| public abstract bool IsWebhook { get; } | |||
| public virtual bool IsWebhook { get; } | |||
| /// <inheritdoc /> | |||
| public UserProperties? PublicFlags { get; private set; } | |||
| internal abstract SocketGlobalUser GlobalUser { get; set; } | |||
| internal virtual Lazy<SocketGlobalUser> GlobalUser { get; set; } | |||
| internal virtual Lazy<SocketPresence> Presence { get; set; } | |||
| /// <inheritdoc /> | |||
| @@ -57,9 +57,10 @@ namespace Discord.WebSocket | |||
| : 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; | |||
| if (model.Avatar != AvatarId) | |||
| { | |||
| @@ -98,6 +99,8 @@ namespace Discord.WebSocket | |||
| return hasChanges; | |||
| } | |||
| public abstract void Dispose(); | |||
| /// <inheritdoc /> | |||
| public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | |||
| => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | |||
| @@ -117,8 +120,6 @@ namespace Discord.WebSocket | |||
| /// The full name of the user. | |||
| /// </returns> | |||
| public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | |||
| ~SocketUser() => GlobalUser?.Dispose(); | |||
| public void Dispose() => GlobalUser?.Dispose(); | |||
| private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | |||
| internal SocketUser Clone() => MemberwiseClone() as SocketUser; | |||
| @@ -136,12 +137,9 @@ namespace Discord.WebSocket | |||
| 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, | |||
| 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 | |||
| } | |||
| } | |||
| @@ -34,7 +34,7 @@ namespace Discord.WebSocket | |||
| public override bool IsWebhook => true; | |||
| /// <inheritdoc /> | |||
| 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) | |||
| : base(guild.Discord, id) | |||
| @@ -42,16 +42,17 @@ namespace Discord.WebSocket | |||
| Guild = guild; | |||
| WebhookId = webhookId; | |||
| } | |||
| internal static SocketWebhookUser Create(SocketGuild guild, ClientStateManager state, Model model, ulong webhookId) | |||
| internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId) | |||
| { | |||
| var entity = new SocketWebhookUser(guild, model.Id, webhookId); | |||
| entity.Update(state, model); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; | |||
| internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; | |||
| #endregion | |||
| public override void Dispose() { } | |||
| #endregion | |||
| #region IGuildUser | |||
| /// <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 | |||
| } | |||
| } | |||