@@ -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. | |||
@@ -200,11 +205,6 @@ namespace Discord.WebSocket | |||
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> | |||
public DiscordSocketConfig() | |||
@@ -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 | |||
} | |||
} |