Browse Source

Final POC for users

v4/state-cache-providers
Quin Lynch 3 years ago
parent
commit
d89d13d703
21 changed files with 241 additions and 98 deletions
  1. +6
    -1
      src/Discord.Net.Core/Cache/ICached.cs
  2. +76
    -0
      src/Discord.Net.WebSocket/Cache/LazyCached.cs
  3. +56
    -14
      src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs
  4. +19
    -19
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  5. +2
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  6. +2
    -4
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  7. +0
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs
  8. +17
    -15
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  9. +1
    -1
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs
  10. +1
    -1
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
  11. +1
    -1
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs
  12. +2
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  13. +2
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  14. +2
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
  15. +5
    -5
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  16. +30
    -4
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  17. +5
    -12
      src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs
  18. +1
    -1
      src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
  19. +2
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  20. +9
    -7
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  21. +2
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs

+ 6
- 1
src/Discord.Net.Core/Cache/ICached.cs View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;


namespace Discord namespace Discord
{ {
internal interface ICached<TType>
internal interface ICached<TType> : ICached, IDisposable
{ {
void Update(TType model); void Update(TType model);


@@ -14,4 +14,9 @@ namespace Discord


TResult ToModel<TResult>() where TResult : TType, new(); TResult ToModel<TResult>() where TResult : TType, new();
} }

public interface ICached
{
bool IsFreed { get; }
}
} }

+ 76
- 0
src/Discord.Net.WebSocket/Cache/LazyCached.cs View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a lazily-loaded cached value that can be loaded synchronously or asynchronously.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <typeparam name="TId">The primary id type of the entity.</typeparam>
public class LazyCached<TEntity, TId>
where TEntity : class, ICached
where TId : IEquatable<TId>
{
/// <summary>
/// Gets or loads the cached value synchronously.
/// </summary>
public TEntity Value
=> GetOrLoad();

/// <summary>
/// Gets whether or not the <see cref="Value"/> has been loaded and is still alive.
/// </summary>
public bool IsValueCreated
=> _loadedValue != null && _loadedValue.IsFreed;

private TEntity _loadedValue;
private readonly ILookupReferenceStore<TEntity, TId> _store;
private readonly TId _id;
private readonly object _lock = new();

internal LazyCached(TEntity value)
{
_loadedValue = value;
}

internal LazyCached(TId id, ILookupReferenceStore<TEntity, TId> store)
{
_store = store;
_id = id;
}

private TEntity GetOrLoad()
{
lock (_lock)
{
if(!IsValueCreated)
_loadedValue = _store.Get(_id);
return _loadedValue;
}
}

/// <summary>
/// Gets or loads the value from the cache asynchronously.
/// </summary>
/// <returns>The loaded or fetched entity.</returns>
public async ValueTask<TEntity> GetAsync()
{
if (!IsValueCreated)
_loadedValue = await _store.GetAsync(_id).ConfigureAwait(false);
return _loadedValue;
}
}

public class LazyCached<TEntity> : LazyCached<TEntity, ulong>
where TEntity : class, ICached
{
internal LazyCached(ulong id, ILookupReferenceStore<TEntity, ulong> store)
: base(id, store) { }
internal LazyCached(TEntity entity)
: base(entity) { }
}
}

+ 56
- 14
src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs View File

@@ -19,8 +19,6 @@ namespace Discord.WebSocket


private int _referenceCount; private int _referenceCount;


private readonly object _lock = new object();

public CacheReference(TType value) public CacheReference(TType value)
{ {
Reference = new(value); Reference = new(value);
@@ -39,28 +37,31 @@ namespace Discord.WebSocket


public void ReleaseReference() public void ReleaseReference()
{ {
lock (_lock)
{
if (_referenceCount > 0)
_referenceCount--;
}
Interlocked.Decrement(ref _referenceCount);
} }
} }
internal class ReferenceStore<TEntity, TModel, TId, ISharedEntity>
where TEntity : class, ICached<TModel>, ISharedEntity

internal interface ILookupReferenceStore<TEntity, TId>
{
TEntity Get(TId id);
ValueTask<TEntity> GetAsync(TId id);
}

internal class ReferenceStore<TEntity, TModel, TId, TSharedEntity> : ILookupReferenceStore<TEntity, TId>
where TEntity : class, ICached<TModel>, TSharedEntity
where TModel : IEntityModel<TId> where TModel : IEntityModel<TId>
where TId : IEquatable<TId> where TId : IEquatable<TId>
where ISharedEntity : class
where TSharedEntity : class
{ {
private readonly ICacheProvider _cacheProvider; private readonly ICacheProvider _cacheProvider;
private readonly ConcurrentDictionary<TId, CacheReference<TEntity>> _references = new(); private readonly ConcurrentDictionary<TId, CacheReference<TEntity>> _references = new();
private IEntityStore<TModel, TId> _store; private IEntityStore<TModel, TId> _store;
private Func<TModel, TEntity> _entityBuilder; private Func<TModel, TEntity> _entityBuilder;
private Func<TId, RequestOptions, Task<ISharedEntity>> _restLookup;
private Func<TId, RequestOptions, Task<TSharedEntity>> _restLookup;
private readonly bool _allowSyncWaits; private readonly bool _allowSyncWaits;
private readonly object _lock = new(); private readonly object _lock = new();


public ReferenceStore(ICacheProvider cacheProvider, Func<TModel, TEntity> entityBuilder, Func<TId, RequestOptions, Task<ISharedEntity>> restLookup, bool allowSyncWaits)
public ReferenceStore(ICacheProvider cacheProvider, Func<TModel, TEntity> entityBuilder, Func<TId, RequestOptions, Task<TSharedEntity>> restLookup, bool allowSyncWaits)
{ {
_allowSyncWaits = allowSyncWaits; _allowSyncWaits = allowSyncWaits;
_cacheProvider = cacheProvider; _cacheProvider = cacheProvider;
@@ -68,6 +69,19 @@ namespace Discord.WebSocket
_restLookup = restLookup; _restLookup = restLookup;
} }


internal bool RemoveReference(TId id)
{
if(_references.TryGetValue(id, out var rf))
{
rf.ReleaseReference();

if (rf.CanRelease)
return _references.TryRemove(id, out _);
}

return false;
}

internal void ClearDeadReferences() internal void ClearDeadReferences()
{ {
lock (_lock) lock (_lock)
@@ -135,7 +149,7 @@ namespace Discord.WebSocket
return null; return null;
} }


public async ValueTask<ISharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null)
public async ValueTask<TSharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null)
{ {
if (TryGetReference(id, out var entity)) if (TryGetReference(id, out var entity))
{ {
@@ -216,6 +230,28 @@ namespace Discord.WebSocket
return _store.AddOrUpdateAsync(model, CacheRunMode.Async); return _store.AddOrUpdateAsync(model, CacheRunMode.Async);
} }


public void BulkAddOrUpdate(IEnumerable<TModel> models)
{
RunOrThrowValueTask(_store.AddOrUpdateBatchAsync(models, CacheRunMode.Sync));

foreach(var model in models)
{
if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity))
entity.Update(model);
}
}

public async ValueTask BulkAddOrUpdateAsync(IEnumerable<TModel> models)
{
await _store.AddOrUpdateBatchAsync(models, CacheRunMode.Async).ConfigureAwait(false);

foreach (var model in models)
{
if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity))
entity.Update(model);
}
}

public void Remove(TId id) public void Remove(TId id)
{ {
RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync)); RunOrThrowValueTask(_store.RemoveAsync(id, CacheRunMode.Sync));
@@ -239,6 +275,9 @@ namespace Discord.WebSocket
_references.Clear(); _references.Clear();
return _store.PurgeAllAsync(CacheRunMode.Async); return _store.PurgeAllAsync(CacheRunMode.Async);
} }

TEntity ILookupReferenceStore<TEntity, TId>.Get(TId id) => Get(id);
async ValueTask<TEntity> ILookupReferenceStore<TEntity, TId>.GetAsync(TId id) => (TEntity)await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false);
} }


internal partial class ClientStateManager internal partial class ClientStateManager
@@ -261,7 +300,7 @@ namespace Discord.WebSocket


PresenceStore = new ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence>( PresenceStore = new ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence>(
_cacheProvider, _cacheProvider,
m => SocketPresence.Create(m),
m => SocketPresence.Create(_client, m),
(id, options) => Task.FromResult<IPresence>(null), (id, options) => Task.FromResult<IPresence>(null),
AllowSyncWaits); AllowSyncWaits);


@@ -284,6 +323,9 @@ namespace Discord.WebSocket
await PresenceStore.InitializeAsync(); await PresenceStore.InitializeAsync();
} }


public ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> GetMemberStore(ulong guildId)
=> TryGetMemberStore(guildId, out var store) ? store : null;

public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store) public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store)
=> _memberStores.TryGetValue(guildId, out store); => _memberStores.TryGetValue(guildId, out store);




+ 19
- 19
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -214,7 +214,7 @@ namespace Discord.WebSocket
{ {
if (StateManager.TryGetMemberStore(guildId, out var store)) if (StateManager.TryGetMemberStore(guildId, out var store))
return store.GetAsync(userId, cacheMode, options); return store.GetAsync(userId, cacheMode, options);
return ValueTask.FromResult<IGuildUser>(null);
return new ValueTask<IGuildUser>((IGuildUser)null);
} }


#endregion #endregion
@@ -690,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;
await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false);
await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false);


var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null);


@@ -870,7 +870,7 @@ namespace Discord.WebSocket
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;
await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(Status, null, activities).ToModel()).ConfigureAwait(false);
await StateManager.PresenceStore.AddOrUpdateAsync(new SocketPresence(this, Status, null, activities).ToModel()).ConfigureAwait(false);


ApiClient.CurrentUserId = currentUser.Id; ApiClient.CurrentUserId = currentUser.Id;
ApiClient.CurrentApplicationId = data.Application.Id; ApiClient.CurrentApplicationId = data.Application.Id;
@@ -1345,7 +1345,7 @@ namespace Discord.WebSocket
if (user != null) if (user != null)
user.Update(data.User); user.Update(data.User);
else else
user = StateManager.GetOrAddUser(data.User.Id, (x) => data.User);
user = await StateManager.UserStore.GetOrAddAsync(data.User.Id, _ => data.User).ConfigureAwait(false);


await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false); await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), guild, user).ConfigureAwait(false);
} }
@@ -1560,7 +1560,7 @@ namespace Discord.WebSocket


SocketUser user = guild.GetUser(data.User.Id); SocketUser user = guild.GetUser(data.User.Id);
if (user == null) if (user == null)
user = SocketUnknownUser.Create(this, StateManager, data.User);
user = SocketUnknownUser.Create(this, data.User);
await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false);
} }
else else
@@ -1584,9 +1584,9 @@ namespace Discord.WebSocket
return; return;
} }


SocketUser user = StateManager.GetUser(data.User.Id);
SocketUser user = (SocketUser)await StateManager.UserStore.GetAsync(data.User.Id, CacheMode.CacheOnly).ConfigureAwait(false);
if (user == null) if (user == null)
user = SocketUnknownUser.Create(this, StateManager, data.User);
user = SocketUnknownUser.Create(this, data.User);
await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false);
} }
else else
@@ -1630,7 +1630,7 @@ namespace Discord.WebSocket
if (guild != null) if (guild != null)
{ {
if (data.WebhookId.IsSpecified) if (data.WebhookId.IsSpecified)
author = SocketWebhookUser.Create(guild, StateManager, data.Author.Value, data.WebhookId.Value);
author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value);
else else
author = guild.GetUser(data.Author.Value.Id); author = guild.GetUser(data.Author.Value.Id);
} }
@@ -1695,7 +1695,7 @@ namespace Discord.WebSocket
if (guild != null) if (guild != null)
{ {
if (data.WebhookId.IsSpecified) if (data.WebhookId.IsSpecified)
author = SocketWebhookUser.Create(guild, StateManager, data.Author.Value, data.WebhookId.Value);
author = SocketWebhookUser.Create(guild, data.Author.Value, data.WebhookId.Value);
else else
author = guild.GetUser(data.Author.Value.Id); author = guild.GetUser(data.Author.Value.Id);
} }
@@ -1966,7 +1966,7 @@ namespace Discord.WebSocket
else else
{ {
var globalBefore = user.GlobalUser.Value.Clone(); var globalBefore = user.GlobalUser.Value.Clone();
if (user.GlobalUser.Value.Update(StateManager, data.User))
if (user.GlobalUser.Value.Update(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);
@@ -1975,7 +1975,7 @@ namespace Discord.WebSocket
} }
else else
{ {
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)
{ {
await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false);
@@ -1984,9 +1984,9 @@ namespace Discord.WebSocket
} }


var before = user.Presence?.Value?.Clone(); var before = user.Presence?.Value?.Clone();
user.Update(StateManager, data.User);
var after = SocketPresence.Create(data);
StateManager.AddOrUpdatePresence(data);
user.Update(data.User);
var after = SocketPresence.Create(this, data);
await StateManager.PresenceStore.AddOrUpdateAsync(data).ConfigureAwait(false);
await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false); await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, after).ConfigureAwait(false);
} }
break; break;
@@ -2114,7 +2114,7 @@ namespace Discord.WebSocket
if (data.Id == CurrentUser.Id) if (data.Id == CurrentUser.Id)
{ {
var before = CurrentUser.Clone(); var before = CurrentUser.Clone();
CurrentUser.Update(StateManager, data);
CurrentUser.Update(data);
await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false);
} }
else else
@@ -2277,7 +2277,7 @@ namespace Discord.WebSocket
: null; : null;


SocketUser target = data.TargetUser.IsSpecified SocketUser target = data.TargetUser.IsSpecified
? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, StateManager, data.TargetUser.Value))
? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, data.TargetUser.Value))
: null; : null;


var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); var invite = SocketInvite.Create(this, guild, channel, inviter, target, data);
@@ -2332,7 +2332,7 @@ namespace Discord.WebSocket
} }


SocketUser user = data.User.IsSpecified SocketUser user = data.User.IsSpecified
? StateManager.GetOrAddUser(data.User.Value.Id, (_) => data.User.Value)
? await StateManager.UserStore.GetOrAddAsync(data.User.Value.Id, (_) => data.User.Value).ConfigureAwait(false)
: 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;
@@ -2821,7 +2821,7 @@ namespace Discord.WebSocket
return; return;
} }


var user = (SocketUser)guild.GetUser(data.UserId) ?? StateManager.GetUser(data.UserId);
var user = (SocketUser)guild.GetUser(data.UserId) ?? StateManager.UserStore.Get(data.UserId);


var cacheableUser = new Cacheable<SocketUser, RestUser, IUser, ulong>(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); var cacheableUser = new Cacheable<SocketUser, RestUser, IUser, ulong>(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId));


@@ -2958,7 +2958,7 @@ namespace Discord.WebSocket


internal async Task<SocketGuild> AddGuildAsync(ExtendedGuild model) internal async Task<SocketGuild> AddGuildAsync(ExtendedGuild model)
{ {
await StateManager.InitializeGuildStoreAsync(model.Id).ConfigureAwait(false);
await StateManager.GetMemberStoreAsync(model.Id).ConfigureAwait(false);
var guild = SocketGuild.Create(this, StateManager, model); var guild = SocketGuild.Create(this, StateManager, model);
StateManager.AddGuild(guild); StateManager.AddGuild(guild);
if (model.Large) if (model.Large)


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs View File

@@ -43,7 +43,7 @@ namespace Discord.WebSocket
} }
internal override void Update(ClientStateManager state, Model model) internal override void Update(ClientStateManager state, Model model)
{ {
Recipient.Update(state, model.Recipients.Value[0]);
Recipient.Update(model.Recipients.Value[0]);
} }
internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, ulong channelId, API.User recipient) internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, ulong channelId, API.User recipient)
{ {
@@ -53,7 +53,7 @@ namespace Discord.WebSocket
} }
internal void Update(ClientStateManager state, API.User recipient) internal void Update(ClientStateManager state, API.User recipient)
{ {
Recipient.Update(state, recipient);
Recipient.Update(recipient);
} }


/// <inheritdoc /> /// <inheritdoc />


+ 2
- 4
src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -77,7 +77,7 @@ namespace Discord.WebSocket
{ {
var users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); var users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05));
for (int i = 0; i < models.Length; i++) for (int i = 0; i < models.Length; i++)
users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]);
users[models[i].Id] = SocketGroupUser.Create(this, models[i]);
_users = users; _users = users;
} }


@@ -265,8 +265,7 @@ namespace Discord.WebSocket
return user; return user;
else else
{ {
var privateUser = SocketGroupUser.Create(this, Discord.StateManager, model);
privateUser.GlobalUser.AddRef();
var privateUser = SocketGroupUser.Create(this, model);
_users[privateUser.Id] = privateUser; _users[privateUser.Id] = privateUser;
return privateUser; return privateUser;
} }
@@ -275,7 +274,6 @@ namespace Discord.WebSocket
{ {
if (_users.TryRemove(id, out SocketGroupUser user)) if (_users.TryRemove(id, out SocketGroupUser user))
{ {
user.GlobalUser.RemoveRef(Discord);
return user; return user;
} }
return null; return null;


+ 0
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs View File

@@ -171,7 +171,6 @@ namespace Discord.WebSocket
else else
{ {
member = SocketThreadUser.Create(Guild, this, model, guildMember); member = SocketThreadUser.Create(Guild, this, model, guildMember);
member.GlobalUser.AddRef();
_members[member.Id] = member; _members[member.Id] = member;
} }
return member; return member;


+ 17
- 15
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -305,7 +305,7 @@ namespace Discord.WebSocket
/// <summary> /// <summary>
/// Gets the current logged-in user. /// Gets the current logged-in user.
/// </summary> /// </summary>
public SocketGuildUser CurrentUser => Discord.StateManager.GetMember(Discord.CurrentUser.Id, Id);
public SocketGuildUser CurrentUser => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(Discord.CurrentUser.Id) : null;
/// <summary> /// <summary>
/// Gets the built-in role containing all users in this guild. /// Gets the built-in role containing all users in this guild.
/// </summary> /// </summary>
@@ -356,7 +356,7 @@ namespace Discord.WebSocket
/// <returns> /// <returns>
/// A collection of guild users found within this guild. /// A collection of guild users found within this guild.
/// </returns> /// </returns>
public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.GetMembers(Id).Cast<SocketGuildUser>().ToImmutableArray();
public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray<SocketGuildUser>.Empty;
/// <summary> /// <summary>
/// Gets a collection of all roles in this guild. /// Gets a collection of all roles in this guild.
/// </summary> /// </summary>
@@ -547,8 +547,12 @@ namespace Discord.WebSocket


internal async ValueTask UpdateCacheAsync(ExtendedModel model) internal async ValueTask UpdateCacheAsync(ExtendedModel model)
{ {
await Discord.StateManager.BulkAddOrUpdatePresenceAsync(model.Presences).ConfigureAwait(false);
await Discord.StateManager.BulkAddOrUpdateMembersAsync(Id, model.Members).ConfigureAwait(false);
await Discord.StateManager.PresenceStore.BulkAddOrUpdateAsync(model.Presences);

await Discord.StateManager.UserStore.BulkAddOrUpdateAsync(model.Members.Select(x => x.User));

if(Discord.StateManager.TryGetMemberStore(Id, out var store))
store.BulkAddOrUpdate(model.Members);
} }


internal void Update(ClientStateManager state, EmojiUpdateModel model) internal void Update(ClientStateManager state, EmojiUpdateModel model)
@@ -1055,7 +1059,7 @@ namespace Discord.WebSocket
/// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. /// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found.
/// </returns> /// </returns>
public SocketGuildUser GetUser(ulong id) public SocketGuildUser GetUser(ulong id)
=> Discord.StateManager.GetMember(id, Id);
=> Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(id) : null;
/// <inheritdoc /> /// <inheritdoc />
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null) public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds);
@@ -1064,11 +1068,10 @@ namespace Discord.WebSocket
{ {
SocketGuildUser member; SocketGuildUser member;
if ((member = GetUser(model.Id)) != null) if ((member = GetUser(model.Id)) != null)
member.GlobalUser?.Update(Discord.StateManager, model);
member.Update(model);
else else
{ {
member = SocketGuildUser.Create(Id, Discord, model); member = SocketGuildUser.Create(Id, Discord, model);
member.GlobalUser.AddRef();
DownloadedMemberCount++; DownloadedMemberCount++;
} }
return member; return member;
@@ -1076,12 +1079,11 @@ namespace Discord.WebSocket
internal SocketGuildUser AddOrUpdateUser(MemberModel model) internal SocketGuildUser AddOrUpdateUser(MemberModel model)
{ {
SocketGuildUser member; SocketGuildUser member;
if ((member = GetUser(model.User.Id)) != null)
member.Update(Discord.StateManager, model);
if ((member = GetUser(model.Id)) != null)
member.Update(model);
else else
{ {
member = SocketGuildUser.Create(Id, Discord, model); member = SocketGuildUser.Create(Id, Discord, model);
member.GlobalUser.AddRef();
DownloadedMemberCount++; DownloadedMemberCount++;
} }
return member; return member;
@@ -1092,8 +1094,8 @@ namespace Discord.WebSocket
if ((member = GetUser(id)) != null) if ((member = GetUser(id)) != null)
{ {
DownloadedMemberCount--; DownloadedMemberCount--;
member.GlobalUser.RemoveRef(Discord);
Discord.StateManager.RemoveMember(id, Id);
if (Discord.StateManager.TryGetMemberStore(Id, out var store))
store.Remove(id);
return member; return member;
} }
return null; return null;
@@ -1114,8 +1116,9 @@ namespace Discord.WebSocket
var membersToPurge = users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); var membersToPurge = users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id);
var membersToKeep = users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); var membersToKeep = users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id);


foreach (var member in membersToPurge)
Discord.StateManager.RemoveMember(member.Id, Id);
if(Discord.StateManager.TryGetMemberStore(Id, out var store))
foreach (var member in membersToPurge)
store.Remove(member.Id);


_downloaderPromise = new TaskCompletionSource<bool>(); _downloaderPromise = new TaskCompletionSource<bool>();
DownloadedMemberCount = membersToKeep.Count(); DownloadedMemberCount = membersToKeep.Count();
@@ -1240,7 +1243,6 @@ namespace Discord.WebSocket
/// in order to use this property. /// in order to use this property.
/// </remarks> /// </remarks>
/// </param> /// </param>
/// <param name="speakers">A collection of speakers for the event.</param>
/// <param name="location">The location of the event; links are supported</param> /// <param name="location">The location of the event; links are supported</param>
/// <param name="coverImage">The optional banner image for the event.</param> /// <param name="coverImage">The optional banner image for the event.</param>
/// <param name="options">The options to be used when sending the request.</param> /// <param name="options">The options to be used when sending the request.</param>


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs View File

@@ -89,7 +89,7 @@ namespace Discord.WebSocket
if(guildUser != null) if(guildUser != null)
{ {
if(model.Creator.IsSpecified) if(model.Creator.IsSpecified)
guildUser.Update(Discord.StateManager, model.Creator.Value);
guildUser.Update(model.Creator.Value);


Creator = guildUser; Creator = guildUser;
} }


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs View File

@@ -56,7 +56,7 @@ namespace Discord.WebSocket
if (Channel is SocketGuildChannel channel) if (Channel is SocketGuildChannel channel)
{ {
if (model.Message.Value.WebhookId.IsSpecified) if (model.Message.Value.WebhookId.IsSpecified)
author = SocketWebhookUser.Create(channel.Guild, Discord.StateManager, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value);
author = SocketWebhookUser.Create(channel.Guild, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value);
else if (model.Message.Value.Author.IsSpecified) else if (model.Message.Value.Author.IsSpecified)
author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id);
} }


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs View File

@@ -88,7 +88,7 @@ namespace Discord.WebSocket
if (guild != null) if (guild != null)
{ {
if (msg.Value.WebhookId.IsSpecified) if (msg.Value.WebhookId.IsSpecified)
author = SocketWebhookUser.Create(guild, discord.StateManager, msg.Value.Author.Value, msg.Value.WebhookId.Value);
author = SocketWebhookUser.Create(guild, msg.Value.Author.Value, msg.Value.WebhookId.Value);
else else
author = guild.GetUser(msg.Value.Author.Value.Id); author = guild.GetUser(msg.Value.Author.Value.Id);
} }


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs View File

@@ -251,7 +251,7 @@ namespace Discord.WebSocket
if (user != null) if (user != null)
newMentions.Add(user); newMentions.Add(user);
else else
newMentions.Add(SocketUnknownUser.Create(Discord, state, val));
newMentions.Add(SocketUnknownUser.Create(Discord, val));
} }
} }
_userMentions = newMentions.ToImmutable(); _userMentions = newMentions.ToImmutable();
@@ -263,7 +263,7 @@ namespace Discord.WebSocket
Interaction = new MessageInteraction<SocketUser>(model.Interaction.Value.Id, Interaction = new MessageInteraction<SocketUser>(model.Interaction.Value.Id,
model.Interaction.Value.Type, model.Interaction.Value.Type,
model.Interaction.Value.Name, model.Interaction.Value.Name,
SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User));
SocketGlobalUser.Create(Discord, model.Interaction.Value.User));
} }


if (model.Flags.IsSpecified) if (model.Flags.IsSpecified)


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -122,14 +122,14 @@ namespace Discord.WebSocket
if (guild != null) if (guild != null)
{ {
if (webhookId != null) if (webhookId != null)
refMsgAuthor = SocketWebhookUser.Create(guild, state, refMsg.Author.Value, webhookId.Value);
refMsgAuthor = SocketWebhookUser.Create(guild, refMsg.Author.Value, webhookId.Value);
else else
refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id);
} }
else else
refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id);
if (refMsgAuthor == null) if (refMsgAuthor == null)
refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value);
refMsgAuthor = SocketUnknownUser.Create(Discord, refMsg.Author.Value);
} }
else else
// Message author wasn't specified in the payload, so create a completely anonymous unknown user // Message author wasn't specified in the payload, so create a completely anonymous unknown user


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs View File

@@ -26,11 +26,11 @@ namespace Discord.WebSocket
return entity; return entity;
} }


~SocketGlobalUser() => Discord.StateManager.RemoveReferencedGlobalUser(Id);
~SocketGlobalUser() => Dispose();
public override void Dispose() public override void Dispose()
{ {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
Discord.StateManager.RemoveReferencedGlobalUser(Id);
Discord.StateManager.UserStore.RemoveReference(Id);
} }


private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)";


+ 5
- 5
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -28,7 +28,7 @@ namespace Discord.WebSocket
/// <summary> /// <summary>
/// Gets the guild the user is in. /// Gets the guild the user is in.
/// </summary> /// </summary>
public Lazy<SocketGuild> Guild { get; }
public Lazy<SocketGuild> Guild { get; } // TODO: convert to LazyCached once guilds are cached.
/// <summary> /// <summary>
/// Gets the guilds id that the user is in. /// Gets the guilds id that the user is in.
/// </summary> /// </summary>
@@ -146,7 +146,7 @@ namespace Discord.WebSocket
{ {
var entity = new SocketGuildUser(guildId, model.Id, client); var entity = new SocketGuildUser(guildId, model.Id, client);
if (entity.Update(model)) if (entity.Update(model))
client.StateManager.AddOrUpdateMember(guildId, entity.ToModel());
client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(entity.ToModel());
entity.UpdateRoles(Array.Empty<ulong>()); entity.UpdateRoles(Array.Empty<ulong>());
return entity; return entity;
} }
@@ -154,7 +154,7 @@ namespace Discord.WebSocket
{ {
var entity = new SocketGuildUser(guildId, model.Id, client); var entity = new SocketGuildUser(guildId, model.Id, client);
entity.Update(model); entity.Update(model);
client.StateManager.AddOrUpdateMember(guildId, model);
client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(model);
return entity; return entity;
} }
internal void Update(MemberModel model) internal void Update(MemberModel model)
@@ -301,9 +301,9 @@ namespace Discord.WebSocket
public override void Dispose() public override void Dispose()
{ {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
Discord.StateManager.RemovedReferencedMember(Id, _guildId);
Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id);
} }
~SocketGuildUser() => Discord.StateManager.RemovedReferencedMember(Id, _guildId);
~SocketGuildUser() => Dispose();


#endregion #endregion
} }


+ 30
- 4
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -15,6 +15,8 @@ namespace Discord.WebSocket
{ {
internal ulong UserId; internal ulong UserId;
internal ulong? GuildId; internal ulong? GuildId;
internal bool IsFreed;
internal DiscordSocketClient Discord;


/// <inheritdoc /> /// <inheritdoc />
public UserStatus Status { get; private set; } public UserStatus Status { get; private set; }
@@ -23,17 +25,24 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<IActivity> Activities { get; private set; } public IReadOnlyCollection<IActivity> Activities { get; private set; }


internal SocketPresence() { }
internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities)
public static SocketPresence Default
=> new SocketPresence(null, UserStatus.Offline, null, null);

internal SocketPresence(DiscordSocketClient discord)
{
Discord = discord;
}
internal SocketPresence(DiscordSocketClient discord, UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities)
: this(discord)
{ {
Status = status; Status = status;
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty;
Activities = activities ?? ImmutableList<IActivity>.Empty; Activities = activities ?? ImmutableList<IActivity>.Empty;
} }


internal static SocketPresence Create(Model model)
internal static SocketPresence Create(DiscordSocketClient client, Model model)
{ {
var entity = new SocketPresence();
var entity = new SocketPresence(client);
entity.Update(model); entity.Update(model);
return entity; return entity;
} }
@@ -102,6 +111,22 @@ namespace Discord.WebSocket


internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; internal SocketPresence Clone() => MemberwiseClone() as SocketPresence;


~SocketPresence() => Dispose();

public void Dispose()
{
if (IsFreed)
return;

GC.SuppressFinalize(this);

if(Discord != null)
{
Discord.StateManager.PresenceStore.RemoveReference(UserId);
IsFreed = true;
}
}

#region Cache #region Cache
private struct CacheModel : Model private struct CacheModel : Model
{ {
@@ -205,6 +230,7 @@ namespace Discord.WebSocket
Model ICached<Model>.ToModel() => ToModel(); Model ICached<Model>.ToModel() => ToModel();
TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>(); TResult ICached<Model>.ToModel<TResult>() => ToModel<TResult>();
void ICached<Model>.Update(Model model) => Update(model); void ICached<Model>.Update(Model model) => Update(model);
bool ICached.IsFreed => IsFreed;


#endregion #endregion
} }


+ 5
- 12
src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs View File

@@ -19,17 +19,6 @@ 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; }

/// <inheritdoc />
public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } }
/// <inheritdoc />
public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } }
/// <inheritdoc />
public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } }
/// <inheritdoc />
public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } }
/// <inheritdoc />
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 />
@@ -99,8 +88,12 @@ namespace Discord.WebSocket
internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser;
public override void Dispose() public override void Dispose()
{ {
if (IsFreed)
return;

GC.SuppressFinalize(this); GC.SuppressFinalize(this);
Discord.StateManager.RemoveReferencedGlobalUser(Id);
Discord.StateManager.UserStore.RemoveReference(Id);
IsFreed = true;
} }


#region Cache #region Cache


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs View File

@@ -238,7 +238,7 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.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 LazyCached<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; }


public override void Dispose() public override void Dispose()
{ {


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs View File

@@ -26,8 +26,8 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
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<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } }
internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } }
internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } }


internal SocketUnknownUser(DiscordSocketClient discord, ulong id) internal SocketUnknownUser(DiscordSocketClient discord, ulong id)
: base(discord, id) : base(discord, id)


+ 9
- 7
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -15,7 +15,7 @@ namespace Discord.WebSocket
/// Represents a WebSocket-based user. /// Represents a WebSocket-based user.
/// </summary> /// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>, IDisposable
public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>
{ {
/// <inheritdoc /> /// <inheritdoc />
public virtual bool IsBot { get; internal set; } public virtual bool IsBot { get; internal set; }
@@ -29,9 +29,9 @@ namespace Discord.WebSocket
public virtual bool IsWebhook { get; } public virtual bool IsWebhook { get; }
/// <inheritdoc /> /// <inheritdoc />
public UserProperties? PublicFlags { get; private set; } public UserProperties? PublicFlags { get; private set; }
internal virtual Lazy<SocketGlobalUser> GlobalUser { get; set; }
internal virtual Lazy<SocketPresence> Presence { get; set; }
internal virtual LazyCached<SocketGlobalUser> GlobalUser { get; set; }
internal virtual LazyCached<SocketPresence> Presence { get; set; }
internal bool IsFreed { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc /> /// <inheritdoc />
@@ -56,11 +56,11 @@ namespace Discord.WebSocket
internal SocketUser(DiscordSocketClient discord, ulong id) internal SocketUser(DiscordSocketClient discord, ulong id)
: base(discord, id) : base(discord, id)
{ {
Presence = new LazyCached<SocketPresence>(id, discord.StateManager.PresenceStore);
GlobalUser = new LazyCached<SocketGlobalUser>(id, discord.StateManager.UserStore);
} }
internal virtual bool Update(Model model) internal virtual bool Update(Model model)
{ {
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)
{ {
@@ -124,7 +124,7 @@ namespace Discord.WebSocket
internal SocketUser Clone() => MemberwiseClone() as SocketUser; internal SocketUser Clone() => MemberwiseClone() as SocketUser;


#region Cache #region Cache
private struct CacheModel : Model
private class CacheModel : Model
{ {
public string Username { get; set; } public string Username { get; set; }


@@ -160,6 +160,8 @@ namespace Discord.WebSocket


void ICached<Model>.Update(Model model) => Update(model); void ICached<Model>.Update(Model model) => Update(model);


bool ICached.IsFreed => IsFreed;

#endregion #endregion
} }
} }

+ 2
- 2
src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs View File

@@ -33,8 +33,8 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
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<SocketGlobalUser> GlobalUser { get => new Lazy<SocketGlobalUser>(() => null); set { } }
internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } }
internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } }


internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId)
: base(guild.Discord, id) : base(guild.Discord, id)


Loading…
Cancel
Save