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