diff --git a/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs index 36d54deccb..61aee35d8b 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs @@ -48,7 +48,7 @@ public override async Task ExecuteCheckAsync(CommandContext ctx, bool help return true; } - Permissions pbot = ctx.Channel.PermissionsFor(bot); + Permissions pbot = await ctx.Channel.PermissionsForMemberAsync(bot); return (pbot & Permissions.Administrator) != 0 ? true : (pbot & this.Permissions) == this.Permissions; } diff --git a/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs index b1108acd63..72bfd4d063 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs @@ -43,7 +43,7 @@ public override async Task ExecuteCheckAsync(CommandContext ctx, bool help return false; } - Permissions pusr = ctx.Channel.PermissionsFor(usr); + Permissions pusr = await ctx.Channel.PermissionsForMemberAsync(usr); DSharpPlus.Entities.DiscordMember bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id); if (bot == null) @@ -51,7 +51,7 @@ public override async Task ExecuteCheckAsync(CommandContext ctx, bool help return false; } - Permissions pbot = ctx.Channel.PermissionsFor(bot); + Permissions pbot = await ctx.Channel.PermissionsForMemberAsync(bot); bool usrok = ctx.Guild.OwnerId == usr.Id; bool botok = ctx.Guild.OwnerId == bot.Id; diff --git a/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs index 3345d88cd1..c129d5e56d 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs @@ -30,28 +30,26 @@ public RequireUserPermissionsAttribute(Permissions permissions, bool ignoreDms = this.IgnoreDms = ignoreDms; } - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + public async override Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (ctx.Guild == null) { - return Task.FromResult(this.IgnoreDms); + return this.IgnoreDms; } DSharpPlus.Entities.DiscordMember? usr = ctx.Member; if (usr == null) { - return Task.FromResult(false); + return false; } if (usr.Id == ctx.Guild.OwnerId) { - return Task.FromResult(true); + return true; } - Permissions pusr = ctx.Channel.PermissionsFor(usr); + Permissions pusr = await ctx.Channel.PermissionsForMemberAsync(usr); - return (pusr & Permissions.Administrator) != 0 - ? Task.FromResult(true) - : (pusr & this.Permissions) == this.Permissions ? Task.FromResult(true) : Task.FromResult(false); + return (pusr & Permissions.Administrator) != 0 || (pusr & this.Permissions) == this.Permissions; } } diff --git a/DSharpPlus.CommandsNext/CommandsNextExtension.cs b/DSharpPlus.CommandsNext/CommandsNextExtension.cs index fcfa0b664d..a34be01869 100644 --- a/DSharpPlus.CommandsNext/CommandsNextExtension.cs +++ b/DSharpPlus.CommandsNext/CommandsNextExtension.cs @@ -131,7 +131,7 @@ internal CommandsNextExtension(CommandsNextConfiguration cfg) Type t = typeof(CommandsNextExtension); IEnumerable ms = t.GetTypeInfo().DeclaredMethods; - MethodInfo? m = ms.FirstOrDefault(xm => xm.Name == nameof(ConvertArgument) && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); + MethodInfo? m = ms.FirstOrDefault(xm => xm.Name == nameof(ConvertArgumentAsync) && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); this.ConvertGeneric = m; } @@ -949,7 +949,7 @@ public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to /// Command to execute. /// Raw arguments to pass to command. /// Created fake context. - public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string? rawArguments = null) + public async Task CreateFakeContextAsync(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string? rawArguments = null) { DateTimeOffset epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -987,7 +987,14 @@ public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channe } else { - mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList(); + foreach (ulong mentionedUserId in Utilities.GetUserMentions(msg)) + { + DiscordUser? User = await this.Client.TryGetCachedUserInternalAsync(mentionedUserId); + if (User is not null) + { + mentionedUsers.Add(User); + } + } } } @@ -1026,7 +1033,7 @@ public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channe /// Value to convert. /// Context in which to convert to. /// Converted object. - public async Task ConvertArgument(string value, CommandContext ctx) + public async Task ConvertArgumentAsync(string value, CommandContext ctx) { Type t = typeof(T); if (!this.ArgumentConverters.ContainsKey(t)) @@ -1050,7 +1057,7 @@ public async Task ConvertArgument(string value, CommandContext ctx) /// Context in which to convert to. /// Type to convert to. /// Converted object. - public async Task ConvertArgument(string? value, CommandContext ctx, Type type) + public async Task ConvertArgumentAsync(string? value, CommandContext ctx, Type type) { MethodInfo m = this.ConvertGeneric.MakeGenericMethod(type); try diff --git a/DSharpPlus.CommandsNext/CommandsNextUtilities.cs b/DSharpPlus.CommandsNext/CommandsNextUtilities.cs index ff4369b027..208fbc9eab 100644 --- a/DSharpPlus.CommandsNext/CommandsNextUtilities.cs +++ b/DSharpPlus.CommandsNext/CommandsNextUtilities.cs @@ -269,7 +269,7 @@ internal static async Task BindArgumentsAsync(CommandCont { try { - array.SetValue(await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type), i - start); + array.SetValue(await ctx.CommandsNext.ConvertArgumentAsync(rawArgumentList[i], ctx, arg.Type), i - start); } catch (Exception ex) { @@ -285,7 +285,7 @@ internal static async Task BindArgumentsAsync(CommandCont { try { - args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type) : arg.DefaultValue; + args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgumentAsync(rawArgumentList[i], ctx, arg.Type) : arg.DefaultValue; } catch (Exception ex) { diff --git a/DSharpPlus.CommandsNext/Converters/EntityConverters.cs b/DSharpPlus.CommandsNext/Converters/EntityConverters.cs index e0218cbde6..67cca67017 100644 --- a/DSharpPlus.CommandsNext/Converters/EntityConverters.cs +++ b/DSharpPlus.CommandsNext/Converters/EntityConverters.cs @@ -36,7 +36,7 @@ async Task> IArgumentConverter.ConvertAsync(s string un = di != -1 ? value.Substring(0, di) : value; string? dv = di != -1 ? value.Substring(di + 1) : null; - System.Collections.Generic.IEnumerable us = ctx.Client.Guilds.Values + System.Collections.Generic.IEnumerable us = ctx.Client._guildIds.Values .SelectMany(xkvp => xkvp.Members.Values).Where(xm => xm.Username.Equals(un, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase) && ((dv != null && xm.Discriminator == dv) || dv == null)); @@ -137,14 +137,14 @@ Task> IArgumentConverter.Co { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong threadId)) { - DiscordThreadChannel result = ctx.Client.InternalGetCachedThread(threadId); + DiscordThreadChannel result = ctx.Client.InternalGetCachedThreadAsync(threadId); return Task.FromResult(result != null ? Optional.FromValue(result) : Optional.FromNoValue()); } Match m = ThreadRegex.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out threadId)) { - DiscordThreadChannel result = ctx.Client.InternalGetCachedThread(threadId); + DiscordThreadChannel result = ctx.Client.InternalGetCachedThreadAsync(threadId); return Task.FromResult(result != null ? Optional.FromValue(result) : Optional.FromNoValue()); } @@ -199,14 +199,14 @@ Task> IArgumentConverter.ConvertAsync(strin { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong gid)) { - return ctx.Client.Guilds.TryGetValue(gid, out DiscordGuild? result) + return ctx.Client._guildIds.TryGetValue(gid, out DiscordGuild? result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); } bool cs = ctx.Config.CaseSensitive; - DiscordGuild? gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => + DiscordGuild? gld = ctx.Client._guildIds.Values.FirstOrDefault(xg => xg.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); return Task.FromResult(gld != null ? Optional.FromValue(gld) : Optional.FromNoValue()); } @@ -286,7 +286,7 @@ Task> IArgumentConverter.ConvertAsync(strin return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong id) ? Task.FromResult(Optional.FromNoValue()) - : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) + : DiscordEmoji.TryFromGuildEmoteAsync(ctx.Client, id, out emoji) ? Task.FromResult(Optional.FromValue(emoji)) : Task.FromResult(Optional.FromValue(new DiscordEmoji { diff --git a/DSharpPlus.Rest/DiscordRestClient.cs b/DSharpPlus.Rest/DiscordRestClient.cs index 0888315a0b..364aca89ea 100644 --- a/DSharpPlus.Rest/DiscordRestClient.cs +++ b/DSharpPlus.Rest/DiscordRestClient.cs @@ -11,17 +11,10 @@ namespace DSharpPlus; +using Caching; + public class DiscordRestClient : BaseDiscordClient { - /// - /// Gets the dictionary of guilds cached by this client. - /// - public override IReadOnlyDictionary Guilds - => this._guilds_lazy.Value; - - internal Dictionary _guilds = new(); - private Lazy> _guilds_lazy; - public DiscordRestClient(DiscordConfiguration config) : base(config) => this._disposed = false; /// @@ -31,11 +24,11 @@ public override IReadOnlyDictionary Guilds public async Task InitializeCacheAsync() { await base.InitializeAsync(); - this._guilds_lazy = new Lazy>(() => new ReadOnlyDictionary(this._guilds)); IReadOnlyList gs = await this.ApiClient.GetCurrentUserGuildsAsync(100, null, null); - foreach (DiscordGuild g in gs) + foreach (DiscordGuild guild in gs) { - this._guilds[g.Id] = g; + this._guildIds.Add(guild.Id); + await this.Cache.AddGuildAsync(guild); } } @@ -412,7 +405,7 @@ public async Task> ListGuildMembersAsync(ulong guil Discord = this }; - this.UpdateUserCache(usr); + await this.Cache.AddUserAsync(usr); } recmbr.AddRange(tms.Select(xtm => new DiscordMember(xtm) { Discord = this, _guild_id = guild_id })); @@ -2312,7 +2305,6 @@ public override void Dispose() } this._disposed = true; - this._guilds = null; this.ApiClient?._rest?.Dispose(); } } diff --git a/DSharpPlus.VoiceNext/VoiceNextConnection.cs b/DSharpPlus.VoiceNext/VoiceNextConnection.cs index 5aec987b00..a382c77e03 100644 --- a/DSharpPlus.VoiceNext/VoiceNextConnection.cs +++ b/DSharpPlus.VoiceNext/VoiceNextConnection.cs @@ -960,7 +960,7 @@ private async Task HandleDispatch(JObject jo) // No longer spam, Discord supposedly doesn't send many of these this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SPEAKING (OP5)"); VoiceSpeakingPayload spd = opp.ToDiscordObject(); - bool foundUserInCache = this.Discord.TryGetCachedUserInternal(spd.UserId.Value, out DiscordUser? resolvedUser); + bool foundUserInCache = this.Discord.TryGetCachedUserInternalAsync(spd.UserId.Value, out DiscordUser? resolvedUser); UserSpeakingEventArgs spk = new UserSpeakingEventArgs { Speaking = spd.Speaking, diff --git a/DSharpPlus/Caching/CacheConfiguration.cs b/DSharpPlus/Caching/CacheConfiguration.cs new file mode 100644 index 0000000000..8bc133255f --- /dev/null +++ b/DSharpPlus/Caching/CacheConfiguration.cs @@ -0,0 +1,22 @@ +namespace DSharpPlus.Caching; + +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +/// +/// Configuration for the cache how long to keep items in the cache. This configuration is only used by and is ignored if you provide your own implementation of . +/// +/// The default implementation of only supports timebased expiration +public record CacheConfiguration +{ + public Dictionary MemoryCacheEntryOptions { get; set; } = new() + { + {typeof(DiscordGuild), new CacheEntryOptions(){SlidingExpiration = TimeSpan.FromMinutes(60)}}, + {typeof(DiscordChannel), new CacheEntryOptions(){SlidingExpiration = TimeSpan.FromMinutes(60)}}, + {typeof(DiscordMessage), new CacheEntryOptions(){SlidingExpiration = TimeSpan.FromMinutes(60)}}, + {typeof(DiscordMember), new CacheEntryOptions(){SlidingExpiration = TimeSpan.FromMinutes(60)}}, + {typeof(DiscordUser), new CacheEntryOptions(){SlidingExpiration = TimeSpan.FromMinutes(60)}}, + {typeof(DiscordPresence), new CacheEntryOptions(){SlidingExpiration = TimeSpan.FromMinutes(60)}} + }; +} diff --git a/DSharpPlus/Caching/CacheEntryOptions.cs b/DSharpPlus/Caching/CacheEntryOptions.cs new file mode 100644 index 0000000000..77a72f58f6 --- /dev/null +++ b/DSharpPlus/Caching/CacheEntryOptions.cs @@ -0,0 +1,54 @@ +namespace DSharpPlus.Caching; + +using System; +using Microsoft.Extensions.Caching.Memory; + +public class CacheEntryOptions +{ + /// + /// Sliding expiration for the cache entry + /// + /// + public TimeSpan? SlidingExpiration { get; set; } + + + /// + /// Absolute expiration for the cache entry relative to creation + /// + /// + + public TimeSpan? AbsoluteExpirationRelativeToCreation { get; set; } + + + internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions() + { + if (this.AbsoluteExpirationRelativeToCreation is null && this.SlidingExpiration is null) + { + throw new ArgumentException("Either SlidingExpiration or AbsoluteExpirationRelativeToCreation must be set"); + } + + if + ( + this.AbsoluteExpirationRelativeToCreation is not null && + this.SlidingExpiration is not null && + this.AbsoluteExpirationRelativeToCreation < this.SlidingExpiration + ) + { + throw new ArgumentException("AbsoluteExpirationRelativeToCreation must be greater than SlidingExpiration"); + } + + MemoryCacheEntryOptions options = new(); + + if (this.AbsoluteExpirationRelativeToCreation is not null) + { + options.AbsoluteExpirationRelativeToNow = this.AbsoluteExpirationRelativeToCreation; + } + + if (this.SlidingExpiration is not null) + { + options.SlidingExpiration = this.SlidingExpiration; + } + + return options; + } +} diff --git a/DSharpPlus/Caching/CacheExtensionMethods.cs b/DSharpPlus/Caching/CacheExtensionMethods.cs new file mode 100644 index 0000000000..7ca4d13e1f --- /dev/null +++ b/DSharpPlus/Caching/CacheExtensionMethods.cs @@ -0,0 +1,83 @@ +namespace DSharpPlus.Caching; + +using System.Threading.Tasks; +using Entities; + +internal static class CacheExtensionMethods +{ + /// + /// Adds a new entity to the cache, if it is not already present. + /// + /// Cache which should be used + /// Entity which will be added + /// Key which is used to see if its present and which will be the key for the new entity + /// Type of + internal static async ValueTask AddIfNotPresentAsync(this IDiscordCache cache, T entity, ICacheKey key) + { + T? cachedEntity = await cache.TryGet(key); + if (cachedEntity is null) + { + await cache.Set(entity, key); + } + } + + /// + /// Adds a new user to cache and overwrites the old one if it is already present. + /// + /// + /// + internal static async ValueTask AddUserAsync(this IDiscordCache cache, DiscordUser newUser) => + await cache.Set(newUser, newUser.GetCacheKey()); + + internal static async ValueTask AddUserPresenceAsync(this IDiscordCache cache, DiscordPresence newPresence) => + await cache.Set(newPresence, newPresence.GetCacheKey()); + + internal static async ValueTask AddGuildAsync(this IDiscordCache cache, DiscordGuild newGuild) => + await cache.Set(newGuild, newGuild.GetCacheKey()); + + internal static async ValueTask AddChannelAsync(this IDiscordCache cache, DiscordChannel newChannel) => + await cache.Set(newChannel, newChannel.GetCacheKey()); + + internal static async ValueTask AddMemberAsync(this IDiscordCache cache, DiscordMember newMember) => + await cache.Set(newMember, newMember.GetCacheKey()); + + internal static async ValueTask AddMessageAsync(this IDiscordCache cache, DiscordMessage newMessage) => + await cache.Set(newMessage, newMessage.GetCacheKey()); + + internal static async ValueTask TryGetUserAsync(this IDiscordCache cache, ulong userId) => + await cache.TryGet(ICacheKey.ForUser(userId)); + + internal static async ValueTask TryGetUserPresenceAsync(this IDiscordCache cache, ulong userId) => + await cache.TryGet(ICacheKey.ForUser(userId)); + + internal static async ValueTask TryGetGuildAsync(this IDiscordCache cache, ulong guildId) => + await cache.TryGet(ICacheKey.ForGuild(guildId)); + + internal static async ValueTask TryGetChannelAsync(this IDiscordCache cache, ulong channelId) => + await cache.TryGet(ICacheKey.ForChannel(channelId)); + + internal static async ValueTask TryGetMemberAsync(this IDiscordCache cache, ulong memberId, + ulong guildId) => await cache.TryGet(ICacheKey.ForMember(memberId, guildId)); + + internal static async ValueTask TryGetMessageAsync(this IDiscordCache cache, ulong messageId) => + await cache.TryGet(ICacheKey.ForMessage(messageId)); + + internal static async ValueTask RemoveUserAsync(this IDiscordCache cache, ulong userId) => + await cache.Remove(ICacheKey.ForUser(userId)); + + internal static async ValueTask RemoveUserPresenceAsync(this IDiscordCache cache, ulong userId) => + await cache.Remove(ICacheKey.ForUser(userId)); + + internal static async ValueTask RemoveGuildAsync(this IDiscordCache cache, ulong guildId) => + await cache.Remove(ICacheKey.ForGuild(guildId)); + + internal static async ValueTask RemoveChannelAsync(this IDiscordCache cache, ulong channelId) => + await cache.Remove(ICacheKey.ForChannel(channelId)); + + internal static async ValueTask RemoveMemberAsync(this IDiscordCache cache, ulong memberId, ulong guildId) => + await cache.Remove(ICacheKey.ForMember(memberId, guildId)); + + internal static async ValueTask RemoveMessageAsync(this IDiscordCache cache, ulong messageId) => + await cache.Remove(ICacheKey.ForMessage(messageId)); + +} diff --git a/DSharpPlus/Caching/CacheKeys/CacheKeyExtensions.cs b/DSharpPlus/Caching/CacheKeys/CacheKeyExtensions.cs new file mode 100644 index 0000000000..acd6100a39 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/CacheKeyExtensions.cs @@ -0,0 +1,18 @@ +using DSharpPlus.Entities; + +namespace DSharpPlus.Caching; + +public static class CacheKeyExtensions +{ + public static GuildCacheKey GetCacheKey(this DiscordGuild guild) => new(guild.Id); + + public static ChannelCacheKey GetCacheKey(this DiscordChannel channel) => new(channel.Id); + + public static MessageCacheKey GetCacheKey(this DiscordMessage message) => new(message.Id); + + public static MemberCacheKey GetCacheKey(this DiscordMember member) => new(member.Id, member._guild_id); + + public static UserCacheKey GetCacheKey(this DiscordUser user) => new(user.Id); + + public static UserPresenceCacheKey GetCacheKey(this DiscordPresence presence) => new(presence.UserId); +} diff --git a/DSharpPlus/Caching/CacheKeys/ChannelCacheKey.cs b/DSharpPlus/Caching/CacheKeys/ChannelCacheKey.cs new file mode 100644 index 0000000000..38010e73b1 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/ChannelCacheKey.cs @@ -0,0 +1,7 @@ +namespace DSharpPlus.Caching; + +public readonly record struct ChannelCacheKey(ulong Id) : ICacheKey +{ + //key format is "{keyPrefix}-channel-{channelId}" + public override string ToString() => $"{ICacheKey.KeyPrefix}-channel-{this.Id}"; +} diff --git a/DSharpPlus/Caching/CacheKeys/GuildCacheKey.cs b/DSharpPlus/Caching/CacheKeys/GuildCacheKey.cs new file mode 100644 index 0000000000..8dbd066982 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/GuildCacheKey.cs @@ -0,0 +1,7 @@ +namespace DSharpPlus.Caching; + +public readonly record struct GuildCacheKey(ulong Id) : ICacheKey +{ + //key format is "{keyPrefix}-guild-{guildId}" + public override string ToString() => $"{ICacheKey.KeyPrefix}-guild-{this.Id}"; +} diff --git a/DSharpPlus/Caching/CacheKeys/ICacheKey.cs b/DSharpPlus/Caching/CacheKeys/ICacheKey.cs new file mode 100644 index 0000000000..b01cb76074 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/ICacheKey.cs @@ -0,0 +1,16 @@ +namespace DSharpPlus.Caching; + +/// +/// +/// +public interface ICacheKey +{ + public const string KeyPrefix = "dsharpplus"; + + public static ICacheKey ForGuild(ulong id) => new GuildCacheKey(id); + public static ICacheKey ForChannel(ulong id) => new ChannelCacheKey(id); + public static ICacheKey ForMessage(ulong id) => new MessageCacheKey(id); + public static ICacheKey ForMember(ulong id, ulong guildId) => new MemberCacheKey(id, guildId); + public static ICacheKey ForUser(ulong id) => new UserCacheKey(id); + public static ICacheKey ForUserPresence(ulong id) => new UserPresenceCacheKey(id); +} diff --git a/DSharpPlus/Caching/CacheKeys/MemberCacheKey.cs b/DSharpPlus/Caching/CacheKeys/MemberCacheKey.cs new file mode 100644 index 0000000000..058e84b8c7 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/MemberCacheKey.cs @@ -0,0 +1,7 @@ +namespace DSharpPlus.Caching; + +public readonly record struct MemberCacheKey(ulong Id, ulong GuildId) : ICacheKey +{ + //key format is "{keyPrefix}-guild-{this.GuildId}-member-{this.Id}" + public override string ToString() => $"{ICacheKey.KeyPrefix}-guild-{this.GuildId}-member-{this.Id}"; +} diff --git a/DSharpPlus/Caching/CacheKeys/MessageCacheKey.cs b/DSharpPlus/Caching/CacheKeys/MessageCacheKey.cs new file mode 100644 index 0000000000..651a2e1412 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/MessageCacheKey.cs @@ -0,0 +1,7 @@ +namespace DSharpPlus.Caching; + +public readonly record struct MessageCacheKey(ulong Id) : ICacheKey +{ + //key format is "{keyPrefix}-message-{messageId}" + public override string ToString() => $"{ICacheKey.KeyPrefix}-message-{this.Id}"; +} diff --git a/DSharpPlus/Caching/CacheKeys/UserCacheKey.cs b/DSharpPlus/Caching/CacheKeys/UserCacheKey.cs new file mode 100644 index 0000000000..e017c6b6c3 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/UserCacheKey.cs @@ -0,0 +1,6 @@ +namespace DSharpPlus.Caching; + +public readonly record struct UserCacheKey(ulong Id) : ICacheKey +{ + public override string ToString() => $"{ICacheKey.KeyPrefix}-user-{this.Id}"; +} diff --git a/DSharpPlus/Caching/CacheKeys/UserPresenceCacheKey.cs b/DSharpPlus/Caching/CacheKeys/UserPresenceCacheKey.cs new file mode 100644 index 0000000000..3614eb61d1 --- /dev/null +++ b/DSharpPlus/Caching/CacheKeys/UserPresenceCacheKey.cs @@ -0,0 +1,6 @@ +namespace DSharpPlus.Caching; + +public readonly record struct UserPresenceCacheKey(ulong Id) : ICacheKey +{ + public override string ToString() => $"{ICacheKey.KeyPrefix}-userPresence-{this.Id}"; +} diff --git a/DSharpPlus/Caching/CachedEntity.cs b/DSharpPlus/Caching/CachedEntity.cs new file mode 100644 index 0000000000..6fc8cf082d --- /dev/null +++ b/DSharpPlus/Caching/CachedEntity.cs @@ -0,0 +1,25 @@ +namespace DSharpPlus.Caching; + +using System.Diagnostics.CodeAnalysis; + +public readonly record struct CachedEntity where TValue : class +{ + public TKey Key { get; } + + //private to encourage a more responsible usage + private TValue? Value { get; } = null; + + public CachedEntity(TKey key, TValue? value) + { + Key = key; + Value = value; + } + + public bool HasCachedValue => Value is not null; + + public bool TryGetCachedValue([NotNullWhen(true)] out TValue? value) + { + value = Value; + return HasCachedValue; + } +} diff --git a/DSharpPlus/Caching/DiscordMemoryCache.cs b/DSharpPlus/Caching/DiscordMemoryCache.cs new file mode 100644 index 0000000000..9af9c3ed74 --- /dev/null +++ b/DSharpPlus/Caching/DiscordMemoryCache.cs @@ -0,0 +1,66 @@ +namespace DSharpPlus.Caching; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +public class DiscordMemoryCache : IDiscordCache +{ + private readonly MemoryCache _cache = new(new MemoryCacheOptions()); + + private readonly Dictionary _memoryCacheEntryOptions; + + public DiscordMemoryCache(CacheConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + this._memoryCacheEntryOptions = configuration.MemoryCacheEntryOptions; + } + + public ValueTask Set(T entity, ICacheKey key) + { + if (entity is null) + { + return default; + } + ArgumentNullException.ThrowIfNull(key); + + KeyValuePair? first = new(); + foreach (KeyValuePair pair in this._memoryCacheEntryOptions) + { + if(!entity.GetType().IsAssignableFrom(pair.Key)) + { + continue; + } + first = pair; + break; + } + CacheEntryOptions? entryOptions = first?.Value; + + if (entryOptions is null) + { + return default; + } + + this._cache.Set(key, entity, entryOptions.ToMemoryCacheEntryOptions()); + return default; + } + + public ValueTask Remove(ICacheKey key) + { + ArgumentNullException.ThrowIfNull(key); + + this._cache.Remove(key); + return ValueTask.CompletedTask; + } + + public ValueTask TryGet(ICacheKey key) + { + ArgumentNullException.ThrowIfNull(key); + + return ValueTask.FromResult(this._cache.Get(key)); + } + + public void Dispose() => this._cache.Dispose(); +} diff --git a/DSharpPlus/Caching/IDiscordCache.cs b/DSharpPlus/Caching/IDiscordCache.cs new file mode 100644 index 0000000000..eb0859d0f6 --- /dev/null +++ b/DSharpPlus/Caching/IDiscordCache.cs @@ -0,0 +1,29 @@ +namespace DSharpPlus.Caching; + +using System; +using System.Threading.Tasks; + +public interface IDiscordCache : IDisposable +{ + /// + /// Add entity of type T to the cache and overwrite the old one if it is already present + /// + /// Entity to cache + /// + public ValueTask Set(T entity, ICacheKey key); + + /// + /// Remove entity with given key from the cache + /// + /// + public ValueTask Remove(ICacheKey key); + + /// + /// Tries to get a cached entity of type T with given key + /// + /// + /// + /// + /// Returns the entity if present otherwise returns null + public ValueTask TryGet(ICacheKey key); +} diff --git a/DSharpPlus/Caching/WeakDictionary.cs b/DSharpPlus/Caching/WeakDictionary.cs new file mode 100644 index 0000000000..97079f0c2c --- /dev/null +++ b/DSharpPlus/Caching/WeakDictionary.cs @@ -0,0 +1,89 @@ +namespace DSharpPlus.Caching; + +using System; +using System.Collections.Generic; +using System.Linq; + +public class WeakDictionary where TValue : class +{ + private List>> _list = new(); + + public void Clear() => this._list.Clear(); + + public void Add(TKey key, TValue value) + { + WeakReference item = new(value, false); + KeyValuePair> newPair = new(key, item); + foreach (KeyValuePair> pair in this._list) + { + if (pair.Key != null && pair.Key.Equals(key)) + { + this._list.Remove(pair); + this._list.Add(newPair); + return; + } + } + + this._list.Add(newPair); + } + + public bool ContainsKey(TKey key) + { + foreach (KeyValuePair> pair in this._list) + { + if (pair.Key != null && pair.Key.Equals(key)) + { + if (pair.Value.TryGetTarget(out _) == false) + { + this._list.Remove(pair); + return false; + } + return true; + } + } + + return false; + } + + public bool Remove(TKey key) + { + foreach (KeyValuePair> pair in this._list) + { + if (pair.Key != null && Equals(pair.Key, key)) + { + this._list.Remove(pair); + return true; + } + } + + return false; + } + + public bool TryGetValue(TKey key, out TValue? value) + { + foreach (KeyValuePair> pair in this._list) + { + if (pair.Key != null && pair.Key.Equals(key)) + { + if (pair.Value.TryGetTarget(out value) == false) + { + this._list.Remove(pair); + return false; + } + return true; + } + } + + value = null; + return false; + } + + public IEnumerable Keys => this._list.Select(x => x.Key); + + public IEnumerable Values => this._list.Select(x => + { + x.Value.TryGetTarget(out TValue? xValue); + return xValue; + }) + .Where(x => x is not null)!; +} diff --git a/DSharpPlus/Clients/BaseDiscordClient.cs b/DSharpPlus/Clients/BaseDiscordClient.cs index ea6a632433..0260519324 100644 --- a/DSharpPlus/Clients/BaseDiscordClient.cs +++ b/DSharpPlus/Clients/BaseDiscordClient.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using DSharpPlus.Caching; using DSharpPlus.Entities; using DSharpPlus.Net; using Microsoft.Extensions.Logging; @@ -40,16 +41,15 @@ public abstract class BaseDiscordClient : IDisposable /// public DiscordApplication CurrentApplication { get; internal set; } - /// - /// Gets the cached guilds for this client. - /// - public abstract IReadOnlyDictionary Guilds { get; } + public IReadOnlyCollection Guilds => new ReadOnlyCollection(this._guildIds); /// - /// Gets the cached users for this client. + /// Gets the cached guilds for this client. /// - protected internal ConcurrentDictionary UserCache { get; } - + internal List _guildIds { get; set; } + + public IDiscordCache Cache { get; } + /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// @@ -66,19 +66,19 @@ public IReadOnlyDictionary VoiceRegions /// Initializes this Discord API client. /// /// Configuration for this client. - internal BaseDiscordClient(DiscordConfiguration config, RestClient? rest_client = null) + /// Rest client to use for this client. + internal BaseDiscordClient(DiscordConfiguration config, RestClient? restClient = null) { this.Configuration = new DiscordConfiguration(config); - if (this.Configuration.LoggerFactory == null) + if (this.Configuration.LoggerFactory is null) { this.Configuration.LoggerFactory = new DefaultLoggerFactory(); this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this)); } this.Logger = this.Configuration.LoggerFactory.CreateLogger(); - this.ApiClient = new DiscordApiClient(this, rest_client); - this.UserCache = new ConcurrentDictionary(); + this.ApiClient = new DiscordApiClient(this, restClient); this.InternalVoiceRegions = new ConcurrentDictionary(); this._voice_regions_lazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions)); @@ -99,6 +99,10 @@ internal BaseDiscordClient(DiscordConfiguration config, RestClient? rest_client this.VersionString = $"{vs}, CI build {v.Revision}"; } } + + this.Cache = this.Configuration.CacheProvider is not null + ? this.Configuration.CacheProvider + : new DiscordMemoryCache(this.Configuration.CacheConfiguration); } /// @@ -174,7 +178,7 @@ public virtual async Task InitializeAsync() if (this.CurrentUser == null) { this.CurrentUser = await this.ApiClient.GetCurrentUserAsync(); - this.UpdateUserCache(this.CurrentUser); + await this.Cache.AddUserAsync(this.CurrentUser); } if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null) @@ -220,32 +224,124 @@ public async Task GetGatewayInfoAsync(string token = null) return await this.ApiClient.GetGatewayInfoAsync(); } - - internal DiscordUser GetCachedOrEmptyUserInternal(ulong user_id) + + internal async ValueTask TryGetCachedUserInternalAsync(ulong userId) { - this.TryGetCachedUserInternal(user_id, out DiscordUser? user); + ICacheKey key = ICacheKey.ForUser(userId); + + DiscordUser? user = await this.Cache.TryGet(key); + return user; } - internal bool TryGetCachedUserInternal(ulong user_id, out DiscordUser user) + #region Cached requests + + /// + /// Gets a user + /// + /// ID of the user + /// Whether to always make a REST request and update cache. Passing true will update the user, updating stale properties such as . + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async ValueTask GetUserAsync(ulong userId, bool skipCache = false) { - if (this.UserCache.TryGetValue(user_id, out user)) + if (!skipCache) { - return true; + DiscordUser? cachedUser = await this.TryGetCachedUserInternalAsync(userId); + if (cachedUser is not null) + { + return cachedUser; + } } - user = new DiscordUser { Id = user_id, Discord = this }; - return false; + DiscordUser usr = await this.ApiClient.GetUserAsync(userId); + + await this.Cache.Set(usr, usr.GetCacheKey()); + + return usr; } - // This previously set properties on the old user and re-injected into the cache. - // That's terrible. Instead, insert the new reference and let the old one get GC'd. - // End-users are more likely to be holding a reference to the new object via an event or w/e - // anyways. - // Furthermore, setting properties requires keeping track of where we update cache and updating repeat code. - internal DiscordUser UpdateUserCache(DiscordUser newUser) - => this.UserCache.AddOrUpdate(newUser.Id, newUser, (_, _) => newUser); + /// + /// Gets a channel + /// + /// The ID of the channel to get. + /// + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async ValueTask GetChannelAsync(ulong id, bool skipCache = false) + { + if (skipCache) + { + return await this.ApiClient.GetChannelAsync(id); + } + + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(id); + if (channel is not null) + { + return channel; + } + + return await this.ApiClient.GetChannelAsync(id); + } + + /// + /// Gets a guild. + /// Setting to true will always make a REST request. + /// + /// The guild ID to search for. + /// Whether to include guild channels. This will add an additional REST request + /// Whether to include approximate presence and member counts in the returned guild. + /// Whether to skip the cache and always excute a REST request + /// The requested Guild. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async ValueTask GetGuildAsync(ulong id, bool withChannels = true, bool? withCounts = null, bool skipCache = false) + { + DiscordGuild guild; + IReadOnlyList channels; + if (skipCache) + { + guild = await this.ApiClient.GetGuildAsync(id, withCounts); + + if (withChannels) + { + channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id); + foreach (DiscordChannel channel in channels) + { + guild._channels[channel.Id] = channel; + } + } + } + + if (!withCounts.HasValue || !withCounts.Value || !withChannels) + { + DiscordGuild? cachedGuild = await this.Cache.TryGetGuildAsync(id); + if (cachedGuild is not null) + { + return cachedGuild; + } + } + + guild = await this.ApiClient.GetGuildAsync(id, withCounts); + if (withChannels) + { + channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id); + foreach (DiscordChannel channel in channels) + { + guild._channels[channel.Id] = channel; + } + } + + return guild; + } + + #endregion + + /// /// Disposes this client. /// diff --git a/DSharpPlus/Clients/DiscordClient.Dispatch.cs b/DSharpPlus/Clients/DiscordClient.Dispatch.cs index ab9696646e..c2fec8ec81 100644 --- a/DSharpPlus/Clients/DiscordClient.Dispatch.cs +++ b/DSharpPlus/Clients/DiscordClient.Dispatch.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DSharpPlus.Caching; using DSharpPlus.Entities; using DSharpPlus.Entities.AuditLogs; using DSharpPlus.EventArgs; @@ -23,6 +23,8 @@ public sealed partial class DiscordClient private string _sessionId; private bool _guildDownloadCompleted = false; + private List _readyGuilds = new(); + private int _readyGuildCount = 0; #endregion @@ -30,22 +32,24 @@ public sealed partial class DiscordClient internal async Task HandleDispatchAsync(GatewayPayload payload) { - if (payload.Data is not JObject dat) + if (payload.Data is not JObject payloadData) { - this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {Op} event: {Event}; payload: {Payload}", payload.OpCode, payload.EventName, payload.Data); + this.Logger.LogWarning(LoggerEvents.WebSocketReceive, + "Invalid payload body (this message is probably safe to ignore); opcode: {Op} event: {Event}; payload: {Payload}", + payload.OpCode, payload.EventName, payload.Data); return; } - DiscordChannel chn; + DiscordChannel channel; DiscordThreadChannel thread; - ulong gid; - ulong cid; - TransportUser usr = default; - TransportMember mbr = default; - TransportUser refUsr = default; - TransportMember refMbr = default; - JToken rawMbr = default; - JToken? rawRefMsg = dat["referenced_message"]; + ulong guildId; + ulong channelId; + TransportUser transportUser = default; + TransportMember transportMember = default; + TransportUser referencedUser = default; + TransportMember referencedMember = default; + JToken rawMember = default; + JToken? rawRefMessage = payloadData["referenced_message"]; JArray rawMembers = default; JArray rawPresences = default; @@ -54,13 +58,13 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Gateway Status case "ready": - JArray? glds = (JArray)dat["guilds"]; - JArray? dmcs = (JArray)dat["private_channels"]; + JArray? rawGuilds = (JArray)payloadData["guilds"]; + JArray? rawDmChannels = (JArray)payloadData["private_channels"]; - dat.Remove("guilds"); - dat.Remove("private_channels"); + payloadData.Remove("guilds"); + payloadData.Remove("private_channels"); - await this.OnReadyEventAsync(dat.ToDiscordObject(), glds, dmcs); + await this.OnReadyEventAsync(payloadData.ToDiscordObject(), rawGuilds, rawDmChannels); break; case "resumed": @@ -72,25 +76,31 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Channel case "channel_create": - chn = dat.ToDiscordObject(); - await this.OnChannelCreateEventAsync(chn); + channel = payloadData.ToDiscordObject(); + await this.OnChannelCreateEventAsync(channel); break; case "channel_update": - await this.OnChannelUpdateEventAsync(dat.ToDiscordObject()); + await this.OnChannelUpdateEventAsync(payloadData.ToDiscordObject()); break; case "channel_delete": - bool isPrivate = dat["is_private"]?.ToObject() ?? false; + bool isPrivate = payloadData["is_private"]?.ToObject() ?? false; - chn = isPrivate ? dat.ToDiscordObject() : dat.ToDiscordObject(); - await this.OnChannelDeleteEventAsync(chn); + channel = isPrivate + ? payloadData.ToDiscordObject() + : payloadData.ToDiscordObject(); + await this.OnChannelDeleteEventAsync(channel); break; case "channel_pins_update": - cid = (ulong)dat["channel_id"]; - string? ts = (string)dat["last_pin_timestamp"]; - await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)); + ulong? optionalGuildId = (ulong?)payloadData["guild_id"]; + channelId = (ulong)payloadData["channel_id"]; + string? ts = (string)payloadData["last_pin_timestamp"]; + DateTimeOffset? lastPinTimestamp = ts != null + ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) + : default(DateTimeOffset?); + await this.OnChannelPinsUpdateAsync(optionalGuildId, channelId, lastPinTimestamp); break; #endregion @@ -98,83 +108,80 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Scheduled Guild Events case "guild_scheduled_event_create": - DiscordScheduledGuildEvent cevt = dat.ToDiscordObject(); + DiscordScheduledGuildEvent cevt = payloadData.ToDiscordObject(); await this.OnScheduledGuildEventCreateEventAsync(cevt); break; case "guild_scheduled_event_delete": - DiscordScheduledGuildEvent devt = dat.ToDiscordObject(); + DiscordScheduledGuildEvent devt = payloadData.ToDiscordObject(); await this.OnScheduledGuildEventDeleteEventAsync(devt); break; case "guild_scheduled_event_update": - DiscordScheduledGuildEvent uevt = dat.ToDiscordObject(); + DiscordScheduledGuildEvent uevt = payloadData.ToDiscordObject(); await this.OnScheduledGuildEventUpdateEventAsync(uevt); break; case "guild_scheduled_event_user_add": - gid = (ulong)dat["guild_id"]; - ulong uid = (ulong)dat["user_id"]; - ulong eid = (ulong)dat["guild_scheduled_event_id"]; - await this.OnScheduledGuildEventUserAddEventAsync(gid, eid, uid); + guildId = (ulong)payloadData["guild_id"]; + ulong userId = (ulong)payloadData["user_id"]; + ulong eventId = (ulong)payloadData["guild_scheduled_event_id"]; + await this.OnScheduledGuildEventUserAddEventAsync(guildId, eventId, userId); break; case "guild_scheduled_event_user_remove": - gid = (ulong)dat["guild_id"]; - uid = (ulong)dat["user_id"]; - eid = (ulong)dat["guild_scheduled_event_id"]; - await this.OnScheduledGuildEventUserRemoveEventAsync(gid, eid, uid); + guildId = (ulong)payloadData["guild_id"]; + userId = (ulong)payloadData["user_id"]; + eventId = (ulong)payloadData["guild_scheduled_event_id"]; + await this.OnScheduledGuildEventUserRemoveEventAsync(guildId, eventId, userId); break; + #endregion #region Guild case "guild_create": - rawMembers = (JArray)dat["members"]; - rawPresences = (JArray)dat["presences"]; - dat.Remove("members"); - dat.Remove("presences"); + rawMembers = (JArray)payloadData["members"]; + rawPresences = (JArray)payloadData["presences"]; + payloadData.Remove("members"); + payloadData.Remove("presences"); + DiscordGuild guild = payloadData.ToDiscordObject(); + IEnumerable presences = rawPresences.ToDiscordObject>(); - await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), rawMembers, rawPresences.ToDiscordObject>()); + await this.OnGuildCreateEventAsync(guild, rawMembers, presences); break; case "guild_update": - rawMembers = (JArray)dat["members"]; - dat.Remove("members"); + rawMembers = (JArray)payloadData["members"]; + payloadData.Remove("members"); + guild = payloadData.ToDiscordObject(); - await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), rawMembers); + await this.OnGuildUpdateEventAsync(guild, rawMembers); break; case "guild_delete": - rawMembers = (JArray)dat["members"]; - dat.Remove("members"); + rawMembers = (JArray)payloadData["members"]; + payloadData.Remove("members"); + guild = payloadData.ToDiscordObject(); - await this.OnGuildDeleteEventAsync(dat.ToDiscordObject(), rawMembers); + await this.OnGuildDeleteEventAsync(guild, rawMembers); break; case "guild_emojis_update": - gid = (ulong)dat["guild_id"]; - IEnumerable ems = dat["emojis"].ToDiscordObject>(); - await this.OnGuildEmojisUpdateEventAsync(this._guilds[gid], ems); + guildId = (ulong)payloadData["guild_id"]; + IEnumerable ems = payloadData["emojis"].ToDiscordObject>(); + await this.OnGuildEmojisUpdateEventAsync(guildId, ems); break; case "guild_integrations_update": - gid = (ulong)dat["guild_id"]; - - // discord fires this event inconsistently if the current user leaves a guild. - if (!this._guilds.ContainsKey(gid)) - { - return; - } - - await this.OnGuildIntegrationsUpdateEventAsync(this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + await this.OnGuildIntegrationsUpdateEventAsync(guildId); break; + //TODO: Fix this event to use the new caching system case "guild_audit_log_entry_create": - gid = (ulong)dat["guild_id"]; - DiscordGuild guild = _guilds[gid]; - AuditLogAction auditLogAction = dat.ToDiscordObject(); - DiscordAuditLogEntry entry = await AuditLogParser.ParseAuditLogEntryAsync(guild, auditLogAction); - await this.OnGuildAuditLogEntryCreateEventAsync(guild, entry); + guildId = (ulong)payloadData["guild_id"]; + AuditLogAction auditLogAction = payloadData.ToDiscordObject(); + await this.OnGuildAuditLogEntryCreateEventAsync(guildId, auditLogAction); break; #endregion @@ -182,15 +189,15 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Guild Ban case "guild_ban_add": - usr = dat["user"].ToDiscordObject(); - gid = (ulong)dat["guild_id"]; - await this.OnGuildBanAddEventAsync(usr, this._guilds[gid]); + transportUser = payloadData["user"].ToDiscordObject(); + guildId = (ulong)payloadData["guild_id"]; + await this.OnGuildBanAddEventAsync(transportUser, guildId); break; case "guild_ban_remove": - usr = dat["user"].ToDiscordObject(); - gid = (ulong)dat["guild_id"]; - await this.OnGuildBanRemoveEventAsync(usr, this._guilds[gid]); + transportUser = payloadData["user"].ToDiscordObject(); + guildId = (ulong)payloadData["guild_id"]; + await this.OnGuildBanRemoveEventAsync(transportUser, guildId); break; #endregion @@ -198,35 +205,27 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Guild Member case "guild_member_add": - gid = (ulong)dat["guild_id"]; - await this.OnGuildMemberAddEventAsync(dat.ToDiscordObject(), this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + transportMember = payloadData.ToDiscordObject(); + await this.OnGuildMemberAddEventAsync(transportMember, guildId); break; case "guild_member_remove": - gid = (ulong)dat["guild_id"]; - usr = dat["user"].ToDiscordObject(); - - if (!this._guilds.ContainsKey(gid)) - { - // discord fires this event inconsistently if the current user leaves a guild. - if (usr.Id != this.CurrentUser.Id) - { - this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {Guild} in guild cache", gid); - } + guildId = (ulong)payloadData["guild_id"]; + transportUser = payloadData["user"].ToDiscordObject(); - return; - } - - await this.OnGuildMemberRemoveEventAsync(usr, this._guilds[gid]); + await this.OnGuildMemberRemoveEventAsync(transportUser, guildId); break; case "guild_member_update": - gid = (ulong)dat["guild_id"]; - await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + transportMember = payloadData.ToDiscordObject(); + + await this.OnGuildMemberUpdateEventAsync(transportMember, guildId); break; case "guild_members_chunk": - await this.OnGuildMembersChunkEventAsync(dat); + await this.OnGuildMembersChunkEventAsync(payloadData); break; #endregion @@ -234,18 +233,21 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Guild Role case "guild_role_create": - gid = (ulong)dat["guild_id"]; - await this.OnGuildRoleCreateEventAsync(dat["role"].ToDiscordObject(), this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + DiscordRole role = payloadData["role"].ToDiscordObject(); + await this.OnGuildRoleCreateEventAsync(role, guildId); break; case "guild_role_update": - gid = (ulong)dat["guild_id"]; - await this.OnGuildRoleUpdateEventAsync(dat["role"].ToDiscordObject(), this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + role = payloadData["role"].ToDiscordObject(); + await this.OnGuildRoleUpdateEventAsync(payloadData["role"].ToDiscordObject(), guildId); break; case "guild_role_delete": - gid = (ulong)dat["guild_id"]; - await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + ulong roleId = (ulong)payloadData["role_id"]; + await this.OnGuildRoleDeleteEventAsync(roleId, guildId); break; #endregion @@ -253,15 +255,16 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Invite case "invite_create": - gid = (ulong)dat["guild_id"]; - cid = (ulong)dat["channel_id"]; - await this.OnInviteCreateEventAsync(cid, gid, dat.ToDiscordObject()); + guildId = (ulong)payloadData["guild_id"]; + channelId = (ulong)payloadData["channel_id"]; + DiscordInvite invite = payloadData.ToDiscordObject(); + await this.OnInviteCreateEventAsync(channelId, guildId, invite); break; case "invite_delete": - gid = (ulong)dat["guild_id"]; - cid = (ulong)dat["channel_id"]; - await this.OnInviteDeleteEventAsync(cid, gid, dat); + guildId = (ulong)payloadData["guild_id"]; + channelId = (ulong)payloadData["channel_id"]; + await this.OnInviteDeleteEventAsync(channelId, guildId, payloadData); break; #endregion @@ -269,64 +272,75 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Message case "message_create": - rawMbr = dat["member"]; + rawMember = payloadData["member"]; - if (rawMbr != null) + if (rawMember != null) { - mbr = rawMbr.ToDiscordObject(); + transportMember = rawMember.ToDiscordObject(); } - if (rawRefMsg != null && rawRefMsg.HasValues) + if (rawRefMessage != null && rawRefMessage.HasValues) { - if (rawRefMsg.SelectToken("author") != null) + if (rawRefMessage.SelectToken("author") != null) { - refUsr = rawRefMsg.SelectToken("author").ToDiscordObject(); + referencedUser = rawRefMessage.SelectToken("author").ToDiscordObject(); } - if (rawRefMsg.SelectToken("member") != null) + if (rawRefMessage.SelectToken("member") != null) { - refMbr = rawRefMsg.SelectToken("member").ToDiscordObject(); + referencedMember = rawRefMessage.SelectToken("member").ToDiscordObject(); } } - TransportUser author = dat["author"].ToDiscordObject(); - dat.Remove("author"); - dat.Remove("member"); + TransportUser author = payloadData["author"].ToDiscordObject(); + payloadData.Remove("author"); + payloadData.Remove("member"); - await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), author, mbr, refUsr, refMbr); + await this.OnMessageCreateEventAsync(payloadData.ToDiscordObject(), author, + transportMember, referencedUser, referencedMember); break; case "message_update": - rawMbr = dat["member"]; + rawMember = payloadData["member"]; - if (rawMbr != null) + if (rawMember != null) { - mbr = rawMbr.ToDiscordObject(); + transportMember = rawMember.ToDiscordObject(); } - if (rawRefMsg != null && rawRefMsg.HasValues) + if (rawRefMessage != null && rawRefMessage.HasValues) { - if (rawRefMsg.SelectToken("author") != null) + if (rawRefMessage.SelectToken("author") != null) { - refUsr = rawRefMsg.SelectToken("author").ToDiscordObject(); + referencedUser = rawRefMessage.SelectToken("author").ToDiscordObject(); } - if (rawRefMsg.SelectToken("member") != null) + if (rawRefMessage.SelectToken("member") != null) { - refMbr = rawRefMsg.SelectToken("member").ToDiscordObject(); + referencedMember = rawRefMessage.SelectToken("member").ToDiscordObject(); } } - await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToDiscordObject(), mbr, refUsr, refMbr); + DiscordMessage message = payloadData.ToDiscordObject(); + TransportUser? optionalAuthor = payloadData["author"].ToDiscordObject(); + + await this.OnMessageUpdateEventAsync(message, optionalAuthor, transportMember, referencedUser, + referencedMember); break; // delete event does *not* include message object case "message_delete": - await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); + ulong messageId = (ulong)payloadData["id"]; + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"] ?? 0; + await this.OnMessageDeleteEventAsync(messageId, channelId, optionalGuildId); break; case "message_delete_bulk": - await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToDiscordObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); + ulong[] messageIds = payloadData["ids"].ToDiscordObject(); + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + await this.OnMessageBulkDeleteEventAsync(messageIds, channelId, optionalGuildId); break; #endregion @@ -334,26 +348,53 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Message Reaction case "message_reaction_add": - rawMbr = dat["member"]; + TransportMember? optionalTransportMember = null; + rawMember = payloadData["member"]; - if (rawMbr != null) + if (rawMember != null) { - mbr = rawMbr.ToDiscordObject(); + optionalTransportMember = rawMember.ToDiscordObject(); } - await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToDiscordObject()); + userId = (ulong)payloadData["user_id"]; + messageId = (ulong)payloadData["message_id"]; + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + DiscordEmoji emoji = payloadData["emoji"].ToDiscordObject(); + + + await this.OnMessageReactionAddAsync(userId, messageId, channelId, optionalGuildId, + optionalTransportMember, emoji); break; case "message_reaction_remove": - await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToDiscordObject()); + + userId = (ulong)payloadData["user_id"]; + messageId = (ulong)payloadData["message_id"]; + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + emoji = payloadData["emoji"].ToDiscordObject(); + + await this.OnMessageReactionRemoveAsync(userId, messageId, channelId, optionalGuildId, emoji); break; case "message_reaction_remove_all": - await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); + + messageId = (ulong)payloadData["message_id"]; + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + + await this.OnMessageReactionRemoveAllAsync(messageId, channelId, optionalGuildId); break; case "message_reaction_remove_emoji": - await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]); + + messageId = (ulong)payloadData["message_id"]; + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + emoji = payloadData["emoji"].ToDiscordObject(); + + await this.OnMessageReactionRemoveEmojiAsync(messageId, channelId, optionalGuildId, emoji); break; #endregion @@ -362,15 +403,17 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) case "presence_update": // Presences are a mess. I'm not touching this. ~Velvet - await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]); + await this.OnPresenceUpdateEventAsync(payloadData, (JObject)payloadData["user"]); break; case "user_settings_update": - await this.OnUserSettingsUpdateEventAsync(dat.ToDiscordObject()); + transportUser = payloadData.ToDiscordObject(); + await this.OnUserSettingsUpdateEventAsync(transportUser); break; case "user_update": - await this.OnUserUpdateEventAsync(dat.ToDiscordObject()); + transportUser = payloadData.ToDiscordObject(); + await this.OnUserUpdateEventAsync(transportUser); break; #endregion @@ -378,12 +421,14 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Voice case "voice_state_update": - await this.OnVoiceStateUpdateEventAsync(dat); + await this.OnVoiceStateUpdateEventAsync(payloadData); break; case "voice_server_update": - gid = (ulong)dat["guild_id"]; - await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this._guilds[gid]); + guildId = (ulong)payloadData["guild_id"]; + string token = (string)payloadData["token"]; + string endpoint = (string)payloadData["endpoint"]; + await this.OnVoiceServerUpdateEventAsync(endpoint, token, guildId); break; #endregion @@ -391,32 +436,48 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) #region Thread case "thread_create": - thread = dat.ToDiscordObject(); + thread = payloadData.ToDiscordObject(); await this.OnThreadCreateEventAsync(thread, thread.IsNew); break; case "thread_update": - thread = dat.ToDiscordObject(); + thread = payloadData.ToDiscordObject(); await this.OnThreadUpdateEventAsync(thread); break; case "thread_delete": - thread = dat.ToDiscordObject(); + thread = payloadData.ToDiscordObject(); await this.OnThreadDeleteEventAsync(thread); break; case "thread_list_sync": - gid = (ulong)dat["guild_id"]; //get guild - await this.OnThreadListSyncEventAsync(this._guilds[gid], dat["channel_ids"].ToDiscordObject>(), dat["threads"].ToDiscordObject>(), dat["members"].ToDiscordObject>()); + + guildId = (ulong)payloadData["guild_id"]; + ulong[] channelIds = payloadData["channel_ids"].ToObject(); + DiscordThreadChannel[] threads = payloadData["threads"].ToObject(); + DiscordThreadChannelMember[] members = payloadData["members"].ToObject(); + + await this.OnThreadListSyncEventAsync(guildId, channelIds, threads, members); break; case "thread_member_update": - await this.OnThreadMemberUpdateEventAsync(dat.ToDiscordObject()); + DiscordThreadChannelMember threadChannelMember = + payloadData.ToDiscordObject(); + await this.OnThreadMemberUpdateEventAsync(threadChannelMember); break; case "thread_members_update": - gid = (ulong)dat["guild_id"]; - await this.OnThreadMembersUpdateEventAsync(this._guilds[gid], (ulong)dat["id"], dat["added_members"]?.ToDiscordObject>(), dat["removed_member_ids"]?.ToDiscordObject>(), (int)dat["member_count"]); + + guildId = (ulong)payloadData["guild_id"]; + ulong threadId = (ulong)payloadData["id"]; + IReadOnlyList addedMembers = + payloadData["added_members"].ToObject>(); + IReadOnlyList removedMemberIds = + payloadData["removed_member_ids"].ToObject>(); + int memberCount = (int)payloadData["member_count"]; + + await this.OnThreadMembersUpdateEventAsync(guildId, threadId, addedMembers, removedMemberIds, + memberCount); break; #endregion @@ -425,55 +486,70 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) case "interaction_create": - rawMbr = dat["member"]; + rawMember = payloadData["member"]; - if (rawMbr != null) + if (rawMember != null) { - mbr = dat["member"].ToDiscordObject(); - usr = mbr.User; + transportMember = payloadData["member"].ToDiscordObject(); + transportUser = transportMember.User; } else { - usr = dat["user"].ToDiscordObject(); + transportUser = payloadData["user"].ToDiscordObject(); } // Re: Removing re-serialized data: This one is probably fine? // The user on the object is marked with [JsonIgnore]. - cid = (ulong)dat["channel_id"]; - await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject()); + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + DiscordInteraction interaction = payloadData.ToDiscordObject(); + + await this.OnInteractionCreateAsync(optionalGuildId, channelId, transportUser, transportMember, + interaction); break; case "integration_create": - await this.OnIntegrationCreateAsync(dat.ToDiscordObject(), (ulong)dat["guild_id"]); + guildId = (ulong)payloadData["guild_id"]; + DiscordIntegration integration = payloadData.ToDiscordObject(); + await this.OnIntegrationCreateAsync(integration, guildId); break; case "integration_update": - await this.OnIntegrationUpdateAsync(dat.ToDiscordObject(), (ulong)dat["guild_id"]); + guildId = (ulong)payloadData["guild_id"]; + integration = payloadData.ToDiscordObject(); + await this.OnIntegrationUpdateAsync(integration, guildId); break; case "integration_delete": - await this.OnIntegrationDeleteAsync((ulong)dat["id"], (ulong)dat["guild_id"], (ulong?)dat["application_id"]); + guildId = (ulong)payloadData["guild_id"]; + ulong integrationId = (ulong)payloadData["id"]; + ulong applicationId = (ulong)payloadData["application_id"]; + await this.OnIntegrationDeleteAsync(integrationId, guildId, applicationId); break; case "application_command_permissions_update": - await this.OnApplicationCommandPermissionsUpdateAsync(dat); + await this.OnApplicationCommandPermissionsUpdateAsync(payloadData); break; + #endregion #region Stage Instance case "stage_instance_create": - await this.OnStageInstanceCreateAsync(dat.ToDiscordObject()); + DiscordStageInstance stageInstance = payloadData.ToDiscordObject(); + await this.OnStageInstanceCreateAsync(stageInstance); break; case "stage_instance_update": - await this.OnStageInstanceUpdateAsync(dat.ToDiscordObject()); + stageInstance = payloadData.ToDiscordObject(); + await this.OnStageInstanceUpdateAsync(stageInstance); break; case "stage_instance_delete": - await this.OnStageInstanceDeleteAsync(dat.ToDiscordObject()); + stageInstance = payloadData.ToDiscordObject(); + await this.OnStageInstanceDeleteAsync(stageInstance); break; #endregion @@ -487,56 +563,53 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) break; case "typing_start": - cid = (ulong)dat["channel_id"]; - rawMbr = dat["member"]; + userId = (ulong)payloadData["user_id"]; + channelId = (ulong)payloadData["channel_id"]; + optionalGuildId = (ulong?)payloadData["guild_id"]; + rawMember = payloadData["member"]; + DateTimeOffset timestamp = Utilities.GetDateTimeOffset((long)payloadData["timestamp"]); - if (rawMbr != null) + if (rawMember != null) { - mbr = rawMbr.ToDiscordObject(); + transportMember = rawMember.ToDiscordObject(); } - await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr); - break; - case "webhooks_update": - gid = (ulong)dat["guild_id"]; - cid = (ulong)dat["channel_id"]; - await this.OnWebhooksUpdateAsync(this._guilds[gid].GetChannel(cid), this._guilds[gid]); + await this.OnTypingStartEventAsync(userId, channelId, optionalGuildId, timestamp, transportMember); break; - case "guild_stickers_update": - IEnumerable strs = dat["stickers"].ToDiscordObject>(); - await this.OnStickersUpdatedAsync(strs, dat); - break; - - default: - await this.OnUnknownEventAsync(payload); - if (this.Configuration.LogUnknownEvents) - { - this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {EventName}\npayload: {@Payload}", payload.EventName, payload.Data); - } - + case "webhooks_update": + guildId = (ulong)payloadData["guild_id"]; + channelId = (ulong)payloadData["channel_id"]; + await this.OnWebhooksUpdateAsync(channelId, guildId); break; #endregion #region AutoModeration + case "auto_moderation_rule_create": - await this.OnAutoModerationRuleCreateAsync(dat.ToDiscordObject()); + DiscordAutoModerationRule autoModerationRule = payloadData.ToDiscordObject(); + await this.OnAutoModerationRuleCreateAsync(autoModerationRule); break; case "auto_moderation_rule_update": - await this.OnAutoModerationRuleUpdatedAsync(dat.ToDiscordObject()); + autoModerationRule = payloadData.ToDiscordObject(); + await this.OnAutoModerationRuleUpdatedAsync(autoModerationRule); break; case "auto_moderation_rule_delete": - await this.OnAutoModerationRuleDeletedAsync(dat.ToDiscordObject()); + autoModerationRule = payloadData.ToDiscordObject(); + await this.OnAutoModerationRuleDeletedAsync(autoModerationRule); break; case "auto_moderation_action_execution": - await this.OnAutoModerationRuleExecutedAsync(dat.ToDiscordObject()); + DiscordAutoModerationActionExecution actionExecution = + payloadData.ToDiscordObject(); + await this.OnAutoModerationRuleExecutedAsync(actionExecution); break; - #endregion + + #endregion } } @@ -548,113 +621,111 @@ internal async Task HandleDispatchAsync(GatewayPayload payload) internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds, JArray rawDmChannels) { - //ready.CurrentUser.Discord = this; - - TransportUser rusr = ready.CurrentUser; - this.CurrentUser.Username = rusr.Username; - this.CurrentUser.Discriminator = rusr.Discriminator; - this.CurrentUser.AvatarHash = rusr.AvatarHash; - this.CurrentUser.MfaEnabled = rusr.MfaEnabled; - this.CurrentUser.Verified = rusr.Verified; - this.CurrentUser.IsBot = rusr.IsBot; + TransportUser rawCurrentUser = ready.CurrentUser; + this.CurrentUser.Username = rawCurrentUser.Username; + this.CurrentUser.Discriminator = rawCurrentUser.Discriminator; + this.CurrentUser.AvatarHash = rawCurrentUser.AvatarHash; + this.CurrentUser.MfaEnabled = rawCurrentUser.MfaEnabled; + this.CurrentUser.Verified = rawCurrentUser.Verified; + this.CurrentUser.IsBot = rawCurrentUser.IsBot; this.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; - Dictionary raw_guild_index = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); + Dictionary rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); + this._readyGuilds = rawGuildIndex.Keys.ToList(); + this._readyGuildCount = rawGuildIndex.Count; - this._privateChannels.Clear(); foreach (JToken rawChannel in rawDmChannels) { DiscordDmChannel channel = rawChannel.ToDiscordObject(); - channel.Discord = this; - //xdc._recipients = - // .Select(xtu => this.InternalGetCachedUser(xtu.Id) ?? new DiscordUser(xtu) { Discord = this }) - // .ToList(); - - IEnumerable recips_raw = rawChannel["recipients"].ToDiscordObject>(); - List recipients = new List(); - foreach (TransportUser xr in recips_raw) + IEnumerable rawRecipients = + rawChannel["recipients"].ToDiscordObject>(); + List recipients = new(); + foreach (TransportUser rawRecipient in rawRecipients) { - DiscordUser xu = new DiscordUser(xr) { Discord = this }; - xu = this.UpdateUserCache(xu); + DiscordUser recipientDiscordUser = new(rawRecipient) {Discord = this}; + await this.Cache.AddUserAsync(recipientDiscordUser); - recipients.Add(xu); + recipients.Add(recipientDiscordUser); } - channel.Recipients = recipients; + channel.Recipients = recipients; - this._privateChannels[channel.Id] = channel; + await this.Cache.AddChannelAsync(channel); } - this._guilds.Clear(); + this._guildIds.Clear(); IEnumerable guilds = rawGuilds.ToDiscordObject>(); foreach (DiscordGuild guild in guilds) { + this._guildIds.Add(guild.Id); guild.Discord = this; guild._channels ??= new ConcurrentDictionary(); guild._threads ??= new ConcurrentDictionary(); - foreach (DiscordChannel xc in guild.Channels.Values) + foreach (DiscordChannel channel in guild.Channels.Values) { - xc.GuildId = guild.Id; - xc.Discord = this; - foreach (DiscordOverwrite xo in xc._permissionOverwrites) + channel.GuildId = guild.Id; + channel.Discord = this; + foreach (DiscordOverwrite overwrite in channel._permissionOverwrites) { - xo.Discord = this; - xo._channel_id = xc.Id; + overwrite.Discord = this; + overwrite._channel_id = channel.Id; } } - foreach (DiscordThreadChannel xt in guild.Threads.Values) + + foreach (DiscordThreadChannel threadChannel in guild.Threads.Values) { - xt.GuildId = guild.Id; - xt.Discord = this; + threadChannel.GuildId = guild.Id; + threadChannel.Discord = this; } guild._roles ??= new ConcurrentDictionary(); - foreach (DiscordRole xr in guild.Roles.Values) + foreach (DiscordRole role in guild.Roles.Values) { - xr.Discord = this; - xr._guild_id = guild.Id; + role.Discord = this; + role._guild_id = guild.Id; } - JObject raw_guild = raw_guild_index[guild.Id]; - JArray? raw_members = (JArray)raw_guild["members"]; + JObject rawGuild = rawGuildIndex[guild.Id]; + JArray? rawMembers = (JArray)rawGuild["members"]; guild._members?.Clear(); guild._members ??= new ConcurrentDictionary(); - if (raw_members != null) + if (rawMembers != null) { - foreach (JToken xj in raw_members) + foreach (JToken rawMember in rawMembers) { - TransportMember xtm = xj.ToDiscordObject(); + TransportMember transportMember = rawMember.ToDiscordObject(); - DiscordUser xu = new DiscordUser(xtm.User) { Discord = this }; - xu = this.UpdateUserCache(xu); + DiscordUser newUser = new(transportMember.User) {Discord = this}; + await this.Cache.AddUserAsync(newUser); - guild._members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, _guild_id = guild.Id }; + guild._members[transportMember.User.Id] = + new DiscordMember(transportMember) {Discord = this, _guild_id = guild.Id}; } } guild._emojis ??= new ConcurrentDictionary(); - foreach (DiscordEmoji xe in guild.Emojis.Values) + foreach (DiscordEmoji emoji in guild.Emojis.Values) { - xe.Discord = this; + emoji.Discord = this; } guild._voiceStates ??= new ConcurrentDictionary(); - foreach (DiscordVoiceState xvs in guild.VoiceStates.Values) + foreach (DiscordVoiceState voiceState in guild.VoiceStates.Values) { - xvs.Discord = this; + voiceState.Discord = this; } - this._guilds[guild.Id] = guild; + await this.Cache.AddGuildAsync(guild); } await this._ready.InvokeAsync(this, new SessionReadyEventArgs()); @@ -662,7 +733,7 @@ internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds, JArr if (!guilds.Any()) { this._guildDownloadCompleted = true; - GuildDownloadCompletedEventArgs ea = new(this.Guilds); + GuildDownloadCompletedEventArgs ea = new(new Dictionary()); await this._guildDownloadCompletedEv.InvokeAsync(this, ea); } } @@ -680,140 +751,178 @@ internal Task OnResumedAsync() internal async Task OnChannelCreateEventAsync(DiscordChannel channel) { channel.Discord = this; - foreach (DiscordOverwrite xo in channel._permissionOverwrites) + foreach (DiscordOverwrite discordOverwrite in channel._permissionOverwrites) { - xo.Discord = this; - xo._channel_id = channel.Id; + discordOverwrite.Discord = this; + discordOverwrite._channel_id = channel.Id; } - this._guilds[channel.GuildId.Value]._channels[channel.Id] = channel; + CachedEntity? cachedGuild = null; + if (channel.GuildId.HasValue) + { + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(channel.GuildId.Value); + if (guild is not null) + { + guild._channels[channel.Id] = channel; + } + + cachedGuild = new CachedEntity(channel.Id, guild); + } - await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs { Channel = channel, Guild = channel.Guild }); + await this._channelCreated.InvokeAsync(this, + new ChannelCreateEventArgs {Channel = channel, Guild = cachedGuild}); } internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) { - if (channel == null) + if (channel is null) { return; } channel.Discord = this; - DiscordGuild? gld = channel.Guild; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(channel.GuildId.Value); + if (guild is not null) + { + guild._channels[channel.Id] = channel; + } + + CachedEntity cachedGuild = new(channel.Id, guild); - DiscordChannel? channel_new = this.InternalGetCachedChannel(channel.Id); - DiscordChannel channel_old = null; + DiscordChannel? newChannel = await this.Cache.TryGetChannelAsync(channel.Id); + DiscordChannel? oldChannel = null; - if (channel_new != null) + if (newChannel is not null) { - channel_old = new DiscordChannel + oldChannel = new DiscordChannel { - Bitrate = channel_new.Bitrate, + Bitrate = newChannel.Bitrate, Discord = this, - GuildId = channel_new.GuildId, - Id = channel_new.Id, - //IsPrivate = channel_new.IsPrivate, - LastMessageId = channel_new.LastMessageId, - Name = channel_new.Name, - _permissionOverwrites = new List(channel_new._permissionOverwrites), - Position = channel_new.Position, - Topic = channel_new.Topic, - Type = channel_new.Type, - UserLimit = channel_new.UserLimit, - ParentId = channel_new.ParentId, - IsNSFW = channel_new.IsNSFW, - PerUserRateLimit = channel_new.PerUserRateLimit, - RtcRegionId = channel_new.RtcRegionId, - QualityMode = channel_new.QualityMode + GuildId = newChannel.GuildId, + Id = newChannel.Id, + LastMessageId = newChannel.LastMessageId, + Name = newChannel.Name, + _permissionOverwrites = new List(newChannel._permissionOverwrites), + Position = newChannel.Position, + Topic = newChannel.Topic, + Type = newChannel.Type, + UserLimit = newChannel.UserLimit, + ParentId = newChannel.ParentId, + IsNSFW = newChannel.IsNSFW, + PerUserRateLimit = newChannel.PerUserRateLimit, + RtcRegionId = newChannel.RtcRegionId, + QualityMode = newChannel.QualityMode }; - channel_new.Bitrate = channel.Bitrate; - channel_new.Name = channel.Name; - channel_new.Position = channel.Position; - channel_new.Topic = channel.Topic; - channel_new.UserLimit = channel.UserLimit; - channel_new.ParentId = channel.ParentId; - channel_new.IsNSFW = channel.IsNSFW; - channel_new.PerUserRateLimit = channel.PerUserRateLimit; - channel_new.Type = channel.Type; - channel_new.RtcRegionId = channel.RtcRegionId; - channel_new.QualityMode = channel.QualityMode; - - channel_new._permissionOverwrites.Clear(); - - foreach (DiscordOverwrite po in channel._permissionOverwrites) + newChannel.Bitrate = channel.Bitrate; + newChannel.Name = channel.Name; + newChannel.Position = channel.Position; + newChannel.Topic = channel.Topic; + newChannel.UserLimit = channel.UserLimit; + newChannel.ParentId = channel.ParentId; + newChannel.IsNSFW = channel.IsNSFW; + newChannel.PerUserRateLimit = channel.PerUserRateLimit; + newChannel.Type = channel.Type; + newChannel.RtcRegionId = channel.RtcRegionId; + newChannel.QualityMode = channel.QualityMode; + + newChannel._permissionOverwrites.Clear(); + + foreach (DiscordOverwrite overwrite in channel._permissionOverwrites) { - po.Discord = this; - po._channel_id = channel.Id; + overwrite.Discord = this; + overwrite._channel_id = channel.Id; } - channel_new._permissionOverwrites.AddRange(channel._permissionOverwrites); + newChannel._permissionOverwrites.AddRange(channel._permissionOverwrites); } - else if (gld != null) + else { - gld._channels[channel.Id] = channel; + newChannel = channel; } - await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs { ChannelAfter = channel_new, Guild = gld, ChannelBefore = channel_old }); + await this.Cache.AddChannelAsync(newChannel); + + await this._channelUpdated.InvokeAsync(this, + new ChannelUpdateEventArgs {ChannelAfter = newChannel, Guild = guild, ChannelBefore = oldChannel}); } internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) { - if (channel == null) + if (channel is null) //TODO why is this check here? { return; } channel.Discord = this; - //if (channel.IsPrivate) - if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private) + if (channel.Type is ChannelType.Group || channel.Type is ChannelType.Private) { DiscordDmChannel? dmChannel = channel as DiscordDmChannel; - _ = this._privateChannels.TryRemove(dmChannel.Id, out _); + DiscordChannel? cachedChannel = await this.Cache.TryGetChannelAsync(channel.Id); + if (cachedChannel is not null && cachedChannel is DiscordDmChannel cachedDmChannel) + { + dmChannel = cachedDmChannel; + } - await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs { Channel = dmChannel }); + await this.Cache.RemoveChannelAsync(channel.Id); + await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs {Channel = dmChannel}); } else { - DiscordGuild gld = channel.Guild; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(channel.GuildId.Value); - if (gld._channels.TryRemove(channel.Id, out DiscordChannel? cachedChannel)) + if (guild is not null) { - channel = cachedChannel; + if (guild._channels.TryRemove(channel.Id, out DiscordChannel? cachedChannel)) + { + channel = cachedChannel; + } + } + else + { + DiscordChannel? cachedChannel = await this.Cache.TryGetChannelAsync(channel.Id); + if (cachedChannel is not null) + { + channel = cachedChannel; + } } - await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs { Channel = channel, Guild = gld }); + await this.Cache.RemoveChannelAsync(channel.Id); + await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs {Channel = channel, Guild = guild}); } } internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordChannel? channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); + CachedEntity? cachedGuild = null; + if (guildId.HasValue) + { + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId.Value); + cachedGuild = new CachedEntity(guild.Id, guild); + } - if (channel == null) + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + if (channel is null) { channel = new DiscordDmChannel { - Id = channelId, - Discord = this, - Type = ChannelType.Private, - Recipients = Array.Empty() + Id = channelId, Discord = this, Type = ChannelType.Private, Recipients = Array.Empty() }; DiscordDmChannel chn = (DiscordDmChannel)channel; - - this._privateChannels[channelId] = chn; + await this.Cache.AddChannelAsync(chn); + //TODO track private channel ids on client + //this._privateChannels[channelId] = chn; } - ChannelPinsUpdateEventArgs ea = new ChannelPinsUpdateEventArgs + CachedEntity cachedChannel = new(channel.Id, channel); + + ChannelPinsUpdateEventArgs ea = new() { - Guild = guild, - Channel = channel, - LastPinTimestamp = lastPinTimestamp + Guild = cachedGuild, Channel = cachedChannel, LastPinTimestamp = lastPinTimestamp }; await this._channelPinsUpdated.InvokeAsync(this, ea); } @@ -826,136 +935,171 @@ private async Task OnScheduledGuildEventCreateEventAsync(DiscordScheduledGuildEv { evt.Discord = this; - if (evt.Creator != null) + if (evt.Creator is not null) { evt.Creator.Discord = this; - this.UpdateUserCache(evt.Creator); + + //Add user to cache if its not already there + await this.Cache.AddIfNotPresentAsync(evt.Creator, evt.Creator.GetCacheKey()); } - evt.Guild._scheduledEvents[evt.Id] = evt; + //Add event to cached guild if guild is cached + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(evt.GuildId); + if (guild is not null) + { + guild._scheduledEvents[evt.Id] = evt; + } - await this._scheduledGuildEventCreated.InvokeAsync(this, new ScheduledGuildEventCreateEventArgs { Event = evt }); + await this._scheduledGuildEventCreated.InvokeAsync(this, new ScheduledGuildEventCreateEventArgs {Event = evt}); } private async Task OnScheduledGuildEventDeleteEventAsync(DiscordScheduledGuildEvent evt) { - DiscordGuild guild = this.InternalGetCachedGuild(evt.GuildId); - - if (guild == null) // ??? // + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(evt.GuildId); + if (guild is not null) { - return; + guild._scheduledEvents.TryRemove(evt.Id, out _); } - guild._scheduledEvents.TryRemove(evt.Id, out _); - evt.Discord = this; - if (evt.Creator != null) + if (evt.Creator is not null) { evt.Creator.Discord = this; - this.UpdateUserCache(evt.Creator); + await this.Cache.AddIfNotPresentAsync(evt.Creator, evt.Creator.GetCacheKey()); } - await this._scheduledGuildEventDeleted.InvokeAsync(this, new ScheduledGuildEventDeleteEventArgs { Event = evt }); + await this._scheduledGuildEventDeleted.InvokeAsync(this, new ScheduledGuildEventDeleteEventArgs {Event = evt}); } private async Task OnScheduledGuildEventUpdateEventAsync(DiscordScheduledGuildEvent evt) { evt.Discord = this; - if (evt.Creator != null) + if (evt.Creator is not null) { evt.Creator.Discord = this; - this.UpdateUserCache(evt.Creator); + await this.Cache.AddIfNotPresentAsync(evt.Creator, evt.Creator.GetCacheKey()); } - DiscordGuild guild = this.InternalGetCachedGuild(evt.GuildId); - guild._scheduledEvents.TryGetValue(evt.GuildId, out DiscordScheduledGuildEvent? oldEvt); - - evt.Guild._scheduledEvents[evt.Id] = evt; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(evt.GuildId); + DiscordScheduledGuildEvent? oldEvt = null; + if (guild is not null) + { + guild._scheduledEvents.TryGetValue(evt.GuildId, out oldEvt); + guild._scheduledEvents[evt.Id] = evt; + } if (evt.Status is ScheduledGuildEventStatus.Completed) { - await this._scheduledGuildEventCompleted.InvokeAsync(this, new ScheduledGuildEventCompletedEventArgs() { Event = evt }); + await this._scheduledGuildEventCompleted.InvokeAsync(this, + new ScheduledGuildEventCompletedEventArgs() {Event = evt}); } else { - await this._scheduledGuildEventUpdated.InvokeAsync(this, new ScheduledGuildEventUpdateEventArgs() { EventBefore = oldEvt, EventAfter = evt }); + await this._scheduledGuildEventUpdated.InvokeAsync(this, + new ScheduledGuildEventUpdateEventArgs() {EventBefore = oldEvt, EventAfter = evt}); } } private async Task OnScheduledGuildEventUserAddEventAsync(ulong guildId, ulong eventId, ulong userId) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordScheduledGuildEvent evt = guild._scheduledEvents.GetOrAdd(eventId, new DiscordScheduledGuildEvent() + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordScheduledGuildEvent? evt = guild?._scheduledEvents.GetOrAdd(eventId, + new DiscordScheduledGuildEvent() {Id = eventId, GuildId = guildId, Guild = guild, Discord = this}); + + if (guild is not null && evt?.UserCount is not null) { - Id = eventId, - GuildId = guildId, - Discord = this, - UserCount = 0 - }); + evt.UserCount++; + } - evt.UserCount++; + DiscordUser? cachedUser = await this.Cache.TryGetUserAsync(userId); + if (cachedUser is null) + { + if (guild is not null && guild.Members.TryGetValue(userId, out DiscordMember? mbr)) + { + cachedUser = mbr; + } + else + { + cachedUser = new DiscordUser() {Id = userId, Discord = this}; + await this.Cache.AddUserAsync(cachedUser); + } + } - DiscordUser user = - guild.Members.TryGetValue(userId, out DiscordMember? mbr) ? mbr : - this.GetCachedOrEmptyUserInternal(userId) ?? new DiscordUser() { Id = userId, Discord = this }; + DiscordUser user = cachedUser; - await this._scheduledGuildEventUserAdded.InvokeAsync(this, new ScheduledGuildEventUserAddEventArgs() { Event = evt, User = user }); + await this._scheduledGuildEventUserAdded.InvokeAsync(this, + new ScheduledGuildEventUserAddEventArgs() {Event = evt, User = user}); } private async Task OnScheduledGuildEventUserRemoveEventAsync(ulong guildId, ulong eventId, ulong userId) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordScheduledGuildEvent evt = guild._scheduledEvents.GetOrAdd(eventId, new DiscordScheduledGuildEvent() - { - Id = eventId, - GuildId = guildId, - Discord = this, - UserCount = 0 - }); + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordScheduledGuildEvent? evt = guild?._scheduledEvents.GetOrAdd(eventId, + new DiscordScheduledGuildEvent() {Id = eventId, GuildId = guildId, Guild = guild, Discord = this}); - evt.UserCount = evt.UserCount is 0 ? 0 : evt.UserCount - 1; + if (guild is not null && evt?.UserCount is not null) + { + if (evt!.UserCount > 0) + { + evt.UserCount--; + } + } - DiscordUser user = - guild.Members.TryGetValue(userId, out DiscordMember? mbr) ? mbr : - this.GetCachedOrEmptyUserInternal(userId) ?? new DiscordUser() { Id = userId, Discord = this }; + DiscordUser? cachedUser = await this.Cache.TryGetUserAsync(userId); + if (cachedUser is null) + { + if (guild is not null && guild.Members.TryGetValue(userId, out DiscordMember? mbr)) + { + cachedUser = mbr; + } + else + { + cachedUser = new DiscordUser() {Id = userId, Discord = this}; + } + } - await this._scheduledGuildEventUserRemoved.InvokeAsync(this, new ScheduledGuildEventUserRemoveEventArgs() { Event = evt, User = user }); + DiscordUser user = cachedUser; + await this._scheduledGuildEventUserRemoved.InvokeAsync(this, + new ScheduledGuildEventUserRemoveEventArgs() {Event = evt, User = user}); } #endregion #region Guild - internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) + internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable? presences) { - if (presences != null) + if (presences is not null) { - foreach (DiscordPresence xp in presences) + foreach (DiscordPresence presence in presences) { - xp.Discord = this; - xp.GuildId = guild.Id; - xp.Activity = new DiscordActivity(xp.RawActivity); - if (xp.RawActivities != null) + presence.Discord = this; + presence.GuildId = guild.Id; + presence.Activity = new DiscordActivity(presence.RawActivity); + if (presence.RawActivities is not null) { - xp._internalActivities = new DiscordActivity[xp.RawActivities.Length]; - for (int i = 0; i < xp.RawActivities.Length; i++) + presence._internalActivities = new DiscordActivity[presence.RawActivities.Length]; + for (int i = 0; i < presence.RawActivities.Length; i++) { - xp._internalActivities[i] = new DiscordActivity(xp.RawActivities[i]); + presence._internalActivities[i] = new DiscordActivity(presence.RawActivities[i]); } } - this._presences[xp.User.Id] = xp; + + await this.Cache.AddUserPresenceAsync(presence); } } - bool exists = this._guilds.TryGetValue(guild.Id, out DiscordGuild? foundGuild); + DiscordGuild? foundGuild = await this.Cache.TryGetGuildAsync(guild.Id); + bool exists = this._guildIds.Contains(guild.Id); + guild.Discord = this; guild.IsUnavailable = false; DiscordGuild eventGuild = guild; - if (exists) + if (foundGuild is not null) { guild = foundGuild; } @@ -970,7 +1114,7 @@ internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMember guild._stageInstances ??= new ConcurrentDictionary(); guild._scheduledEvents ??= new ConcurrentDictionary(); - this.UpdateCachedGuild(eventGuild, rawMembers); + this.UpdateCachedGuildAsync(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; @@ -984,55 +1128,57 @@ internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMember guild.IsNSFW = eventGuild.IsNSFW; - foreach (KeyValuePair kvp in eventGuild._voiceStates ??= new()) + foreach (KeyValuePair voicestate in eventGuild._voiceStates ??= new()) { - guild._voiceStates[kvp.Key] = kvp.Value; + guild._voiceStates[voicestate.Key] = voicestate.Value; } - foreach (DiscordScheduledGuildEvent xe in guild._scheduledEvents.Values) + foreach (DiscordScheduledGuildEvent discordEvent in guild._scheduledEvents.Values) { - xe.Discord = this; + discordEvent.Discord = this; - if (xe.Creator != null) + if (discordEvent.Creator is not null) { - xe.Creator.Discord = this; + discordEvent.Creator.Discord = this; } } - foreach (DiscordChannel xc in guild._channels.Values) + foreach (DiscordChannel channel in guild._channels.Values) { - xc.GuildId = guild.Id; - xc.Discord = this; - foreach (DiscordOverwrite xo in xc._permissionOverwrites) + channel.GuildId = guild.Id; + channel.Discord = this; + foreach (DiscordOverwrite xo in channel._permissionOverwrites) { xo.Discord = this; - xo._channel_id = xc.Id; + xo._channel_id = channel.Id; } } - foreach (DiscordThreadChannel xt in guild._threads.Values) + + foreach (DiscordThreadChannel threadChannel in guild._threads.Values) { - xt.GuildId = guild.Id; - xt.Discord = this; + threadChannel.GuildId = guild.Id; + threadChannel.Discord = this; } - foreach (DiscordEmoji xe in guild._emojis.Values) + + foreach (DiscordEmoji emoji in guild._emojis.Values) { - xe.Discord = this; + emoji.Discord = this; } - foreach (DiscordMessageSticker xs in guild._stickers.Values) + foreach (DiscordMessageSticker sticker in guild._stickers.Values) { - xs.Discord = this; + sticker.Discord = this; } - foreach (DiscordVoiceState xvs in guild._voiceStates.Values) + foreach (DiscordVoiceState voiceState in guild._voiceStates.Values) { - xvs.Discord = this; + voiceState.Discord = this; } - foreach (DiscordRole xr in guild._roles.Values) + foreach (DiscordRole role in guild._roles.Values) { - xr.Discord = this; - xr._guild_id = guild.Id; + role.Discord = this; + role._guild_id = guild.Id; } foreach (DiscordStageInstance instance in guild._stageInstances.Values) @@ -1040,74 +1186,94 @@ internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMember instance.Discord = this; } - bool old = Volatile.Read(ref this._guildDownloadCompleted); - bool dcompl = this._guilds.Values.All(xg => !xg.IsUnavailable); - Volatile.Write(ref this._guildDownloadCompleted, dcompl); + bool guildDownloadCompleted = Volatile.Read(ref this._guildDownloadCompleted); + + if (this._readyGuilds.Contains(guild.Id) && this._readyGuildCount > 0) + { + this._readyGuildCount--; + } + + bool guildDownloadComplete = this._readyGuildCount == 0; + Volatile.Write(ref this._guildDownloadCompleted, guildDownloadComplete); if (exists) { - await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs { Guild = guild }); + await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs {Guild = guild}); } else { + this._guildIds.Add(guild.Id); await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs { Guild = guild }); } - if (dcompl && !old) + if (guildDownloadComplete && !guildDownloadCompleted) { - await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds)); + List guilds = new(); + + foreach (ulong guildId in this._readyGuilds) + { + DiscordGuild? cachedGuild = await this.Cache.TryGetGuildAsync(guildId); + if (cachedGuild is not null) + { + guilds.Add(cachedGuild); + } + } + + IReadOnlyDictionary guildsReadOnly = guilds.ToDictionary(x => x.Id, x => x); + + await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(guildsReadOnly)); } } internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) { - DiscordGuild oldGuild; + DiscordGuild? oldGuild = await this.Cache.TryGetGuildAsync(guild.Id); - if (!this._guilds.ContainsKey(guild.Id)) + if (oldGuild is null) { - this._guilds[guild.Id] = guild; + await this.Cache.AddGuildAsync(guild); oldGuild = null; } else { - DiscordGuild gld = this._guilds[guild.Id]; + DiscordGuild discordGuild = oldGuild; oldGuild = new DiscordGuild { - Discord = gld.Discord, - Name = gld.Name, - _afkChannelId = gld._afkChannelId, - AfkTimeout = gld.AfkTimeout, - DefaultMessageNotifications = gld.DefaultMessageNotifications, - ExplicitContentFilter = gld.ExplicitContentFilter, - Features = gld.Features, - IconHash = gld.IconHash, - Id = gld.Id, - IsLarge = gld.IsLarge, - _isSynced = gld._isSynced, - IsUnavailable = gld.IsUnavailable, - JoinedAt = gld.JoinedAt, - MemberCount = gld.MemberCount, - MaxMembers = gld.MaxMembers, - MaxPresences = gld.MaxPresences, - ApproximateMemberCount = gld.ApproximateMemberCount, - ApproximatePresenceCount = gld.ApproximatePresenceCount, - MaxVideoChannelUsers = gld.MaxVideoChannelUsers, - DiscoverySplashHash = gld.DiscoverySplashHash, - PreferredLocale = gld.PreferredLocale, - MfaLevel = gld.MfaLevel, - OwnerId = gld.OwnerId, - SplashHash = gld.SplashHash, - _systemChannelId = gld._systemChannelId, - SystemChannelFlags = gld.SystemChannelFlags, - WidgetEnabled = gld.WidgetEnabled, - _widgetChannelId = gld._widgetChannelId, - VerificationLevel = gld.VerificationLevel, - _rulesChannelId = gld._rulesChannelId, - _publicUpdatesChannelId = gld._publicUpdatesChannelId, - _voiceRegionId = gld._voiceRegionId, - PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, - IsNSFW = gld.IsNSFW, + Discord = discordGuild.Discord, + Name = discordGuild.Name, + _afkChannelId = discordGuild._afkChannelId, + AfkTimeout = discordGuild.AfkTimeout, + DefaultMessageNotifications = discordGuild.DefaultMessageNotifications, + ExplicitContentFilter = discordGuild.ExplicitContentFilter, + Features = discordGuild.Features, + IconHash = discordGuild.IconHash, + Id = discordGuild.Id, + IsLarge = discordGuild.IsLarge, + _isSynced = discordGuild._isSynced, + IsUnavailable = discordGuild.IsUnavailable, + JoinedAt = discordGuild.JoinedAt, + MemberCount = discordGuild.MemberCount, + MaxMembers = discordGuild.MaxMembers, + MaxPresences = discordGuild.MaxPresences, + ApproximateMemberCount = discordGuild.ApproximateMemberCount, + ApproximatePresenceCount = discordGuild.ApproximatePresenceCount, + MaxVideoChannelUsers = discordGuild.MaxVideoChannelUsers, + DiscoverySplashHash = discordGuild.DiscoverySplashHash, + PreferredLocale = discordGuild.PreferredLocale, + MfaLevel = discordGuild.MfaLevel, + OwnerId = discordGuild.OwnerId, + SplashHash = discordGuild.SplashHash, + _systemChannelId = discordGuild._systemChannelId, + SystemChannelFlags = discordGuild.SystemChannelFlags, + WidgetEnabled = discordGuild.WidgetEnabled, + _widgetChannelId = discordGuild._widgetChannelId, + VerificationLevel = discordGuild.VerificationLevel, + _rulesChannelId = discordGuild._rulesChannelId, + _publicUpdatesChannelId = discordGuild._publicUpdatesChannelId, + _voiceRegionId = discordGuild._voiceRegionId, + PremiumProgressBarEnabled = discordGuild.PremiumProgressBarEnabled, + IsNSFW = discordGuild.IsNSFW, _channels = new ConcurrentDictionary(), _threads = new ConcurrentDictionary(), _emojis = new ConcurrentDictionary(), @@ -1116,208 +1282,245 @@ internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMember _voiceStates = new ConcurrentDictionary() }; - foreach (KeyValuePair kvp in gld._channels ??= new()) + foreach (KeyValuePair keyedChannel in discordGuild._channels ??= + new ConcurrentDictionary()) { - oldGuild._channels[kvp.Key] = kvp.Value; + oldGuild._channels[keyedChannel.Key] = keyedChannel.Value; } - foreach (KeyValuePair kvp in gld._threads ??= new()) + foreach (KeyValuePair keyedThreadChannel in discordGuild._threads ??= + new ConcurrentDictionary()) { - oldGuild._threads[kvp.Key] = kvp.Value; + oldGuild._threads[keyedThreadChannel.Key] = keyedThreadChannel.Value; } - foreach (KeyValuePair kvp in gld._emojis ??= new()) + foreach (KeyValuePair keyedEmoji in discordGuild._emojis ??= + new ConcurrentDictionary()) { - oldGuild._emojis[kvp.Key] = kvp.Value; + oldGuild._emojis[keyedEmoji.Key] = keyedEmoji.Value; } - foreach (KeyValuePair kvp in gld._roles ??= new()) + foreach (KeyValuePair keyedRole in discordGuild._roles ??= + new ConcurrentDictionary()) { - oldGuild._roles[kvp.Key] = kvp.Value; + oldGuild._roles[keyedRole.Key] = keyedRole.Value; } + //new ConcurrentDictionary() - foreach (KeyValuePair kvp in gld._voiceStates ??= new()) + foreach (KeyValuePair keyedVoicestate in discordGuild._voiceStates ??= + new ConcurrentDictionary()) { - oldGuild._voiceStates[kvp.Key] = kvp.Value; + oldGuild._voiceStates[keyedVoicestate.Key] = keyedVoicestate.Value; } - foreach (KeyValuePair kvp in gld._members ??= new()) + foreach (KeyValuePair keyedMember in discordGuild._members ??= + new ConcurrentDictionary()) { - oldGuild._members[kvp.Key] = kvp.Value; + oldGuild._members[keyedMember.Key] = keyedMember.Value; } } guild.Discord = this; guild.IsUnavailable = false; DiscordGuild eventGuild = guild; - guild = this._guilds[eventGuild.Id]; + guild = (await this.Cache.TryGetGuildAsync(guild.Id))!; - if (guild._channels == null) + if (guild._channels is null) { guild._channels = new ConcurrentDictionary(); } - if (guild._threads == null) + if (guild._threads is null) { guild._threads = new ConcurrentDictionary(); } - if (guild._roles == null) + if (guild._roles is null) { guild._roles = new ConcurrentDictionary(); } - if (guild._emojis == null) + if (guild._emojis is null) { guild._emojis = new ConcurrentDictionary(); } - if (guild._voiceStates == null) + if (guild._voiceStates is null) { guild._voiceStates = new ConcurrentDictionary(); } - if (guild._members == null) + if (guild._members is null) { guild._members = new ConcurrentDictionary(); } - this.UpdateCachedGuild(eventGuild, rawMembers); + this.UpdateCachedGuildAsync(eventGuild, rawMembers); - foreach (DiscordChannel xc in guild._channels.Values) + foreach (DiscordChannel channel in guild._channels.Values) { - xc.GuildId = guild.Id; - xc.Discord = this; - foreach (DiscordOverwrite xo in xc._permissionOverwrites) + channel.GuildId = guild.Id; + channel.Discord = this; + foreach (DiscordOverwrite channelOverwrite in channel._permissionOverwrites) { - xo.Discord = this; - xo._channel_id = xc.Id; + channelOverwrite.Discord = this; + channelOverwrite._channel_id = channel.Id; } } - foreach (DiscordThreadChannel xc in guild._threads.Values) + + foreach (DiscordThreadChannel threadChannel in guild._threads.Values) { - xc.GuildId = guild.Id; - xc.Discord = this; + threadChannel.GuildId = guild.Id; + threadChannel.Discord = this; } - foreach (DiscordEmoji xe in guild._emojis.Values) + + foreach (DiscordEmoji emoji in guild._emojis.Values) { - xe.Discord = this; + emoji.Discord = this; } - foreach (DiscordVoiceState xvs in guild._voiceStates.Values) + foreach (DiscordVoiceState discordVoiceState in guild._voiceStates.Values) { - xvs.Discord = this; + discordVoiceState.Discord = this; } - foreach (DiscordRole xr in guild._roles.Values) + foreach (DiscordRole discordRole in guild._roles.Values) { - xr.Discord = this; - xr._guild_id = guild.Id; + discordRole.Discord = this; + discordRole._guild_id = guild.Id; } - await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs { GuildBefore = oldGuild, GuildAfter = guild }); + await this._guildUpdated.InvokeAsync(this, + new GuildUpdateEventArgs {GuildBefore = oldGuild, GuildAfter = guild}); } internal async Task OnGuildDeleteEventAsync(DiscordGuild guild, JArray rawMembers) { + DiscordGuild? cachedGuild = await this.Cache.TryGetGuildAsync(guild.Id); if (guild.IsUnavailable) { - if (!this._guilds.TryGetValue(guild.Id, out DiscordGuild? gld)) + if (cachedGuild is not null) { - return; + cachedGuild.IsUnavailable = true; } - gld.IsUnavailable = true; - - await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs { Guild = guild, Unavailable = true }); + await this._guildUnavailable.InvokeAsync(this, + new GuildDeleteEventArgs {Guild = guild, Unavailable = true}); } else { - if (!this._guilds.TryRemove(guild.Id, out DiscordGuild? gld)) - { - return; - } + cachedGuild ??= guild; - await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs { Guild = gld }); + await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs {Guild = cachedGuild}); } } - - internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) - { - ConcurrentDictionary oldEmojis = new ConcurrentDictionary(guild._emojis); - guild._emojis.Clear(); - foreach (DiscordEmoji emoji in newEmojis) + internal async Task OnGuildEmojisUpdateEventAsync(ulong guildId, IEnumerable newEmojis) + { + List newEmojisList = newEmojis.ToList(); + foreach (DiscordEmoji emoji in newEmojisList) { emoji.Discord = this; - guild._emojis[emoji.Id] = emoji; } - GuildEmojisUpdateEventArgs ea = new GuildEmojisUpdateEventArgs + ConcurrentDictionary? oldEmojis = null; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + if (guild is not null) + { + oldEmojis = new ConcurrentDictionary(guild._emojis); + guild._emojis.Clear(); + + foreach (DiscordEmoji emoji in newEmojisList) + { + guild._emojis[emoji.Id] = emoji; + } + } + + + GuildEmojisUpdateEventArgs ea = new() { - Guild = guild, - EmojisAfter = guild.Emojis, - EmojisBefore = oldEmojis + Guild = guild, EmojisAfter = newEmojisList.ToDictionary(x => x.Id), EmojisBefore = oldEmojis }; await this._guildEmojisUpdated.InvokeAsync(this, ea); } - internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) + internal async Task OnGuildIntegrationsUpdateEventAsync(ulong guildId) { - GuildIntegrationsUpdateEventArgs ea = new GuildIntegrationsUpdateEventArgs - { - Guild = guild - }; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + GuildIntegrationsUpdateEventArgs ea = new() {Guild = guild, GuildId = guildId}; await this._guildIntegrationsUpdated.InvokeAsync(this, ea); } - private async Task OnGuildAuditLogEntryCreateEventAsync(DiscordGuild guild, DiscordAuditLogEntry auditLogEntry) + private async Task OnGuildAuditLogEntryCreateEventAsync(ulong guildId, AuditLogAction auditLogAction) { + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + + AuditLogParser parser = new(this, guildId, guild); + DiscordAuditLogEntry? auditLogEntry = await parser.ParseAuditLogEntryAsync(auditLogAction); + + if (auditLogEntry is null) + { + return; + } + GuildAuditLogCreatedEventArgs ea = new() { - Guild = guild, - AuditLogEntry = auditLogEntry + Guild = new CachedEntity(guildId, guild), AuditLogEntry = auditLogEntry! }; - await _guildAuditLogCreated.InvokeAsync(this, ea); + await this._guildAuditLogCreated.InvokeAsync(this, ea); } #endregion #region Guild Ban - internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) + internal async Task OnGuildBanAddEventAsync(TransportUser user, ulong guildId) { - DiscordUser usr = new DiscordUser(user) { Discord = this }; - usr = this.UpdateUserCache(usr); + DiscordUser usr = new(user) {Discord = this}; + await this.Cache.AddIfNotPresentAsync(usr, usr.GetCacheKey()); - if (!guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordMember? member = await this.Cache.TryGetMemberAsync(guildId, user.Id); + + if (guild is not null) { - mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; + if (guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) + { + member ??= mbr; + } } - GuildBanAddEventArgs ea = new GuildBanAddEventArgs - { - Guild = guild, - Member = mbr - }; + member ??= new DiscordMember(usr) {Discord = this, _guild_id = guildId}; + + CachedEntity cachedMember = new(user.Id, member); + CachedEntity cachedGuild = new(guildId, guild); + + GuildBanAddEventArgs ea = new() {Guild = cachedGuild, Member = cachedMember}; await this._guildBanAdded.InvokeAsync(this, ea); } - internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) + internal async Task OnGuildBanRemoveEventAsync(TransportUser user, ulong guildId) { - DiscordUser usr = new DiscordUser(user) { Discord = this }; - usr = this.UpdateUserCache(usr); + DiscordUser usr = new(user) {Discord = this}; + await this.Cache.AddIfNotPresentAsync(usr, usr.GetCacheKey()); + + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordMember? member = await this.Cache.TryGetMemberAsync(guildId, user.Id); - if (!guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) + if (guild is not null) { - mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; + if (guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) + { + member ??= mbr; + } } - GuildBanRemoveEventArgs ea = new GuildBanRemoveEventArgs - { - Guild = guild, - Member = mbr - }; + member ??= new DiscordMember(usr) {Discord = this, _guild_id = guildId}; + + CachedEntity cachedMember = new(user.Id, member); + CachedEntity cachedGuild = new(guildId, guild); + + GuildBanRemoveEventArgs ea = new() {Guild = cachedGuild, Member = cachedMember}; await this._guildBanRemoved.InvokeAsync(this, ea); } @@ -1325,109 +1528,132 @@ internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild #region Guild Member - internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) + internal async Task OnGuildMemberAddEventAsync(TransportMember member, ulong guildId) { - DiscordUser usr = new DiscordUser(member.User) { Discord = this }; - usr = this.UpdateUserCache(usr); + DiscordUser usr = new(member.User) {Discord = this}; + await this.Cache.AddIfNotPresentAsync(usr, usr.GetCacheKey()); + + DiscordMember mbr = new(member) {Discord = this, _guild_id = guildId}; + await this.Cache.AddIfNotPresentAsync(mbr, mbr.GetCacheKey()); - DiscordMember mbr = new DiscordMember(member) + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + if (guild is not null) { - Discord = this, - _guild_id = guild.Id - }; + guild._members[mbr.Id] = mbr; + guild.MemberCount++; + } - guild._members[mbr.Id] = mbr; - guild.MemberCount++; + CachedEntity cachedGuild = new(guildId, guild); - GuildMemberAddEventArgs ea = new GuildMemberAddEventArgs - { - Guild = guild, - Member = mbr - }; + GuildMemberAddEventArgs ea = new() {Guild = cachedGuild, Member = mbr}; await this._guildMemberAdded.InvokeAsync(this, ea); } - internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) + internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, ulong guildId) { - DiscordUser usr = new DiscordUser(user); + DiscordUser usr = new(user); + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordMember? member = await this.Cache.TryGetMemberAsync(guildId, user.Id); - if (!guild._members.TryRemove(user.Id, out DiscordMember? mbr)) + if (guild is not null) { - mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; + if (guild._members.TryRemove(user.Id, out DiscordMember? mbr)) + { + member ??= mbr; + } + + guild.MemberCount--; } - guild.MemberCount--; + member ??= new DiscordMember(new TransportMember() {User = user}) {Discord = this, _guild_id = guildId}; - this.UpdateUserCache(usr); + await this.Cache.AddIfNotPresentAsync(usr, usr.GetCacheKey()); + await this.Cache.RemoveMemberAsync(user.Id, guildId); - GuildMemberRemoveEventArgs ea = new GuildMemberRemoveEventArgs - { - Guild = guild, - Member = mbr - }; + CachedEntity cachedGuild = new(guildId, guild); + CachedEntity cachedMember = new(user.Id, member); + + GuildMemberRemoveEventArgs ea = new() {Guild = cachedGuild, Member = cachedMember}; await this._guildMemberRemoved.InvokeAsync(this, ea); } - internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild) + internal async Task OnGuildMemberUpdateEventAsync(TransportMember transportMember, ulong guildId) { - DiscordUser userAfter = new DiscordUser(member.User) { Discord = this }; - _ = this.UpdateUserCache(userAfter); + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordMember? member = await this.Cache.TryGetMemberAsync(guildId, transportMember.User.Id); - DiscordMember memberAfter = new DiscordMember(member) { Discord = this, _guild_id = guild.Id }; + DiscordUser userAfter = new(transportMember.User) {Discord = this}; + await this.Cache.AddIfNotPresentAsync(userAfter, userAfter.GetCacheKey()); - if (!guild.Members.TryGetValue(member.User.Id, out DiscordMember? memberBefore)) + DiscordMember memberAfter = new(transportMember) {Discord = this, _guild_id = guildId}; + + if (guild is not null) { - memberBefore = new DiscordMember(member) { Discord = this, _guild_id = guild.Id }; - } + if (guild.Members.TryGetValue(transportMember.User.Id, out DiscordMember? memberBefore)) + { + member ??= memberBefore; + } - guild._members.AddOrUpdate(member.User.Id, memberAfter, (_, _) => memberAfter); + guild._members.AddOrUpdate(transportMember.User.Id, memberAfter, (_, _) => memberAfter); + } - GuildMemberUpdateEventArgs ea = new GuildMemberUpdateEventArgs + member ??= new DiscordMember(new TransportMember() {User = transportMember.User}) { - Guild = guild, - MemberAfter = memberAfter, - MemberBefore = memberBefore, + Discord = this, _guild_id = guildId }; + CachedEntity cachedGuild = new(guildId, guild); + + GuildMemberUpdateEventArgs ea = new() {Guild = cachedGuild, MemberAfter = memberAfter, MemberBefore = member}; + await this._guildMemberUpdated.InvokeAsync(this, ea); } internal async Task OnGuildMembersChunkEventAsync(JObject dat) { - DiscordGuild guild = this.Guilds[(ulong)dat["guild_id"]]; + ulong guildId = (ulong)dat["guild_id"]; int chunkIndex = (int)dat["chunk_index"]; int chunkCount = (int)dat["chunk_count"]; string? nonce = (string)dat["nonce"]; - HashSet mbrs = new HashSet(); - HashSet pres = new HashSet(); + DiscordGuild? cachedGuild = await this.Cache.TryGetGuildAsync(guildId); + + HashSet discordMembers = new(); + HashSet discordPresences = new(); TransportMember[] members = dat["members"].ToDiscordObject(); int memCount = members.Count(); for (int i = 0; i < memCount; i++) { - DiscordMember mbr = new DiscordMember(members[i]) { Discord = this, _guild_id = guild.Id }; + DiscordMember member = new(members[i]) {Discord = this, _guild_id = guildId}; + + DiscordUser newUser = new(members[i].User) {Discord = this}; - if (!this.UserCache.ContainsKey(mbr.Id)) + await this.Cache.AddIfNotPresentAsync(newUser, newUser.GetCacheKey()); + + if (cachedGuild is not null) { - this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; + cachedGuild._members[member.Id] = member; } - guild._members[mbr.Id] = mbr; + discordMembers.Add(member); + } - mbrs.Add(mbr); + if (cachedGuild is not null) + { + cachedGuild.MemberCount = cachedGuild._members.Count; } - guild.MemberCount = guild._members.Count; + CachedEntity cachedGuildEntity = new(guildId, cachedGuild); - GuildMembersChunkEventArgs ea = new GuildMembersChunkEventArgs + GuildMembersChunkEventArgs ea = new() { - Guild = guild, - Members = new ReadOnlySet(mbrs), + Guild = cachedGuildEntity, + Members = new ReadOnlySet(discordMembers), ChunkIndex = chunkIndex, ChunkCount = chunkCount, - Nonce = nonce, + Nonce = nonce }; if (dat["presences"] != null) @@ -1450,10 +1676,10 @@ internal async Task OnGuildMembersChunkEventAsync(JObject dat) } } - pres.Add(xp); + discordPresences.Add(xp); } - ea.Presences = new ReadOnlySet(pres); + ea.Presences = new ReadOnlySet(discordPresences); } if (dat["not_found"] != null) @@ -1469,72 +1695,56 @@ internal async Task OnGuildMembersChunkEventAsync(JObject dat) #region Guild Role - internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) + internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, ulong guildId) { role.Discord = this; - role._guild_id = guild.Id; + role._guild_id = guildId; - guild._roles[role.Id] = role; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); - GuildRoleCreateEventArgs ea = new GuildRoleCreateEventArgs + if (guild is not null) { - Guild = guild, - Role = role - }; + guild._roles[role.Id] = role; + } + + CachedEntity cachedGuild = new(guildId, guild); + + GuildRoleCreateEventArgs ea = new() {Guild = cachedGuild, Role = role}; await this._guildRoleCreated.InvokeAsync(this, ea); } - internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) + internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, ulong guildId) { - DiscordRole newRole = guild.GetRole(role.Id); - DiscordRole oldRole = new DiscordRole + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + + DiscordRole? newRole = role; + DiscordRole? oldRole = null; + + if (guild is not null) { - _guild_id = guild.Id, - _color = newRole._color, - Discord = this, - IsHoisted = newRole.IsHoisted, - Id = newRole.Id, - IsManaged = newRole.IsManaged, - IsMentionable = newRole.IsMentionable, - Name = newRole.Name, - Permissions = newRole.Permissions, - Position = newRole.Position, - IconHash = newRole.IconHash, - _emoji = newRole._emoji - }; + oldRole = guild.GetRole(role.Id); + } - newRole._guild_id = guild.Id; - newRole._color = role._color; - newRole.IsHoisted = role.IsHoisted; - newRole.IsManaged = role.IsManaged; - newRole.IsMentionable = role.IsMentionable; - newRole.Name = role.Name; - newRole.Permissions = role.Permissions; - newRole.Position = role.Position; - newRole._emoji = role._emoji; - newRole.IconHash = role.IconHash; - - GuildRoleUpdateEventArgs ea = new GuildRoleUpdateEventArgs - { - Guild = guild, - RoleAfter = newRole, - RoleBefore = oldRole - }; + CachedEntity cachedGuild = new(guildId, guild); + + GuildRoleUpdateEventArgs ea = new() {Guild = cachedGuild, RoleAfter = newRole, RoleBefore = oldRole}; await this._guildRoleUpdated.InvokeAsync(this, ea); } - internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) + internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, ulong guildId) { - if (!guild._roles.TryRemove(roleId, out DiscordRole? role)) + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordRole? role = null; + + if (guild is not null) { - this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild})."); + guild._roles.TryRemove(roleId, out role); } - GuildRoleDeleteEventArgs ea = new GuildRoleDeleteEventArgs - { - Guild = guild, - Role = role - }; + CachedEntity cachedGuild = new(guildId, guild); + CachedEntity cachedRole = new(roleId, role); + + GuildRoleDeleteEventArgs ea = new() {Guild = cachedGuild, Role = cachedRole}; await this._guildRoleDeleted.InvokeAsync(this, ea); } @@ -1544,64 +1754,70 @@ internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordChannel channel = this.InternalGetCachedChannel(channelId); + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); invite.Discord = this; - guild._invites[invite.Code] = invite; - - InviteCreateEventArgs ea = new InviteCreateEventArgs + if (guild is not null) { - Channel = channel, - Guild = guild, - Invite = invite - }; + guild._invites[invite.Code] = invite; + } + + CachedEntity cachedGuild = new(guildId, guild); + CachedEntity cachedChannel = new(channelId, channel); + + InviteCreateEventArgs ea = new() {Channel = cachedChannel, Guild = cachedGuild, Invite = invite}; await this._inviteCreated.InvokeAsync(this, ea); } internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordChannel channel = this.InternalGetCachedChannel(channelId); + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + DiscordInvite? invite = null; - if (!guild._invites.TryRemove(dat["code"].ToString(), out DiscordInvite? invite)) + if (guild is not null) { - invite = dat.ToDiscordObject(); - invite.Discord = this; + if (guild._invites.TryRemove(dat["code"].ToString(), out DiscordInvite? cachedInvite)) + { + invite = cachedInvite; + } } + invite ??= dat.ToDiscordObject(); + invite.Discord = this; invite.IsRevoked = true; - InviteDeleteEventArgs ea = new InviteDeleteEventArgs - { - Channel = channel, - Guild = guild, - Invite = invite - }; + CachedEntity cachedGuild = new(guildId, guild); + CachedEntity cachedChannel = new(channelId, channel); + + InviteDeleteEventArgs ea = new() {Channel = cachedChannel, Guild = cachedGuild, Invite = invite}; await this._inviteDeleted.InvokeAsync(this, ea); } #endregion #region Message - - internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) + + internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, + TransportUser referenceAuthor, TransportMember referenceMember) { message.Discord = this; - this.PopulateMessageReactionsAndCache(message, author, member); - message.PopulateMentions(); + this.PopulateMessageReactionsAndCacheAsync(message, author, member); + await message.PopulateMentionsAsync(); if (message.Channel == null && message.ChannelId == default) { - this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!"); + this.Logger.LogWarning(LoggerEvents.WebSocketReceive, + "Channel which the last message belongs to is not in cache - cache state might be invalid!"); } if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; - this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); - message.ReferencedMessage.PopulateMentions(); + this.PopulateMessageReactionsAndCacheAsync(message.ReferencedMessage, referenceAuthor, referenceMember); + await message.ReferencedMessage.PopulateMentionsAsync(); } foreach (DiscordMessageSticker sticker in message.Stickers) @@ -1609,162 +1825,127 @@ internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportU sticker.Discord = this; } - MessageCreateEventArgs ea = new MessageCreateEventArgs + MessageCreateEventArgs ea = new() { Message = message, - MentionedUsers = new ReadOnlyCollection(message._mentionedUsers), - MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, - MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null + MentionedRoles = + message._mentionedRoles != null + ? new ReadOnlyCollection(message._mentionedRoles) + : null, + MentionedChannels = message._mentionedChannels != null + ? new ReadOnlyCollection(message._mentionedChannels) + : null }; await this._messageCreated.InvokeAsync(this, ea); } - internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) + internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, + TransportUser referenceAuthor, TransportMember referenceMember) { - DiscordGuild guild; - message.Discord = this; - DiscordMessage event_message = message; + DiscordMessage eventMessage = message; - DiscordMessage oldmsg = null; - if (this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(event_message.Id, out message)) // previous message was not in cache + DiscordMessage? oldMessage = await this.Cache.TryGetMessageAsync(message.Id); + if (oldMessage is null) // previous message was not in cache { - message = event_message; - this.PopulateMessageReactionsAndCache(message, author, member); - guild = message.Channel?.Guild; + message = eventMessage; + this.PopulateMessageReactionsAndCacheAsync(message, author, member); if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; - this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); - message.ReferencedMessage.PopulateMentions(); + this.PopulateMessageReactionsAndCacheAsync(message.ReferencedMessage, referenceAuthor, referenceMember); + await message.ReferencedMessage.PopulateMentionsAsync(); } } else // previous message was fetched in cache { - oldmsg = new DiscordMessage(message); + oldMessage = new DiscordMessage(message); // cached message is updated with information from the event message - guild = message.Channel?.Guild; - message.EditedTimestamp = event_message.EditedTimestamp; - if (event_message.Content != null) + message.EditedTimestamp = eventMessage.EditedTimestamp; + if (eventMessage.Content != null) { - message.Content = event_message.Content; + message.Content = eventMessage.Content; } message._embeds.Clear(); - message._embeds.AddRange(event_message._embeds); + message._embeds.AddRange(eventMessage._embeds); message._attachments.Clear(); - message._attachments.AddRange(event_message._attachments); - message.Pinned = event_message.Pinned; - message.IsTTS = event_message.IsTTS; + message._attachments.AddRange(eventMessage._attachments); + message.Pinned = eventMessage.Pinned; + message.IsTTS = eventMessage.IsTTS; // Mentions message._mentionedUsers.Clear(); - message._mentionedUsers.AddRange(event_message._mentionedUsers ?? new()); + message._mentionedUsers.AddRange(eventMessage._mentionedUsers ?? new List()); message._mentionedRoles.Clear(); - message._mentionedRoles.AddRange(event_message._mentionedRoles ?? new()); + message._mentionedRoles.AddRange(eventMessage._mentionedRoles ?? new List()); message._mentionedChannels.Clear(); - message._mentionedChannels.AddRange(event_message._mentionedChannels ?? new()); - message.MentionEveryone = event_message.MentionEveryone; + message._mentionedChannels.AddRange(eventMessage._mentionedChannels ?? new List()); + message.MentionEveryone = eventMessage.MentionEveryone; } - message.PopulateMentions(); + await message.PopulateMentionsAsync(); - MessageUpdateEventArgs ea = new MessageUpdateEventArgs + MessageUpdateEventArgs ea = new() { Message = message, - MessageBefore = oldmsg, + MessageBefore = oldMessage, MentionedUsers = new ReadOnlyCollection(message._mentionedUsers), - MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, - MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null + MentionedRoles = + message._mentionedRoles != null + ? new ReadOnlyCollection(message._mentionedRoles) + : null, + MentionedChannels = message._mentionedChannels != null + ? new ReadOnlyCollection(message._mentionedChannels) + : null }; await this._messageUpdated.InvokeAsync(this, ea); } internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordChannel? channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); + DiscordGuild? guild = guildId.HasValue ? await this.Cache.TryGetGuildAsync(guildId.Value) : null; + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + DiscordMessage? msg = await this.Cache.TryGetMessageAsync(messageId); - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = ChannelType.Private, - Recipients = Array.Empty() - - }; - this._privateChannels[channelId] = (DiscordDmChannel)channel; - } - - if (channel == null - || this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - - Id = messageId, - ChannelId = channelId, - Discord = this, - }; - } - - if (this.Configuration.MessageCacheSize > 0) + MessageDeleteEventArgs ea = new() { - this.MessageCache?.Remove(msg.Id); - } - - MessageDeleteEventArgs ea = new MessageDeleteEventArgs - { - Message = msg, - Channel = channel, - Guild = guild, + Message = new CachedEntity(messageId, msg), + Channel = new CachedEntity(channelId, channel), + Guild = guildId.HasValue ? new CachedEntity(guildId.Value, guild) : null }; await this._messageDeleted.InvokeAsync(this, ea); } internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) { - DiscordChannel? channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); + DiscordChannel? cachedChannel = await this.Cache.TryGetChannelAsync(channelId); + DiscordGuild? cachedGuild = null; + if (guildId.HasValue) + { + cachedGuild = await this.Cache.TryGetGuildAsync(guildId.Value); + } - List msgs = new List(messageIds.Length); + List msgs = new(messageIds.Length); foreach (ulong messageId in messageIds) { - if (channel == null - || this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this, - }; - } - if (this.Configuration.MessageCacheSize > 0) - { - this.MessageCache?.Remove(msg.Id); - } + DiscordMessage? cachedMessage = await this.Cache.TryGetMessageAsync(messageId); - msgs.Add(msg); - } + cachedMessage ??= new DiscordMessage {Id = messageId, ChannelId = channelId, Discord = this}; - DiscordGuild guild = this.InternalGetCachedGuild(guildId); + msgs.Add(cachedMessage); + } - MessageBulkDeleteEventArgs ea = new MessageBulkDeleteEventArgs + MessageBulkDeleteEventArgs ea = new() { - Channel = channel, + Channel = cachedChannel, + ChannelId = channelId, Messages = new ReadOnlyCollection(msgs), - Guild = guild + Guild = cachedGuild, + GuildId = guildId }; await this._messagesBulkDeleted.InvokeAsync(this, ea); } @@ -1773,66 +1954,44 @@ internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong chan #region Message Reaction - internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) + internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, + TransportMember? mbr, DiscordEmoji emoji) { - DiscordChannel? channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); - DiscordGuild? guild = this.InternalGetCachedGuild(guildId); - + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + DiscordGuild? guild = guildId.HasValue ? await this.Cache.TryGetGuildAsync(guildId.Value) : null; + DiscordMessage? cachedMessage = await this.Cache.TryGetMessageAsync(messageId); emoji.Discord = this; - DiscordUser usr = null!; - usr = !this.TryGetCachedUserInternal(userId, out usr) - ? this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr) - : this.UpdateUser(usr, guild?.Id, guild, mbr); - - if (channel == null) + DiscordUser? usr; + usr = await this.Cache.TryGetUserAsync(userId); + if (usr is null && mbr is not null) { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = ChannelType.Private, - Recipients = new DiscordUser[] { usr } - }; - this._privateChannels[channelId] = (DiscordDmChannel)channel; + //I asume the transportMember has enough information to create a DiscordUser + usr = await this.UpdateUserAsync(new DiscordUser {Id = userId, Discord = this}, guildId, mbr); } - - if (channel == null - || this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + DiscordReaction? react = cachedMessage?._reactions.FirstOrDefault(xr => xr.Emoji == emoji); + if (react is not null) { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this, - _reactions = new List() - }; + react.Count++; + react.IsMe |= this.CurrentUser.Id == userId; } - - DiscordReaction? react = msg._reactions.FirstOrDefault(xr => xr.Emoji == emoji); - if (react == null) + else if (cachedMessage is not null) { - msg._reactions.Add(react = new DiscordReaction - { - Count = 1, - Emoji = emoji, - IsMe = this.CurrentUser.Id == userId - }); + react = new DiscordReaction {Count = 1, Emoji = emoji, IsMe = this.CurrentUser.Id == userId}; + cachedMessage._reactions.Add(react); } - else + + if (guildId.HasValue) { - react.Count++; - react.IsMe |= this.CurrentUser.Id == userId; + usr ??= await this.Cache.TryGetMemberAsync(guildId.Value, userId); } - MessageReactionAddEventArgs ea = new MessageReactionAddEventArgs + MessageReactionAddEventArgs ea = new() { - Message = msg, - User = usr, - Guild = guild, + Message = new CachedEntity(messageId, cachedMessage), + User = new CachedEntity(userId, usr), + Guild = guildId.HasValue ? new CachedEntity(guildId.Value, guild) : null, Emoji = emoji }; await this._messageReactionAdded.InvokeAsync(this, ea); @@ -1840,73 +1999,43 @@ internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulo internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) { - DiscordChannel? channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); - + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + DiscordGuild? guild = guildId.HasValue ? await this.Cache.TryGetGuildAsync(guildId.Value) : null; + DiscordMessage? cachedMessage = await this.Cache.TryGetMessageAsync(messageId); emoji.Discord = this; - if (!this.UserCache.TryGetValue(userId, out DiscordUser? usr)) - { - usr = new DiscordUser { Id = userId, Discord = this }; - } - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = ChannelType.Private, - Recipients = new DiscordUser[] { usr } - }; - this._privateChannels[channelId] = (DiscordDmChannel)channel; - } - - if (channel?.Guild != null) + DiscordUser? usr; + usr = await this.Cache.TryGetUserAsync(userId); + if (guildId.HasValue) { - usr = channel.Guild.Members.TryGetValue(userId, out DiscordMember? member) - ? member - : new DiscordMember(usr) { Discord = this, _guild_id = channel.GuildId.Value }; + usr ??= await this.Cache.TryGetMemberAsync(guildId.Value, userId); } - if (channel == null - || this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this - }; - } - - DiscordReaction? react = msg._reactions?.FirstOrDefault(xr => xr.Emoji == emoji); - if (react != null) + DiscordReaction? react = cachedMessage?._reactions.FirstOrDefault(xr => xr.Emoji == emoji); + if (react is not null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; - if (msg._reactions != null && react.Count <= 0) // shit happens + if (cachedMessage._reactions != null && react.Count <= 0) // shit happens { - for (int i = 0; i < msg._reactions.Count; i++) + for (int i = 0; i < cachedMessage._reactions.Count; i++) { - if (msg._reactions[i].Emoji == emoji) + if (cachedMessage._reactions[i].Emoji == emoji) { - msg._reactions.RemoveAt(i); + cachedMessage._reactions.RemoveAt(i); break; } } } } - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - - MessageReactionRemoveEventArgs ea = new MessageReactionRemoveEventArgs + //TODO update event args to reflect nullability + MessageReactionRemoveEventArgs ea = new() { - Message = msg, - User = usr, - Guild = guild, + Message = new CachedEntity(messageId,cachedMessage), + User = new CachedEntity(userId,usr), + Guild = guildId.HasValue ? new CachedEntity(guildId.Value, guild) : null, Emoji = emoji }; await this._messageReactionRemoved.InvokeAsync(this, ea); @@ -1914,81 +2043,42 @@ internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) { - DiscordChannel channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + DiscordMessage? cachedMessage = await this.Cache.TryGetMessageAsync(messageId); + DiscordGuild? guild = guildId.HasValue ? await this.Cache.TryGetGuildAsync(guildId.Value) : null; + cachedMessage._reactions?.Clear(); - if (channel == null - || this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + MessageReactionsClearEventArgs ea = new() { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this - }; - } - - msg._reactions?.Clear(); - - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - - MessageReactionsClearEventArgs ea = new MessageReactionsClearEventArgs - { - Message = msg, + Message = new CachedEntity(messageId, cachedMessage), + Channel = new CachedEntity(channelId, channel), + Guild = guildId.HasValue ? new CachedEntity(guildId.Value, guild) : null }; - await this._messageReactionsCleared.InvokeAsync(this, ea); } - internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) + internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong? guildId, + DiscordEmoji partialEmoji) { - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordChannel? channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = ChannelType.Private, - Recipients = Array.Empty() - }; - this._privateChannels[channelId] = (DiscordDmChannel)channel; - } + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(channelId); + DiscordMessage? cachedMessage = await this.Cache.TryGetMessageAsync(messageId); + DiscordGuild? guild = guildId.HasValue ? await this.Cache.TryGetGuildAsync(guildId.Value) : null; - if (channel == null - || this.Configuration.MessageCacheSize == 0 - || this.MessageCache == null - || !this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this - }; - } + cachedMessage._reactions?.RemoveAll(r => r.Emoji.Equals(partialEmoji)); - DiscordEmoji partialEmoji = dat.ToDiscordObject(); - - if (!guild._emojis.TryGetValue(partialEmoji.Id, out DiscordEmoji? emoji)) + if (!(guild?._emojis.TryGetValue(partialEmoji.Id, out DiscordEmoji? emoji) ?? false)) { emoji = partialEmoji; emoji.Discord = this; } - msg._reactions?.RemoveAll(r => r.Emoji.Equals(emoji)); - - MessageReactionRemoveEmojiEventArgs ea = new MessageReactionRemoveEmojiEventArgs + MessageReactionRemoveEmojiEventArgs ea = new() { - Message = msg, - Channel = channel, - Guild = guild, + Message = new CachedEntity(messageId, cachedMessage), + Channel = new CachedEntity(channelId, channel), + Guild = guildId.HasValue ? new CachedEntity(guildId.Value, guild) : null, Emoji = emoji }; - await this._messageReactionRemovedEmoji.InvokeAsync(this, ea); } @@ -1996,12 +2086,14 @@ internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong cha #region User/Presence Update + //TODO: performance improvements internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) { - ulong uid = (ulong)rawUser["id"]; - DiscordPresence old = null; + ulong userId = (ulong)rawUser["id"]; + DiscordPresence? old = null; + DiscordPresence? presence = await this.Cache.TryGetUserPresenceAsync(userId); - if (this._presences.TryGetValue(uid, out DiscordPresence? presence)) + if (presence is not null) { old = new DiscordPresence(presence); DiscordJson.PopulateObject(rawPresence, presence); @@ -2011,7 +2103,7 @@ internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawU presence = rawPresence.ToDiscordObject(); presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); - this._presences[presence.InternalUser.Id] = presence; + this.Cache.AddUserPresenceAsync(presence); } // reuse arrays / avoid linq (this is a hot zone) @@ -2050,18 +2142,14 @@ internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawU presence.Activity = null; } } - - // Caching partial objects is not a good idea, but considering these - // Objects will most likely be GC'd immediately after this event, - // This probably isn't great for GC pressure because this is a hot zone. - _ = this.UserCache.TryGetValue(uid, out DiscordUser? usr); - + + DiscordUser? usr = await this.Cache.TryGetUserAsync(userId); DiscordUser usrafter = usr ?? new DiscordUser(presence.InternalUser); - PresenceUpdateEventArgs ea = new PresenceUpdateEventArgs + PresenceUpdateEventArgs ea = new() { Status = presence.Status, Activity = presence.Activity, - User = usr, + User = new CachedEntity(userId, usrafter), PresenceBefore = old, PresenceAfter = presence, UserBefore = old != null ? new DiscordUser(old.InternalUser) { Discord = this } : usrafter, @@ -2072,18 +2160,15 @@ internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawU internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) { - DiscordUser usr = new DiscordUser(user) { Discord = this }; + DiscordUser usr = new(user) {Discord = this}; - UserSettingsUpdateEventArgs ea = new UserSettingsUpdateEventArgs - { - User = usr - }; + UserSettingsUpdateEventArgs ea = new() {User = usr}; await this._userSettingsUpdated.InvokeAsync(this, ea); } internal async Task OnUserUpdateEventAsync(TransportUser user) { - DiscordUser usr_old = new DiscordUser + DiscordUser oldUser = new() { AvatarHash = this.CurrentUser.AvatarHash, Discord = this, @@ -2105,11 +2190,7 @@ internal async Task OnUserUpdateEventAsync(TransportUser user) this.CurrentUser.Username = user.Username; this.CurrentUser.Verified = user.Verified; - UserUpdateEventArgs ea = new UserUpdateEventArgs - { - UserAfter = this.CurrentUser, - UserBefore = usr_old - }; + UserUpdateEventArgs ea = new() {UserAfter = this.CurrentUser, UserBefore = oldUser}; await this._userUpdated.InvokeAsync(this, ea); } @@ -2119,21 +2200,21 @@ internal async Task OnUserUpdateEventAsync(TransportUser user) internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { - ulong gid = (ulong)raw["guild_id"]; - ulong uid = (ulong)raw["user_id"]; - DiscordGuild gld = this._guilds[gid]; + ulong guildId = (ulong)raw["guild_id"]; + ulong userId = (ulong)raw["user_id"]; + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); DiscordVoiceState vstateNew = raw.ToDiscordObject(); vstateNew.Discord = this; + DiscordVoiceState? vstateOld = null; + guild?._voiceStates.TryRemove(userId, out vstateOld); - gld._voiceStates.TryRemove(uid, out DiscordVoiceState? vstateOld); - - if (vstateNew.Channel != null) + if (vstateNew.Channel != null && guild is not null) { - gld._voiceStates[vstateNew.UserId] = vstateNew; + guild._voiceStates[vstateNew.UserId] = vstateNew; } - if (gld._members.TryGetValue(uid, out DiscordMember? mbr)) + if (guild?._members.TryGetValue(userId, out DiscordMember? mbr) ?? false) { mbr.IsMuted = vstateNew.IsServerMuted; mbr.IsDeafened = vstateNew.IsServerDeafened; @@ -2141,29 +2222,29 @@ internal async Task OnVoiceStateUpdateEventAsync(JObject raw) else { TransportMember transportMbr = vstateNew.TransportMember; - this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); + this.UpdateUserAsync(new DiscordUser(transportMbr.User) {Discord = this}, guildId, transportMbr); } - VoiceStateUpdateEventArgs ea = new VoiceStateUpdateEventArgs + VoiceStateUpdateEventArgs ea = new() { Guild = vstateNew.Guild, Channel = vstateNew.Channel, User = vstateNew.User, SessionId = vstateNew.SessionId, - Before = vstateOld, After = vstateNew }; await this._voiceStateUpdated.InvokeAsync(this, ea); } - internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) + internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, ulong guildId) { - VoiceServerUpdateEventArgs ea = new VoiceServerUpdateEventArgs + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guildId); + VoiceServerUpdateEventArgs ea = new() { Endpoint = endpoint, VoiceToken = token, - Guild = guild + Guild = new CachedEntity(guildId, guild) }; await this._voiceServerUpdated.InvokeAsync(this, ea); } @@ -2175,9 +2256,13 @@ internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread, bool isNew) { thread.Discord = this; - this.InternalGetCachedGuild(thread.GuildId)._threads[thread.Id] = thread; + + DiscordGuild guild = thread.GuildId != default ? await this.Cache.TryGetGuildAsync(thread.GuildId) : null; + + await this.Cache.TryGetGuildAsync(thread.GuildId)._threads[thread.Id] = thread; - await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }); + await this._threadCreated.InvokeAsync(this, + new ThreadCreateEventArgs {Thread = thread, Guild = thread.Guild, Parent = thread.Parent}); } internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) @@ -2195,7 +2280,7 @@ internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) DiscordGuild guild = thread.Guild; guild.Discord = this; - DiscordThreadChannel cthread = this.InternalGetCachedThread(thread.Id); + DiscordThreadChannel cthread = this.InternalGetCachedThreadAsync(thread.Id); if (cthread != null) //thread is cached { @@ -2212,24 +2297,19 @@ internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) MessageCount = cthread.MessageCount, MemberCount = cthread.MemberCount, ThreadMetadata = cthread.ThreadMetadata, - CurrentMember = cthread.CurrentMember, + CurrentMember = cthread.CurrentMember }; updateEvent = new ThreadUpdateEventArgs { - ThreadAfter = thread, - ThreadBefore = threadOld, - Guild = thread.Guild, - Parent = thread.Parent + ThreadAfter = thread, ThreadBefore = threadOld, Guild = thread.Guild, Parent = thread.Parent }; } else { updateEvent = new ThreadUpdateEventArgs { - ThreadAfter = thread, - Guild = thread.Guild, - Parent = thread.Parent + ThreadAfter = thread, Guild = thread.Guild, Parent = thread.Parent }; guild._threads[thread.Id] = thread; } @@ -2252,13 +2332,16 @@ internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) thread = cachedThread; } - await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }); + await this._threadDeleted.InvokeAsync(this, + new ThreadDeleteEventArgs {Thread = thread, Guild = thread.Guild, Parent = thread.Parent}); } - internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channel_ids, IReadOnlyList threads, IReadOnlyList members) + internal async Task OnThreadListSyncEventAsync(ulong guildId, IReadOnlyList channelIds, + IReadOnlyList threads, IReadOnlyList members) { - guild.Discord = this; - IEnumerable channels = channel_ids.Select(x => guild.GetChannel(x) ?? new DiscordChannel { Id = x, GuildId = guild.Id }); //getting channel objects + guildId.Discord = this; + IEnumerable channels = channelIds.Select(x => + guildId.GetChannel(x) ?? new DiscordChannel {Id = x, GuildId = guildId.Id}); //getting channel objects foreach (DiscordChannel? channel in channels) { @@ -2268,13 +2351,13 @@ internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList foreach (DiscordThreadChannel thread in threads) { thread.Discord = this; - guild._threads[thread.Id] = thread; + guildId._threads[thread.Id] = thread; } foreach (DiscordThreadChannelMember member in members) { member.Discord = this; - member._guild_id = guild.Id; + member._guild_id = guildId.Id; DiscordThreadChannel? thread = threads.SingleOrDefault(x => x.Id == member.ThreadId); if (thread != null) @@ -2283,38 +2366,46 @@ internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList } } - await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, CurrentMembers = members.ToList().AsReadOnly() }); + await this._threadListSynced.InvokeAsync(this, + new ThreadListSyncEventArgs + { + Guild = guildId, + Channels = channels.ToList().AsReadOnly(), + Threads = threads, + CurrentMembers = members.ToList().AsReadOnly() + }); } internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member) { member.Discord = this; - DiscordThreadChannel thread = this.InternalGetCachedThread(member.ThreadId); + DiscordThreadChannel thread = this.InternalGetCachedThreadAsync(member.ThreadId); member._guild_id = thread.Guild.Id; thread.CurrentMember = member; thread.Guild._threads[thread.Id] = thread; - await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs { ThreadMember = member, Thread = thread }); + await this._threadMemberUpdated.InvokeAsync(this, + new ThreadMemberUpdateEventArgs {ThreadMember = member, Thread = thread}); } - internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong thread_id, IReadOnlyList addedMembers, IReadOnlyList removed_member_ids, int member_count) + internal async Task OnThreadMembersUpdateEventAsync(ulong guildId, ulong thread_id, + IReadOnlyList addedMembers, IReadOnlyList removed_member_ids, int member_count) { - DiscordThreadChannel? thread = this.InternalGetCachedThread(thread_id) ?? new DiscordThreadChannel - { - Id = thread_id, - GuildId = guild.Id, - }; + DiscordThreadChannel? thread = this.InternalGetCachedThreadAsync(thread_id) ?? + new DiscordThreadChannel {Id = thread_id, GuildId = guildId.Id}; thread.Discord = this; - guild.Discord = this; + guildId.Discord = this; thread.MemberCount = member_count; - List removedMembers = new List(); + List removedMembers = new(); if (removed_member_ids != null) { foreach (ulong? removedId in removed_member_ids) { - removedMembers.Add(guild._members.TryGetValue(removedId.Value, out DiscordMember? member) ? member : new DiscordMember { Id = removedId.Value, _guild_id = guild.Id, Discord = this }); + removedMembers.Add(guildId._members.TryGetValue(removedId.Value, out DiscordMember? member) + ? member + : new DiscordMember {Id = removedId.Value, _guild_id = guildId.Id, Discord = this}); } if (removed_member_ids.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread @@ -2332,7 +2423,7 @@ internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong th foreach (DiscordThreadChannelMember threadMember in addedMembers) { threadMember.Discord = this; - threadMember._guild_id = guild.Id; + threadMember._guild_id = guildId.Id; } if (addedMembers.Any(member => member.Id == this.CurrentUser.Id)) @@ -2345,9 +2436,9 @@ internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong th addedMembers = Array.Empty(); } - ThreadMembersUpdateEventArgs threadMembersUpdateArg = new ThreadMembersUpdateEventArgs + ThreadMembersUpdateEventArgs threadMembersUpdateArg = new() { - Guild = guild, + Guild = guildId, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, @@ -2360,53 +2451,39 @@ internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong th #endregion - #region Integration internal async Task OnIntegrationCreateAsync(DiscordIntegration integration, ulong guild_id) { - DiscordGuild? guild = this.InternalGetCachedGuild(guild_id) ?? new DiscordGuild - { - Id = guild_id, - Discord = this - }; - IntegrationCreateEventArgs ea = new IntegrationCreateEventArgs + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guild_id) ?? new DiscordGuild { - Guild = guild, - Integration = integration + Id = guild_id, Discord = this }; + IntegrationCreateEventArgs ea = new() {Guild = guild, Integration = integration}; await this._integrationCreated.InvokeAsync(this, ea); } internal async Task OnIntegrationUpdateAsync(DiscordIntegration integration, ulong guild_id) { - DiscordGuild? guild = this.InternalGetCachedGuild(guild_id) ?? new DiscordGuild + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guild_id) ?? new DiscordGuild { - Id = guild_id, - Discord = this - }; - IntegrationUpdateEventArgs ea = new IntegrationUpdateEventArgs - { - Guild = guild, - Integration = integration + Id = guild_id, Discord = this }; + IntegrationUpdateEventArgs ea = new() {Guild = guild, Integration = integration}; await this._integrationUpdated.InvokeAsync(this, ea); } internal async Task OnIntegrationDeleteAsync(ulong integration_id, ulong guild_id, ulong? application_id) { - DiscordGuild? guild = this.InternalGetCachedGuild(guild_id) ?? new DiscordGuild + DiscordGuild? guild = await this.Cache.TryGetGuildAsync(guild_id) ?? new DiscordGuild { - Id = guild_id, - Discord = this + Id = guild_id, Discord = this }; - IntegrationDeleteEventArgs ea = new IntegrationDeleteEventArgs + IntegrationDeleteEventArgs ea = new() { - Guild = guild, - Applicationid = application_id, - IntegrationId = integration_id + Guild = guild, Applicationid = application_id, IntegrationId = integration_id }; await this._integrationDeleted.InvokeAsync(this, ea); @@ -2418,7 +2495,8 @@ internal async Task OnIntegrationDeleteAsync(ulong integration_id, ulong guild_i internal async Task OnApplicationCommandPermissionsUpdateAsync(JObject obj) { - ApplicationCommandPermissionsUpdatedEventArgs? ev = obj.ToObject(); + ApplicationCommandPermissionsUpdatedEventArgs? ev = + obj.ToObject(); await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ev); } @@ -2431,14 +2509,11 @@ internal async Task OnStageInstanceCreateAsync(DiscordStageInstance instance) { instance.Discord = this; - DiscordGuild guild = this.InternalGetCachedGuild(instance.GuildId); + DiscordGuild guild = await this.Cache.TryGetGuildAsync(instance.GuildId); guild._stageInstances[instance.Id] = instance; - StageInstanceCreateEventArgs eventArgs = new StageInstanceCreateEventArgs - { - StageInstance = instance - }; + StageInstanceCreateEventArgs eventArgs = new() {StageInstance = instance}; await this._stageInstanceCreated.InvokeAsync(this, eventArgs); } @@ -2447,19 +2522,21 @@ internal async Task OnStageInstanceUpdateAsync(DiscordStageInstance instance) { instance.Discord = this; - DiscordGuild guild = this.InternalGetCachedGuild(instance.GuildId); + DiscordGuild guild = await this.Cache.TryGetGuildAsync(instance.GuildId); if (!guild._stageInstances.TryRemove(instance.Id, out DiscordStageInstance? oldInstance)) { - oldInstance = new DiscordStageInstance { Id = instance.Id, GuildId = instance.GuildId, ChannelId = instance.ChannelId }; + oldInstance = new DiscordStageInstance + { + Id = instance.Id, GuildId = instance.GuildId, ChannelId = instance.ChannelId + }; } guild._stageInstances[instance.Id] = instance; - StageInstanceUpdateEventArgs eventArgs = new StageInstanceUpdateEventArgs + StageInstanceUpdateEventArgs eventArgs = new() { - StageInstanceBefore = oldInstance, - StageInstanceAfter = instance + StageInstanceBefore = oldInstance, StageInstanceAfter = instance }; await this._stageInstanceUpdated.InvokeAsync(this, eventArgs); @@ -2469,14 +2546,11 @@ internal async Task OnStageInstanceDeleteAsync(DiscordStageInstance instance) { instance.Discord = this; - DiscordGuild guild = this.InternalGetCachedGuild(instance.GuildId); + DiscordGuild guild = await this.Cache.TryGetGuildAsync(instance.GuildId); guild._stageInstances.TryRemove(instance.Id, out _); - StageInstanceDeleteEventArgs eventArgs = new StageInstanceDeleteEventArgs - { - StageInstance = instance - }; + StageInstanceDeleteEventArgs eventArgs = new() {StageInstance = instance}; await this._stageInstanceDeleted.InvokeAsync(this, eventArgs); } @@ -2485,9 +2559,10 @@ internal async Task OnStageInstanceDeleteAsync(DiscordStageInstance instance) #region Misc - internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction) + internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, + TransportMember member, DiscordInteraction interaction) { - DiscordUser usr = new DiscordUser(user) { Discord = this }; + DiscordUser usr = new(user) {Discord = this}; interaction.ChannelId = channelId; interaction.GuildId = guildId; @@ -2496,12 +2571,12 @@ internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, Tr if (member != null) { - usr = new DiscordMember(member) { _guild_id = guildId.Value, Discord = this }; - this.UpdateUser(usr, guildId, interaction.Guild, member); + usr = new DiscordMember(member) {_guild_id = guildId.Value, Discord = this}; + await this.UpdateUserAsync(usr, guildId, interaction.Guild, member); } else { - this.UpdateUserCache(usr); + await this.Cache.AddUserAsync(usr); } interaction.User = usr; @@ -2514,7 +2589,7 @@ internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, Tr foreach (KeyValuePair c in resolved.Users) { c.Value.Discord = this; - this.UpdateUserCache(c.Value); + await this.Cache.AddUserAsync(c.Value); } } @@ -2527,7 +2602,7 @@ internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, Tr c.Value._guild_id = guildId.Value; c.Value.User.Discord = this; - this.UpdateUserCache(c.Value.User); + await this.Cache.AddUserAsync(c.Value.User); } } @@ -2573,20 +2648,15 @@ internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, Tr if (interaction.Type is InteractionType.Component) { - interaction.Message.Discord = this; interaction.Message.ChannelId = interaction.ChannelId; - ComponentInteractionCreateEventArgs cea = new ComponentInteractionCreateEventArgs - { - Message = interaction.Message, - Interaction = interaction - }; + ComponentInteractionCreateEventArgs cea = new() {Message = interaction.Message, Interaction = interaction}; await this._componentInteractionCreated.InvokeAsync(this, cea); } else if (interaction.Type is InteractionType.ModalSubmit) { - ModalSubmitEventArgs mea = new ModalSubmitEventArgs(interaction); + ModalSubmitEventArgs mea = new(interaction); await this._modalSubmitted.InvokeAsync(this, mea); } @@ -2601,63 +2671,46 @@ internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, Tr interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); - ContextMenuInteractionCreateEventArgs ctea = new ContextMenuInteractionCreateEventArgs + ContextMenuInteractionCreateEventArgs ctea = new() { Interaction = interaction, TargetUser = targetMember ?? targetUser, TargetMessage = targetMessage, - Type = interaction.Data.Type, + Type = interaction.Data.Type }; await this._contextMenuInteractionCreated.InvokeAsync(this, ctea); } - InteractionCreateEventArgs ea = new InteractionCreateEventArgs - { - Interaction = interaction - }; + InteractionCreateEventArgs ea = new() {Interaction = interaction}; await this._interactionCreated.InvokeAsync(this, ea); } - internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) + internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, ulong? guildId, DateTimeOffset started, + TransportMember mbr) { if (channel == null) { - channel = new DiscordChannel - { - Discord = this, - Id = channelId, - GuildId = guildId ?? default, - }; + channel = new DiscordChannel {Discord = this, Id = channelId, GuildId = guildId ?? default}; } - DiscordGuild guild = this.InternalGetCachedGuild(guildId); - DiscordUser usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); + DiscordGuild guild = await this.Cache.TryGetGuildAsync(guildId); + DiscordUser usr = this.UpdateUserAsync(new DiscordUser {Id = userId, Discord = this}, guildId, guild, mbr); - TypingStartEventArgs ea = new TypingStartEventArgs - { - Channel = channel, - User = usr, - Guild = guild, - StartedAt = started - }; + TypingStartEventArgs ea = new() {Channel = channel, User = usr, Guild = guild, StartedAt = started}; await this._typingStarted.InvokeAsync(this, ea); } - internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) + internal async Task OnWebhooksUpdateAsync(ulong channelId, ulong guildId) { - WebhooksUpdateEventArgs ea = new WebhooksUpdateEventArgs - { - Channel = channel, - Guild = guild - }; + WebhooksUpdateEventArgs ea = new() {Channel = channelId, Guild = guildId}; await this._webhooksUpdated.InvokeAsync(this, ea); } internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) { - DiscordGuild guild = this.InternalGetCachedGuild((ulong)raw["guild_id"]); - ConcurrentDictionary oldStickers = new ConcurrentDictionary(guild._stickers); + DiscordGuild guild = await this.Cache.TryGetGuildAsync((ulong)raw["guild_id"]); + ConcurrentDictionary oldStickers = new(guild._stickers); guild._stickers.Clear(); @@ -2673,11 +2726,9 @@ internal async Task OnStickersUpdatedAsync(IEnumerable ne guild._stickers[nst.Id] = nst; } - GuildStickersUpdateEventArgs sea = new GuildStickersUpdateEventArgs + GuildStickersUpdateEventArgs sea = new() { - Guild = guild, - StickersBefore = oldStickers, - StickersAfter = guild.Stickers + Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea); @@ -2685,32 +2736,39 @@ internal async Task OnStickersUpdatedAsync(IEnumerable ne internal async Task OnUnknownEventAsync(GatewayPayload payload) { - UnknownEventArgs ea = new UnknownEventArgs { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; + UnknownEventArgs ea = new() {EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString()}; await this._unknownEvent.InvokeAsync(this, ea); } #endregion #region AutoModeration + internal async Task OnAutoModerationRuleCreateAsync(DiscordAutoModerationRule ruleCreated) { ruleCreated.Discord = this; - await this._autoModerationRuleCreated.InvokeAsync(this, new AutoModerationRuleCreateEventArgs { Rule = ruleCreated }); + await this._autoModerationRuleCreated.InvokeAsync(this, + new AutoModerationRuleCreateEventArgs {Rule = ruleCreated}); } internal async Task OnAutoModerationRuleUpdatedAsync(DiscordAutoModerationRule ruleUpdated) { ruleUpdated.Discord = this; - await this._autoModerationRuleUpdated.InvokeAsync(this, new AutoModerationRuleUpdateEventArgs { Rule = ruleUpdated }); + await this._autoModerationRuleUpdated.InvokeAsync(this, + new AutoModerationRuleUpdateEventArgs {Rule = ruleUpdated}); } internal async Task OnAutoModerationRuleDeletedAsync(DiscordAutoModerationRule ruleDeleted) { ruleDeleted.Discord = this; - await this._autoModerationRuleDeleted.InvokeAsync(this, new AutoModerationRuleDeleteEventArgs { Rule = ruleDeleted }); + await this._autoModerationRuleDeleted.InvokeAsync(this, + new AutoModerationRuleDeleteEventArgs {Rule = ruleDeleted}); } - internal async Task OnAutoModerationRuleExecutedAsync(DiscordAutoModerationActionExecution ruleExecuted) => await this._autoModerationRuleExecuted.InvokeAsync(this, new AutoModerationRuleExecuteEventArgs { Rule = ruleExecuted }); + internal async Task OnAutoModerationRuleExecutedAsync(DiscordAutoModerationActionExecution ruleExecuted) => + await this._autoModerationRuleExecuted.InvokeAsync(this, + new AutoModerationRuleExecuteEventArgs {Rule = ruleExecuted}); + #endregion #endregion diff --git a/DSharpPlus/Clients/DiscordClient.Events.cs b/DSharpPlus/Clients/DiscordClient.Events.cs index 573034b215..c624b14fc3 100644 --- a/DSharpPlus/Clients/DiscordClient.Events.cs +++ b/DSharpPlus/Clients/DiscordClient.Events.cs @@ -49,7 +49,7 @@ public event AsyncEventHandler SocketClosed /// Fired when this client has successfully completed its handshake with the websocket gateway. /// /// - /// will not be populated when this event is fired.
+ /// will not be populated when this event is fired.
/// See also: , ///
public event AsyncEventHandler SessionCreated diff --git a/DSharpPlus/Clients/DiscordClient.cs b/DSharpPlus/Clients/DiscordClient.cs index 3c10f7b0b9..d2168f0a2b 100644 --- a/DSharpPlus/Clients/DiscordClient.cs +++ b/DSharpPlus/Clients/DiscordClient.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using DSharpPlus.AsyncEvents; +using DSharpPlus.Caching; using DSharpPlus.Entities; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; @@ -27,7 +28,6 @@ public sealed partial class DiscordClient : BaseDiscordClient #region Internal Fields/Properties internal bool _isShard = false; - internal IMessageCacheProvider? MessageCache { get; } private List _extensions = new(); private StatusUpdate _status = null; @@ -37,6 +37,7 @@ public sealed partial class DiscordClient : BaseDiscordClient #endregion #region Public Fields/Properties + /// /// Gets the gateway protocol version. /// @@ -77,20 +78,8 @@ public DiscordIntents Intents public bool IsConnected => this._webSocketClient is not null && this._webSocketClient.IsConnected; - /// - /// Gets a dictionary of DM channels that have been cached by this client. The dictionary's key is the channel - /// ID. - /// - public IReadOnlyDictionary PrivateChannels { get; } - internal ConcurrentDictionary _privateChannels = new(); + public List DmChannels { get; } - /// - /// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the - /// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the - /// or events haven't been fired yet) - /// - public override IReadOnlyDictionary Guilds { get; } - internal ConcurrentDictionary _guilds = new(); /// /// Gets the WS latency for this client. @@ -100,14 +89,6 @@ public int Ping private int _ping; - /// - /// Gets the collection of presences held by this client. - /// - public IReadOnlyDictionary Presences - => this._presencesLazy.Value; - - internal Dictionary _presences = new(); - private Lazy> _presencesLazy; #endregion #region Constructor/Internal Setup @@ -119,17 +100,11 @@ public IReadOnlyDictionary Presences public DiscordClient(DiscordConfiguration config) : base(config) { - DiscordIntents intents = this.Configuration.Intents; - if (intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages)) - { - this.MessageCache = this.Configuration.MessageCacheProvider - ?? (this.Configuration.MessageCacheSize > 0 ? new MessageCache(this.Configuration.MessageCacheSize) : null); - } - + this.Cache = this.Configuration.CacheProvider ?? new DiscordMemoryCache(this.Configuration.CacheConfiguration); this.InternalSetup(); - this.Guilds = new ReadOnlyConcurrentDictionary(this._guilds); - this.PrivateChannels = new ReadOnlyConcurrentDictionary(this._privateChannels); + this._guildIds = new List(); + this.DmChannels = new List(); } /// @@ -141,16 +116,10 @@ internal DiscordClient(DiscordConfiguration config, RestClient restClient) : base(config, restClient) { DiscordIntents intents = this.Configuration.Intents; - if (intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages)) - { - this.MessageCache = this.Configuration.MessageCacheProvider - ?? (this.Configuration.MessageCacheSize > 0 ? new MessageCache(this.Configuration.MessageCacheSize) : null); - } - this.InternalSetup(); - this.Guilds = new ReadOnlyConcurrentDictionary(this._guilds); - this.PrivateChannels = new ReadOnlyConcurrentDictionary(this._privateChannels); + this._guildIds = new List(); + this.DmChannels = new List(); } internal void InternalSetup() @@ -235,9 +204,7 @@ internal void InternalSetup() this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", this.EventErrorHandler); #endregion - this._guilds.Clear(); - - this._presencesLazy = new Lazy>(() => new ReadOnlyDictionary(this._presences)); + this._guildIds.Clear(); } #endregion @@ -295,12 +262,12 @@ public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? stat } else { - long? since_unix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null; + long? sinceUnix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null; this._status = new StatusUpdate() { Activity = new TransportActivity(activity), Status = status ?? UserStatus.Online, - IdleSince = since_unix, + IdleSince = sinceUnix, IsAFK = idlesince != null, _activity = activity }; @@ -310,7 +277,8 @@ public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? stat { if (this.Configuration.TokenType != TokenType.Bot) { - this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); + this.Logger.LogWarning(LoggerEvents.Misc, + "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); } this.Logger.LogInformation(LoggerEvents.Startup, "DSharpPlus, version {Version}", this.VersionString); @@ -349,7 +317,8 @@ public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? stat break; } - this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, "Connection attempt failed, retrying in {Seconds}s", w / 1000); + this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, + "Connection attempt failed, retrying in {Seconds}s", w / 1000); await Task.Delay(w); if (i > 0) @@ -407,40 +376,7 @@ public async Task GetStickerAsync(ulong stickerId) /// public async Task> GetStickerPacksAsync() => await this.ApiClient.GetStickerPacksAsync(); - - /// - /// Gets a user - /// - /// ID of the user - /// Whether to always make a REST request and update cache. Passing true will update the user, updating stale properties such as . - /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetUserAsync(ulong userId, bool updateCache = false) - { - if (!updateCache && this.TryGetCachedUserInternal(userId, out DiscordUser? usr)) - { - return usr; - } - - usr = await this.ApiClient.GetUserAsync(userId); - - // See BaseDiscordClient.UpdateUser for why this is done like this. - this.UserCache.AddOrUpdate(userId, usr, (_, _) => usr); - - return usr; - } - - /// - /// Gets a channel - /// - /// The ID of the channel to get. - /// - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetChannelAsync(ulong id) - => this.InternalGetCachedThread(id) ?? this.InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id); + /// /// Sends a message @@ -453,7 +389,8 @@ public async Task GetChannelAsync(ulong id) /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordChannel channel, string content) - => await this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + => await this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, replyMessageId: null, + mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Sends a message @@ -466,7 +403,8 @@ public async Task SendMessageAsync(DiscordChannel channel, strin /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) - => await this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + => await this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] {embed} : null, + replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Sends a message @@ -480,7 +418,8 @@ public async Task SendMessageAsync(DiscordChannel channel, Disco /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) - => await this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + => await this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] {embed} : null, + replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Sends a message @@ -526,9 +465,10 @@ public async Task SendMessageAsync(DiscordChannel channel, Actio /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, - DefaultMessageNotifications? defaultMessageNotifications = null, - SystemChannelFlags? systemChannelFlags = null) + public async Task CreateGuildAsync(string name, string region = null, Optional icon = default, + VerificationLevel? verificationLevel = null, + DefaultMessageNotifications? defaultMessageNotifications = null, + SystemChannelFlags? systemChannelFlags = null) { Optional iconb64 = Optional.FromNoValue(); if (icon.HasValue && icon.Value != null) @@ -543,7 +483,8 @@ public async Task CreateGuildAsync(string name, string region = nu iconb64 = null; } - return await this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); + return await this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, + defaultMessageNotifications, systemChannelFlags); } /// @@ -555,15 +496,14 @@ public async Task CreateGuildAsync(string name, string region = nu /// The created guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) + public async Task CreateGuildFromTemplateAsync(string code, string name, + Optional icon = default) { Optional iconb64 = Optional.FromNoValue(); - if (icon.HasValue && icon.Value != null) + if (icon is {HasValue: true, Value: not null}) { - using (ImageTool imgtool = new ImageTool(icon.Value)) - { - iconb64 = imgtool.GetBase64(); - } + using ImageTool imgtool = new ImageTool(icon.Value); + iconb64 = imgtool.GetBase64(); } else if (icon.HasValue) { @@ -572,33 +512,7 @@ public async Task CreateGuildFromTemplateAsync(string code, string return await this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); } - - /// - /// Gets a guild. - /// Setting to true will make a REST request. - /// - /// The guild ID to search for. - /// Whether to include approximate presence and member counts in the returned guild. - /// The requested Guild. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetGuildAsync(ulong id, bool? withCounts = null) - { - if (this._guilds.TryGetValue(id, out DiscordGuild? guild) && (!withCounts.HasValue || !withCounts.Value)) - { - return guild; - } - - guild = await this.ApiClient.GetGuildAsync(id, withCounts); - IReadOnlyList channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id); - foreach (DiscordChannel channel in channels) - { - guild._channels[channel.Id] = channel; - } - - return guild; - } + /// /// Gets a guild preview @@ -621,7 +535,8 @@ public async Task GetGuildPreviewAsync(ulong id) /// Thrown when the invite does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null) + public async Task GetInviteByCodeAsync(string code, bool? withCounts = null, + bool? withExpiration = null) => await this.ApiClient.GetInviteAsync(code, withCounts, withExpiration); /// @@ -663,7 +578,12 @@ public async Task GetWebhookWithTokenAsync(ulong id, string toke /// Status of the user. /// Since when is the client performing the specified activity. /// - public Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) + public Task UpdateStatusAsync + ( + DiscordActivity activity = null, + UserStatus? userStatus = null, + DateTimeOffset? idleSince = null + ) => this.InternalUpdateStatusAsync(activity, userStatus, idleSince); /// @@ -720,7 +640,8 @@ public async Task> GetGlobalApplication /// /// The list of commands to overwrite with. /// The list of global commands. - public async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => + public async Task> BulkOverwriteGlobalApplicationCommandsAsync( + IEnumerable commands) => await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); /// @@ -728,7 +649,8 @@ public async Task> BulkOverwriteGlobalA /// /// The command to create. /// The created command. - public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => + public async Task + CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => await this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); /// @@ -746,7 +668,8 @@ public async Task GetGlobalApplicationCommandAsync(ul /// The command with the name. public async Task GetGlobalApplicationCommandAsync(string commandName) { - foreach (DiscordApplicationCommand command in await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id)) + foreach (DiscordApplicationCommand command in await this.ApiClient.GetGlobalApplicationCommandsAsync( + this.CurrentApplication.Id)) { if (command.Name == commandName) { @@ -763,12 +686,15 @@ public async Task GetGlobalApplicationCommandAsync(st /// The ID of the command to edit. /// Action to perform. /// The edited command. - public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) + public async Task EditGlobalApplicationCommandAsync(ulong commandId, + Action action) { ApplicationCommandEditModel mdl = new ApplicationCommandEditModel(); action(mdl); ulong applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync()).Id; - return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); + return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, + mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, + mdl.DefaultMemberPermissions); } /// @@ -792,7 +718,8 @@ public async Task> GetGuildApplicationC /// The ID of the guild. /// The list of commands to overwrite with. /// The list of guild commands. - public async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => + public async Task> BulkOverwriteGuildApplicationCommandsAsync( + ulong guildId, IEnumerable commands) => await this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); /// @@ -801,7 +728,8 @@ public async Task> BulkOverwriteGuildAp /// The ID of the guild to create the application command in. /// The command to create. /// The created command. - public async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => + public async Task CreateGuildApplicationCommandAsync(ulong guildId, + DiscordApplicationCommand command) => await this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); /// @@ -811,7 +739,7 @@ public async Task CreateGuildApplicationCommandAsync( /// The ID of the command to get. /// The command with the ID. public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - await this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + await this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Edits a application command in a guild. @@ -820,12 +748,15 @@ public async Task GetGuildApplicationCommandAsync(ulo /// The ID of the command to edit. /// Action to perform. /// The edited command. - public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) + public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, + Action action) { ApplicationCommandEditModel mdl = new ApplicationCommandEditModel(); action(mdl); ulong applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync()).Id; - return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); + return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, + mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, + mdl.DefaultMemberPermissions); } /// @@ -924,151 +855,73 @@ private async IAsyncEnumerable GetGuildsInternalAsync(int limit = #region Internal Caching Methods - internal DiscordThreadChannel InternalGetCachedThread(ulong threadId) - { - foreach (DiscordGuild guild in this.Guilds.Values) - { - if (guild.Threads.TryGetValue(threadId, out DiscordThreadChannel? foundThread)) - { - return foundThread; - } - } - - return null; - } - - internal DiscordChannel InternalGetCachedChannel(ulong channelId) - { - if (this._privateChannels?.TryGetValue(channelId, out DiscordDmChannel? foundDmChannel) == true) - { - return foundDmChannel; - } - - foreach (DiscordGuild guild in this.Guilds.Values) - { - if (guild.Channels.TryGetValue(channelId, out DiscordChannel? foundChannel)) - { - return foundChannel; - } - } - - return null; - } - - internal DiscordGuild InternalGetCachedGuild(ulong? guildId) - { - if (this._guilds != null && guildId.HasValue) - { - if (this._guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) - { - return guild; - } - } - - return null; - } - - private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) - { - if (author != null) - { - DiscordUser usr = new DiscordUser(author) { Discord = this }; - - if (member != null) - { - member.User = author; - } - - message.Author = this.UpdateUser(usr, guild?.Id, guild, member); - } - - DiscordChannel? channel = this.InternalGetCachedChannel(message.ChannelId) ?? this.InternalGetCachedThread(message.ChannelId); - - if (channel != null) - { - return; - } - - channel = !message._guildId.HasValue - ? new DiscordDmChannel - { - Id = message.ChannelId, - Discord = this, - Type = ChannelType.Private, - Recipients = new DiscordUser[] { message.Author } - } - : new DiscordChannel - { - Id = message.ChannelId, - GuildId = guild.Id, - Discord = this - }; - - message.Channel = channel; - } - - private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild guild, TransportMember mbr) + private async ValueTask UpdateUserAsync + ( + DiscordUser usr, + ulong? guildId, + TransportMember? mbr + ) { - if (mbr != null) + if (mbr is not null) { - if (mbr.User != null) + if (mbr.User is not null) { - usr = new DiscordUser(mbr.User) { Discord = this }; - - this.UpdateUserCache(usr); - - usr = new DiscordMember(mbr) { Discord = this, _guild_id = guildId.Value }; + usr = new DiscordUser(mbr.User) {Discord = this}; + + usr = new DiscordMember(mbr) {Discord = this, _guild_id = guildId.Value}; } DiscordIntents intents = this.Configuration.Intents; - DiscordMember member = default; + DiscordMember? member = new(mbr); - if (!intents.HasAllPrivilegedIntents() || guild.IsLarge) // we have the necessary privileged intents, no need to worry about caching here unless guild is large. + DiscordMember? cachedMember = await this.Cache.TryGetMemberAsync(usr.Id, guildId.Value); + if (cachedMember is null) { - if (guild?._members.TryGetValue(usr.Id, out member) == false) + if (intents.HasIntent(DiscordIntents.GuildMembers) || + this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it { - if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it - { - guild._members.TryAdd(usr.Id, (DiscordMember)usr); - } + await this.Cache.AddMemberAsync((DiscordMember)usr); } - else if (intents.HasIntent(DiscordIntents.GuildPresences) || this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache. + } + else if (intents.HasIntent(DiscordIntents.GuildPresences) || + this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache. + { + if (!intents.HasIntent(DiscordIntents + .GuildMembers)) // no need to update if we already have the member events { - if (!intents.HasIntent(DiscordIntents.GuildMembers)) // no need to update if we already have the member events - { - _ = guild._members.TryUpdate(usr.Id, (DiscordMember)usr, member); - } + await this.Cache.AddMemberAsync(member); } } - } - else if (usr.Username != null) // check if not a skeleton user - { - this.UpdateUserCache(usr); + else if (usr.Username is not null) // check if not a skeleton user + { + await this.Cache.AddUserAsync(usr); + } } return usr; } - private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) + private async void UpdateCachedGuildAsync(DiscordGuild newGuild, JArray rawMembers) { if (this._disposed) { return; } - if (!this._guilds.ContainsKey(newGuild.Id)) + DiscordGuild? cachedGuild = await this.Cache.TryGetGuildAsync(newGuild.Id); + if (cachedGuild is null) { - this._guilds[newGuild.Id] = newGuild; + cachedGuild = newGuild; + await this.Cache.AddGuildAsync(cachedGuild); } - DiscordGuild guild = this._guilds[newGuild.Id]; - - if (newGuild._channels != null && newGuild._channels.Count > 0) + if (newGuild.Channels is {Count: > 0}) { - foreach (DiscordChannel channel in newGuild._channels.Values) + foreach (DiscordChannel channel in newGuild.Channels.Values) { - if (guild._channels.TryGetValue(channel.Id, out _)) + DiscordChannel? cachedChannel = await this.Cache.TryGetChannelAsync(channel.Id); + if (cachedChannel is not null) { continue; } @@ -1079,98 +932,106 @@ private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) overwrite._channel_id = channel.Id; } - guild._channels[channel.Id] = channel; + await this.Cache.AddChannelAsync(channel); } } - if (newGuild._threads != null && newGuild._threads.Count > 0) + + if (newGuild.Threads is {Count: > 0}) { - foreach (DiscordThreadChannel thread in newGuild._threads.Values) + foreach (DiscordThreadChannel thread in newGuild.Threads.Values) { - if (guild._threads.TryGetValue(thread.Id, out _)) + DiscordChannel? cachedChannel = await this.Cache.TryGetChannelAsync(thread.Id); + if (cachedChannel is not null) { continue; } - guild._threads[thread.Id] = thread; + foreach (DiscordOverwrite overwrite in thread._permissionOverwrites) + { + overwrite.Discord = this; + overwrite._channel_id = thread.Id; + } + + await this.Cache.AddChannelAsync(thread); } } - foreach (DiscordEmoji newEmoji in newGuild._emojis.Values) + foreach (DiscordEmoji newEmoji in newGuild.Emojis.Values) { - _ = guild._emojis.GetOrAdd(newEmoji.Id, _ => newEmoji); + _ = cachedGuild._emojis.GetOrAdd(newEmoji.Id, _ => newEmoji); } - foreach (DiscordMessageSticker newSticker in newGuild._stickers.Values) + foreach (DiscordMessageSticker newSticker in newGuild.Stickers.Values) { - _ = guild._stickers.GetOrAdd(newSticker.Id, _ => newSticker); + _ = cachedGuild._stickers.GetOrAdd(newSticker.Id, _ => newSticker); } - if (rawMembers != null) + if (rawMembers is not null) { - guild._members.Clear(); + cachedGuild._members.Clear(); - foreach (JToken xj in rawMembers) + foreach (JToken rawMember in rawMembers) { - TransportMember xtm = xj.ToDiscordObject(); - - DiscordUser xu = new DiscordUser(xtm.User) { Discord = this }; - this.UpdateUserCache(xu); + TransportMember transportMember = rawMember.ToDiscordObject(); - guild._members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, _guild_id = guild.Id }; + DiscordUser newUser = new(transportMember.User) {Discord = this}; + await this.Cache.AddUserAsync(newUser); + await this.Cache.AddMemberAsync( + new DiscordMember(transportMember) {Discord = this, _guild_id = newGuild.Id}); } } - foreach (DiscordRole role in newGuild._roles.Values) + foreach (DiscordRole role in newGuild.Roles.Values) { - if (guild._roles.TryGetValue(role.Id, out _)) + if (cachedGuild._roles.TryGetValue(role.Id, out _)) { continue; } - role._guild_id = guild.Id; - guild._roles[role.Id] = role; + role._guild_id = cachedGuild.Id; + cachedGuild._roles[role.Id] = role; } - if (newGuild._stageInstances != null) + if (newGuild.StageInstances is not null) { - foreach (DiscordStageInstance newStageInstance in newGuild._stageInstances.Values) + foreach (DiscordStageInstance newStageInstance in newGuild.StageInstances.Values) { - _ = guild._stageInstances.GetOrAdd(newStageInstance.Id, _ => newStageInstance); + _ = cachedGuild._stageInstances.GetOrAdd(newStageInstance.Id, _ => newStageInstance); } } - guild.Name = newGuild.Name; - guild._afkChannelId = newGuild._afkChannelId; - guild.AfkTimeout = newGuild.AfkTimeout; - guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; - guild.Features = newGuild.Features; - guild.IconHash = newGuild.IconHash; - guild.MfaLevel = newGuild.MfaLevel; - guild.OwnerId = newGuild.OwnerId; - guild._voiceRegionId = newGuild._voiceRegionId; - guild.SplashHash = newGuild.SplashHash; - guild.VerificationLevel = newGuild.VerificationLevel; - guild.WidgetEnabled = newGuild.WidgetEnabled; - guild._widgetChannelId = newGuild._widgetChannelId; - guild.ExplicitContentFilter = newGuild.ExplicitContentFilter; - guild.PremiumTier = newGuild.PremiumTier; - guild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount; - guild.Banner = newGuild.Banner; - guild.Description = newGuild.Description; - guild.VanityUrlCode = newGuild.VanityUrlCode; - guild.Banner = newGuild.Banner; - guild._systemChannelId = newGuild._systemChannelId; - guild.SystemChannelFlags = newGuild.SystemChannelFlags; - guild.DiscoverySplashHash = newGuild.DiscoverySplashHash; - guild.MaxMembers = newGuild.MaxMembers; - guild.MaxPresences = newGuild.MaxPresences; - guild.ApproximateMemberCount = newGuild.ApproximateMemberCount; - guild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount; - guild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers; - guild.PreferredLocale = newGuild.PreferredLocale; - guild._rulesChannelId = newGuild._rulesChannelId; - guild._publicUpdatesChannelId = newGuild._publicUpdatesChannelId; - guild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; + cachedGuild.Name = newGuild.Name; + cachedGuild._afkChannelId = newGuild._afkChannelId; + cachedGuild.AfkTimeout = newGuild.AfkTimeout; + cachedGuild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; + cachedGuild.Features = newGuild.Features; + cachedGuild.IconHash = newGuild.IconHash; + cachedGuild.MfaLevel = newGuild.MfaLevel; + cachedGuild.OwnerId = newGuild.OwnerId; + cachedGuild._voiceRegionId = newGuild._voiceRegionId; + cachedGuild.SplashHash = newGuild.SplashHash; + cachedGuild.VerificationLevel = newGuild.VerificationLevel; + cachedGuild.WidgetEnabled = newGuild.WidgetEnabled; + cachedGuild._widgetChannelId = newGuild._widgetChannelId; + cachedGuild.ExplicitContentFilter = newGuild.ExplicitContentFilter; + cachedGuild.PremiumTier = newGuild.PremiumTier; + cachedGuild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount; + cachedGuild.Banner = newGuild.Banner; + cachedGuild.Description = newGuild.Description; + cachedGuild.VanityUrlCode = newGuild.VanityUrlCode; + cachedGuild.Banner = newGuild.Banner; + cachedGuild._systemChannelId = newGuild._systemChannelId; + cachedGuild.SystemChannelFlags = newGuild.SystemChannelFlags; + cachedGuild.DiscoverySplashHash = newGuild.DiscoverySplashHash; + cachedGuild.MaxMembers = newGuild.MaxMembers; + cachedGuild.MaxPresences = newGuild.MaxPresences; + cachedGuild.ApproximateMemberCount = newGuild.ApproximateMemberCount; + cachedGuild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount; + cachedGuild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers; + cachedGuild.PreferredLocale = newGuild.PreferredLocale; + cachedGuild._rulesChannelId = newGuild._rulesChannelId; + cachedGuild._publicUpdatesChannelId = newGuild._publicUpdatesChannelId; + cachedGuild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; // fields not sent for update: // - guild.Channels @@ -1181,28 +1042,52 @@ private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) // - guild.Unavailable = new_guild.Unavailable; } - private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) + private async void PopulateMessageReactionsAndCacheAsync + ( + DiscordMessage message, + TransportUser author, + TransportMember member + ) { - DiscordGuild guild = message.Channel?.Guild ?? this.InternalGetCachedGuild(message._guildId); + if (author is not null) + { + DiscordUser usr = new DiscordUser(author) {Discord = this}; - this.UpdateMessage(message, author, guild, member); + if (member is not null) + { + member.User = author; + } - if (message._reactions == null) - { - message._reactions = new List(); + message.Author = await this.UpdateUserAsync(usr, message._guildId, member); } - foreach (DiscordReaction xr in message._reactions) + DiscordChannel? channel = await this.Cache.TryGetChannelAsync(message.ChannelId); + + if (channel is null) { - xr.Emoji.Discord = this; + channel = !message._guildId.HasValue + ? new DiscordDmChannel + { + Id = message.ChannelId, + Discord = this, + Type = ChannelType.Private, + Recipients = new DiscordUser[] {message.Author} + } + : new DiscordChannel {Id = message.ChannelId, GuildId = message._guildId, Discord = this}; + + message.Channel = channel; + await this.Cache.AddChannelAsync(channel); } - if (this.Configuration.MessageCacheSize > 0 && message.Channel != null) + message._reactions ??= new List(); + + foreach (DiscordReaction reaction in message._reactions) { - this.MessageCache?.Add(message); + reaction.Emoji.Discord = this; } - } + await this.Cache.AddMessageAsync(message); + } #endregion @@ -1244,9 +1129,7 @@ public override void Dispose() } catch { } - this._guilds = null!; this._heartbeatTask = null!; - this._privateChannels = null!; } #endregion diff --git a/DSharpPlus/Clients/DiscordShardedClient.cs b/DSharpPlus/Clients/DiscordShardedClient.cs index 8303129b30..a74c290c0d 100644 --- a/DSharpPlus/Clients/DiscordShardedClient.cs +++ b/DSharpPlus/Clients/DiscordShardedClient.cs @@ -400,7 +400,7 @@ private int GetShardIdFromGuilds(ulong id) { foreach (DiscordClient s in this._shards.Values) { - if (s._guilds.TryGetValue(id, out _)) + if (s._guildIds.Contains(id)) { return s.ShardId; } diff --git a/DSharpPlus/DiscordConfiguration.cs b/DSharpPlus/DiscordConfiguration.cs index 4319d218cf..ed53dbc919 100644 --- a/DSharpPlus/DiscordConfiguration.cs +++ b/DSharpPlus/DiscordConfiguration.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Net; +using DSharpPlus.Caching; using DSharpPlus.Net.Udp; using DSharpPlus.Net.WebSocket; using Microsoft.Extensions.Logging; @@ -87,13 +88,6 @@ public string Token /// public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream; - /// - /// Sets the size of the global message cache. - /// Setting this to 0 will disable message caching entirely. Defaults to 1024. - /// This is only applied if the default message cache implementation is used. - /// - public int MessageCacheSize { internal get; set; } = 1024; - /// /// Sets the proxy to use for HTTP and WebSocket connections to Discord. /// Defaults to null. @@ -173,11 +167,16 @@ public UdpClientFactoryDelegate UdpClientFactory public bool LogUnknownAuditlogs { internal get; set; } = true; /// - /// Sets the message cache implementation to use. - /// To create your own implementation, implement the instance. + /// Sets the cache implementation to use. + /// To create your own implementation, implement the instance. /// Defaults to built-in implementation. /// - public IMessageCacheProvider? MessageCacheProvider { internal get; set; } = null; + public IDiscordCache? CacheProvider { internal get; set; } = null; + + /// + /// + /// + public CacheConfiguration? CacheConfiguration { internal get; set; } = new(); /// /// Creates a new configuration with default values. @@ -201,7 +200,6 @@ public DiscordConfiguration(DiscordConfiguration other) this.ShardId = other.ShardId; this.ShardCount = other.ShardCount; this.GatewayCompressionLevel = other.GatewayCompressionLevel; - this.MessageCacheSize = other.MessageCacheSize; this.WebSocketClientFactory = other.WebSocketClientFactory; this.UdpClientFactory = other.UdpClientFactory; this.Proxy = other.Proxy; @@ -211,6 +209,7 @@ public DiscordConfiguration(DiscordConfiguration other) this.LoggerFactory = other.LoggerFactory; this.LogUnknownEvents = other.LogUnknownEvents; this.LogUnknownAuditlogs = other.LogUnknownAuditlogs; - this.MessageCacheProvider = other.MessageCacheProvider; + this.CacheConfiguration = other.CacheConfiguration; + this.CacheProvider = other.CacheProvider; } } diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs b/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs index 32b02a12a6..40e0c8a2a5 100644 --- a/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs +++ b/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - namespace DSharpPlus.Entities.AuditLogs; /// diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs b/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs index 76d7c73473..a46ca9a156 100644 --- a/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs +++ b/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - namespace DSharpPlus.Entities.AuditLogs; // below is taken from diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs b/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs index c50d2e3b93..28e8c7cb44 100644 --- a/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs +++ b/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs @@ -4,100 +4,74 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; - +using DSharpPlus.Caching; using DSharpPlus.Net.Abstractions; using DSharpPlus.Net.Serialization; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json.Linq; namespace DSharpPlus.Entities.AuditLogs; -internal static class AuditLogParser +internal class AuditLogParser { + private BaseDiscordClient _client; + private DiscordGuild? _guild; + private ulong _guildId; + + internal AuditLogParser(BaseDiscordClient client, ulong guildId, DiscordGuild? guild = null) + { + _client = client; + _guildId = guildId; + _guild = guild; + } + /// /// Parses a AuditLog to a list of AuditLogEntries /// - /// which is the parent of the AuditLog /// whose entries should be parsed /// A list of . All entries which cant be parsed are dropped - internal static async IAsyncEnumerable ParseAuditLogToEntriesAsync + internal async IAsyncEnumerable ParseAuditLogToEntriesAsync ( - DiscordGuild guild, AuditLog auditLog ) { - BaseDiscordClient client = guild.Discord; - - //Get all User - IEnumerable users = auditLog.Users; - - //Update cache if user is not known - foreach (DiscordUser discordUser in users) - { - discordUser.Discord = client; - if (client.UserCache.ContainsKey(discordUser.Id)) - { - continue; - } - - client.UpdateUserCache(discordUser); - } - - //get unique webhooks, scheduledEvents, threads + _guild ??= await _client.Cache.TryGetGuildAsync(this._guildId); + + IEnumerable users = auditLog.Users.ToList(); IEnumerable uniqueWebhooks = auditLog.Webhooks; IEnumerable uniqueScheduledEvents = auditLog.Events; - IEnumerable uniqueThreads = auditLog.Threads; - IDictionary webhooks = uniqueWebhooks.ToDictionary(x => x.Id); - - //update event cache and create a dictionary for it - foreach (DiscordScheduledGuildEvent discordEvent in uniqueScheduledEvents) + IEnumerable uniqueThreads = auditLog.Threads; + + List? discordMembers = new List(); + foreach (DiscordUser user in users) { - if (guild._scheduledEvents.ContainsKey(discordEvent.Id)) - { - continue; - } - - guild._scheduledEvents[discordEvent.Id] = discordEvent; - } + DiscordMember? cachedMember = await this._client.Cache.TryGetMemberAsync(user.Id, _guildId); - IDictionary events = guild._scheduledEvents; - - foreach (DiscordThreadChannel thread in uniqueThreads) - { - if (guild._threads.ContainsKey(thread.Id)) + if (cachedMember is null && _guild is not null) { - continue; + cachedMember = _guild._members.TryGetValue(user.Id, out cachedMember) ? cachedMember : null; } - - guild._threads[thread.Id] = thread; - } - - IDictionary threads = guild._threads; - - IEnumerable? discordMembers = users.Select - ( - user => + + cachedMember ??= new DiscordMember { - return guild._members is not null && guild._members.TryGetValue(user.Id, out DiscordMember? member) - ? member - : new DiscordMember - { - Discord = guild.Discord, - Id = user.Id, - _guild_id = guild.Id - }; - } - ); - - Dictionary members = discordMembers.ToDictionary(xm => xm.Id, xm => xm); - + Id = user.Id, + Discord = this._client, + _guild_id = _guild.Id + }; + + discordMembers.Add(cachedMember); + } + + Dictionary? events = uniqueScheduledEvents.ToDictionary(x => x.Id); + Dictionary? threads = uniqueThreads.ToDictionary(x => x.Id); + Dictionary webhooks = uniqueWebhooks.ToDictionary(x => x.Id); + Dictionary? members = discordMembers.ToDictionary(x => x.Id); + IOrderedEnumerable? auditLogActions = auditLog.Entries.OrderByDescending(xa => xa.Id); foreach (AuditLogAction? auditLogAction in auditLogActions) { DiscordAuditLogEntry? entry = - await ParseAuditLogEntryAsync(guild, auditLogAction, members, threads, webhooks, events); + await ParseAuditLogEntryAsync(auditLogAction, members, threads, webhooks, events); if (entry is null) { @@ -111,7 +85,6 @@ AuditLog auditLog /// /// Tries to parse a AuditLogAction to a DiscordAuditLogEntry /// - /// which is the parent of the entry /// which should be parsed /// A dictionary of which is used to inject the entities instead of passing the id /// A dictionary of which is used to inject the entities instead of passing the id @@ -119,9 +92,8 @@ AuditLog auditLog /// A dictionary of which is used to inject the entities instead of passing the id /// Returns a . Is null if the entry can not be parsed /// Will use guild cache for optional parameters if those are not present if possible - internal static async Task ParseAuditLogEntryAsync + internal async Task ParseAuditLogEntryAsync ( - DiscordGuild guild, AuditLogAction auditLogAction, IDictionary? members = null, IDictionary? threads = null, @@ -130,46 +102,59 @@ internal static async Task ParseAuditLogEntryAsync ) { //initialize members if null - members ??= guild._members; + if (members is null && _guild is not null) + { + members = _guild._members; + } //initialize threads if null - threads ??= guild._threads; + if (threads is null && _guild is not null) + { + threads = _guild._threads; + } //initialize scheduled events if null - events ??= guild._scheduledEvents; + if (events is null && _guild is not null) + { + events = _guild._scheduledEvents; + } + members ??= new Dictionary(); + threads ??= new Dictionary(); + events ??= new Dictionary(); webhooks ??= new Dictionary(); + ulong channelId, targetId; + DiscordChannel? channel = null; + DiscordMember? member = null; + DiscordUser? user = null; + DiscordAuditLogEntry? entry = null; switch (auditLogAction.ActionType) { case DiscordAuditLogActionType.GuildUpdate: - entry = await ParseGuildUpdateAsync(guild, auditLogAction); + entry = await ParseGuildUpdateAsync(auditLogAction); break; case DiscordAuditLogActionType.ChannelCreate: case DiscordAuditLogActionType.ChannelDelete: case DiscordAuditLogActionType.ChannelUpdate: - entry = ParseChannelEntry(guild, auditLogAction); + entry = await ParseChannelEntryAsync(auditLogAction); break; case DiscordAuditLogActionType.OverwriteCreate: case DiscordAuditLogActionType.OverwriteDelete: case DiscordAuditLogActionType.OverwriteUpdate: - entry = ParseOverwriteEntry(guild, auditLogAction); + entry = await this.ParseOverwriteEntryAsync(auditLogAction); break; case DiscordAuditLogActionType.Kick: + ulong memberId = auditLogAction.TargetId.Value; + DiscordMember cachedMember = await this._client.Cache.TryGetMemberAsync(memberId, this._guildId); + entry = new DiscordAuditLogKickEntry { - Target = members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? kickMember) - ? kickMember - : new DiscordMember - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord, - _guild_id = guild.Id - } + Target = new CachedEntity(memberId, cachedMember) }; break; @@ -183,50 +168,49 @@ internal static async Task ParseAuditLogEntryAsync case DiscordAuditLogActionType.Ban: case DiscordAuditLogActionType.Unban: + memberId = auditLogAction.TargetId.Value; + cachedMember = await this._client.Cache.TryGetMemberAsync(memberId, this._guildId); + entry = new DiscordAuditLogBanEntry { - Target = members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? unbanMember) - ? unbanMember - : new DiscordMember - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord, - _guild_id = guild.Id - } + Target = new CachedEntity(memberId, cachedMember) }; break; case DiscordAuditLogActionType.MemberUpdate: case DiscordAuditLogActionType.MemberRoleUpdate: - entry = ParseMemberUpdateEntry(guild, auditLogAction); + entry = await this.ParseMemberUpdateEntryAsync(auditLogAction); break; case DiscordAuditLogActionType.RoleCreate: case DiscordAuditLogActionType.RoleDelete: case DiscordAuditLogActionType.RoleUpdate: - entry = ParseRoleUpdateEntry(guild, auditLogAction); + entry = ParseRoleUpdateEntry(auditLogAction); break; case DiscordAuditLogActionType.InviteCreate: case DiscordAuditLogActionType.InviteDelete: case DiscordAuditLogActionType.InviteUpdate: - entry = ParseInviteUpdateEntry(guild, auditLogAction); + entry = await this.ParseInviteUpdateEntryAsync(auditLogAction); break; case DiscordAuditLogActionType.WebhookCreate: case DiscordAuditLogActionType.WebhookDelete: case DiscordAuditLogActionType.WebhookUpdate: - entry = ParseWebhookUpdateEntry(guild, auditLogAction, webhooks); + entry = await this.ParseWebhookUpdateEntryAsync(auditLogAction, webhooks); break; case DiscordAuditLogActionType.EmojiCreate: case DiscordAuditLogActionType.EmojiDelete: case DiscordAuditLogActionType.EmojiUpdate: + ulong emojiId = auditLogAction.TargetId.Value; + DiscordEmoji? emoji = null; + this._guild?._emojis.TryGetValue(emojiId, out emoji); + + entry = new DiscordAuditLogEmojiEntry { - Target = guild._emojis.TryGetValue(auditLogAction.TargetId!.Value, out DiscordEmoji? target) - ? target - : new DiscordEmoji { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + Target = new CachedEntity(emojiId, emoji) }; DiscordAuditLogEmojiEntry emojiEntry = (DiscordAuditLogEmojiEntry)entry; @@ -239,13 +223,12 @@ internal static async Task ParseAuditLogEntryAsync break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in emote update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in emote update: {Key}", actionChange.Key); } - break; } } @@ -255,7 +238,7 @@ internal static async Task ParseAuditLogEntryAsync case DiscordAuditLogActionType.StickerCreate: case DiscordAuditLogActionType.StickerDelete: case DiscordAuditLogActionType.StickerUpdate: - entry = ParseStickerUpdateEntry(guild, auditLogAction); + entry = ParseStickerUpdateEntry(auditLogAction); break; case DiscordAuditLogActionType.MessageDelete: @@ -267,23 +250,29 @@ internal static async Task ParseAuditLogEntryAsync if (auditLogAction.Options is not null) { - messageEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? new DiscordChannel - { - Id = auditLogAction.Options.ChannelId, - Discord = guild.Discord, - GuildId = guild.Id - }; + channelId = auditLogAction.Options.ChannelId; + channel = await this._client.Cache.TryGetChannelAsync(channelId); + messageEntry.Channel = new CachedEntity(channelId, channel); + messageEntry.MessageCount = auditLogAction.Options.Count; } - if (messageEntry.Channel is not null) + if (messageEntry.Channel.HasCachedValue) { - guild.Discord.UserCache.TryGetValue(auditLogAction.UserId, out DiscordUser? user); - messageEntry.Target = user ?? new DiscordUser - { - Id = auditLogAction.UserId, - Discord = guild.Discord - }; + ulong userId = auditLogAction.TargetId.Value; + user = await this._client.Cache.TryGetMemberAsync(userId, this._guildId); + if (user is null) + { + if (this._guild?._members.TryGetValue(userId, out DiscordMember? guildMember) ?? false) + { + user = guildMember; + } + } + if (user is null) + { + user = await this._client.Cache.TryGetUserAsync(userId); + } + messageEntry.Target = new CachedEntity(userId, member); } break; @@ -296,38 +285,41 @@ internal static async Task ParseAuditLogEntryAsync DiscordAuditLogMessagePinEntry messagePinEntry = (DiscordAuditLogMessagePinEntry)entry; - if (guild.Discord is not DiscordClient dc) + if (this._client is not DiscordClient dc) { break; } if (auditLogAction.Options != null) { - DiscordMessage? message = default; - dc.MessageCache?.TryGet(auditLogAction.Options.MessageId, out message); - - messagePinEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? - new DiscordChannel - { - Id = auditLogAction.Options.ChannelId, - Discord = guild.Discord, - GuildId = guild.Id - }; - messagePinEntry.Message = message ?? new DiscordMessage - { - Id = auditLogAction.Options.MessageId, - Discord = guild.Discord - }; + channelId = auditLogAction.Options.ChannelId; + ulong messageId = auditLogAction.Options.MessageId; + + DiscordMessage? message = null; + message = await this._client.Cache.TryGetMessageAsync(messageId); + messagePinEntry.Message = new CachedEntity(messageId, message); + + channel = await this._client.Cache.TryGetChannelAsync(channelId); + messagePinEntry.Channel = new CachedEntity(channelId, channel); + } if (auditLogAction.TargetId.HasValue) { - dc.UserCache.TryGetValue(auditLogAction.TargetId.Value, out DiscordUser? user); - messagePinEntry.Target = user ?? new DiscordUser - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord - }; + targetId = auditLogAction.TargetId.Value; + user = await this._client.Cache.TryGetMemberAsync(targetId, this._guildId); + if (user is null) + { + if (this._guild?._members.TryGetValue(targetId, out DiscordMember? guildMember) ?? false) + { + user = guildMember; + } + } + if (user is null) + { + user = await this._client.Cache.TryGetUserAsync(targetId); + } + messagePinEntry.Target = new CachedEntity(targetId, user); } break; @@ -335,21 +327,28 @@ internal static async Task ParseAuditLogEntryAsync case DiscordAuditLogActionType.BotAdd: { - entry = new DiscordAuditLogBotAddEntry(); + DiscordAuditLogBotAddEntry botAddEntry = new DiscordAuditLogBotAddEntry(); - if (!(guild.Discord is DiscordClient dc && auditLogAction.TargetId.HasValue)) + if (!(this._client is DiscordClient dc && auditLogAction.TargetId.HasValue)) { break; } - dc.UserCache.TryGetValue(auditLogAction.TargetId.Value, out DiscordUser? bot); - (entry as DiscordAuditLogBotAddEntry)!.TargetBot = bot - ?? new DiscordUser + ulong botId = auditLogAction.TargetId.Value; + DiscordUser? bot = await this._client.Cache.TryGetMemberAsync(botId, this._guildId); + if (bot is null) + { + if (this._guild?._members.TryGetValue(botId, out DiscordMember? botMember) ?? false) { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord - }; - + bot = botMember; + } + } + if (bot is null) + { + bot = await this._client.Cache.TryGetUserAsync(botId); + } + botAddEntry.TargetBot = new CachedEntity(botId, bot); + entry = botAddEntry; break; } @@ -362,16 +361,12 @@ internal static async Task ParseAuditLogEntryAsync } DiscordAuditLogMemberMoveEntry? memberMoveEntry = (DiscordAuditLogMemberMoveEntry)entry; - + channelId = auditLogAction.Options.ChannelId; + DiscordChannel? movedInChannel = await this._client.Cache.TryGetChannelAsync(channelId); + memberMoveEntry.Channel = new CachedEntity(channelId, movedInChannel); memberMoveEntry.UserCount = auditLogAction.Options.Count; - memberMoveEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? new DiscordChannel - { - Id = auditLogAction.Options.ChannelId, - Discord = guild.Discord, - GuildId = guild.Id - }; break; - + case DiscordAuditLogActionType.MemberDisconnect: entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = auditLogAction.Options?.Count ?? 0 }; break; @@ -379,19 +374,19 @@ internal static async Task ParseAuditLogEntryAsync case DiscordAuditLogActionType.IntegrationCreate: case DiscordAuditLogActionType.IntegrationDelete: case DiscordAuditLogActionType.IntegrationUpdate: - entry = ParseIntegrationUpdateEntry(guild, auditLogAction); + entry = ParseIntegrationUpdateEntry(auditLogAction); break; case DiscordAuditLogActionType.GuildScheduledEventCreate: case DiscordAuditLogActionType.GuildScheduledEventDelete: case DiscordAuditLogActionType.GuildScheduledEventUpdate: - entry = ParseGuildScheduledEventUpdateEntry(guild, auditLogAction, events); + entry = await this.ParseGuildScheduledEventUpdateEntryAsync(auditLogAction, events); break; case DiscordAuditLogActionType.ThreadCreate: case DiscordAuditLogActionType.ThreadDelete: case DiscordAuditLogActionType.ThreadUpdate: - entry = ParseThreadUpdateEntry(guild, auditLogAction, threads); + entry = await this.ParseThreadUpdateEntryAsync(auditLogAction, threads); break; case DiscordAuditLogActionType.ApplicationCommandPermissionUpdate: @@ -431,40 +426,51 @@ internal static async Task ParseAuditLogEntryAsync case DiscordAuditLogActionType.AutoModerationBlockMessage: case DiscordAuditLogActionType.AutoModerationFlagToChannel: case DiscordAuditLogActionType.AutoModerationUserCommunicationDisabled: - entry = new DiscordAuditLogAutoModerationExecutedEntry(); - - DiscordAuditLogAutoModerationExecutedEntry autoModerationEntry = - (DiscordAuditLogAutoModerationExecutedEntry)entry; - + DiscordAuditLogAutoModerationExecutedEntry autoModerationEntry = new DiscordAuditLogAutoModerationExecutedEntry(); + + + channelId = auditLogAction.Options.ChannelId; + if (auditLogAction.TargetId is not null) { - autoModerationEntry.TargetUser = - members.TryGetValue(auditLogAction.TargetId.Value, out DiscordMember? targetMember) - ? targetMember - : new DiscordUser - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord - }; + targetId = auditLogAction.TargetId.Value; + member = await this._client.Cache.TryGetMemberAsync(targetId, this._guildId); + if (member is null) + { + if (this._guild?._members.TryGetValue(targetId, out DiscordMember? guildMember) ?? false) + { + member = guildMember; + } + } + autoModerationEntry.TargetUser = new CachedEntity(targetId, member); + } + + DiscordChannel? modActionChanel = await this._client.Cache.TryGetChannelAsync(channelId); + if (modActionChanel is null) + { + modActionChanel = this._guild?.Channels.TryGetValue(channelId, out DiscordChannel? guildChannel) ?? false ? guildChannel : null; } + autoModerationEntry.Channel = new CachedEntity(channelId, modActionChanel); + - autoModerationEntry.ResponsibleRule = auditLogAction.Options.RoleName; - autoModerationEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId); + autoModerationEntry.ResponsibleRule = auditLogAction.Options.AutoModerationRuleName; autoModerationEntry.RuleTriggerType = (DiscordRuleTriggerType)int.Parse(auditLogAction.Options.AutoModerationRuleTriggerType); + + entry = autoModerationEntry; break; case DiscordAuditLogActionType.AutoModerationRuleCreate: case DiscordAuditLogActionType.AutoModerationRuleUpdate: case DiscordAuditLogActionType.AutoModerationRuleDelete: - entry = ParseAutoModerationRuleUpdateEntry(guild, auditLogAction); + entry = await this.ParseAutoModerationRuleUpdateEntryAsync(auditLogAction); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown audit log action type: {type} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown audit log action type: {type}", (int)auditLogAction.ActionType); } @@ -502,14 +508,23 @@ or DiscordAuditLogActionType.StickerUpdate entry.ActionType = auditLogAction.ActionType; entry.Id = auditLogAction.Id; entry.Reason = auditLogAction.Reason; - entry.Discord = guild.Discord; - - entry.UserResponsible = members.TryGetValue(auditLogAction.UserId, out DiscordMember? member) - ? member - : guild.Discord.UserCache.TryGetValue(auditLogAction.UserId, out DiscordUser? discordUser) - ? discordUser - : new DiscordUser { Id = auditLogAction.UserId, Discord = guild.Discord }; - + entry.Discord = this._client; + entry.Guild = new CachedEntity(this._guildId, this._guild); + + DiscordUser? responsibleMember = await this._client.Cache.TryGetMemberAsync(auditLogAction.UserId, this._guildId); + if (responsibleMember is null) + { + if (this._guild?._members.TryGetValue(auditLogAction.UserId, out DiscordMember? guildMember) ?? false) + { + responsibleMember = guildMember; + } + } + if (responsibleMember is null) + { + responsibleMember = await this._client.Cache.TryGetUserAsync(auditLogAction.UserId); + } + entry.UserResponsible = new CachedEntity(auditLogAction.UserId, responsibleMember); + return entry; } @@ -518,8 +533,7 @@ or DiscordAuditLogActionType.StickerUpdate /// /// which is the parent of the entry /// which should be parsed - private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction) + private async Task ParseAutoModerationRuleUpdateEntryAsync(AuditLogAction auditLogAction) { DiscordAuditLogAutoModerationRuleEntry ruleEntry = new(); @@ -528,27 +542,27 @@ private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpd switch (change.Key.ToLowerInvariant()) { case "id": - ruleEntry.RuleId = PropertyChange.From(change); + ruleEntry.RuleId = PropertyChange.From(change); break; case "guild_id": - ruleEntry.GuildId = PropertyChange.From(change); + ruleEntry.GuildId = PropertyChange.From(change); break; case "name": - ruleEntry.Name = PropertyChange.From(change); + ruleEntry.Name = PropertyChange.From(change); break; case "creator_id": - ruleEntry.CreatorId = PropertyChange.From(change); + ruleEntry.CreatorId = PropertyChange.From(change); break; case "event_type": - ruleEntry.EventType = PropertyChange.From(change); + ruleEntry.EventType = PropertyChange.From(change); break; case "trigger_type": - ruleEntry.TriggerType = PropertyChange.From(change); + ruleEntry.TriggerType = PropertyChange.From(change); break; case "trigger_metadata": @@ -560,39 +574,62 @@ private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpd break; case "enabled": - ruleEntry.Enabled = PropertyChange.From(change); + ruleEntry.Enabled = PropertyChange.From(change); break; case "exempt_roles": JArray oldRoleIds = (JArray)change.OldValue; JArray newRoleIds = (JArray)change.NewValue; - IEnumerable? oldRoles = oldRoleIds? + IEnumerable> oldRoles = oldRoleIds? .Select(x => x.ToObject()) - .Select(guild.GetRole); + .Select(x => new CachedEntity(x, this._guild?.GetRole(x))); - IEnumerable? newRoles = newRoleIds? + IEnumerable> newRoles = newRoleIds? .Select(x => x.ToObject()) - .Select(guild.GetRole); + .Select(x => new CachedEntity(x, this._guild?.GetRole(x))); ruleEntry.ExemptRoles = - PropertyChange?>.From(oldRoles, newRoles); + PropertyChange>>.From(oldRoles, newRoles); break; case "exempt_channels": JArray oldChannelIds = (JArray)change.OldValue; JArray newChanelIds = (JArray)change.NewValue; - IEnumerable? oldChannels = oldChannelIds? + IEnumerable? oldChannelsIds = oldChannelIds .Select(x => x.ToObject()) - .Select(guild.GetChannel); - - IEnumerable? newChannels = newChanelIds? + .ToList(); + + IEnumerable? newChannelsIds = newChanelIds .Select(x => x.ToObject()) - .Select(guild.GetChannel); + .ToList(); + List> oldChannels = new(); + List> newChannels = new(); + + foreach (ulong oldChannelId in oldChannelsIds) + { + DiscordChannel? channel = await this._client.Cache.TryGetChannelAsync(oldChannelId); + if (channel is null) + { + channel = this._guild?.GetChannel(oldChannelId); + } + oldChannels.Add(new CachedEntity(oldChannelId, channel)); + } + + foreach (ulong newChannelId in newChannelsIds) + { + DiscordChannel? channel = await this._client.Cache.TryGetChannelAsync(newChannelId); + if (channel is null) + { + channel = this._guild?.GetChannel(newChannelId); + } + newChannels.Add(new CachedEntity(newChannelId, channel)); + } + ruleEntry.ExemptChannels = - PropertyChange?>.From(oldChannels, newChannels); + PropertyChange>>.From(oldChannels, newChannels); break; case "$add_keyword_filter": @@ -620,10 +657,10 @@ private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpd break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (_client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in AutoModRule update: {Key} - this should be reported to library developers", + _client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in AutoModRule update: {Key}", change.Key); } @@ -641,16 +678,26 @@ private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpd /// which should be parsed /// Dictionary of to populate entry with thread entities /// - internal static DiscordAuditLogThreadEventEntry ParseThreadUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction, - IDictionary threads) + internal async Task ParseThreadUpdateEntryAsync + ( + AuditLogAction auditLogAction, + IDictionary threads + ) { + ulong targetId = auditLogAction.TargetId.Value; + DiscordThreadChannel? target = null; + if (threads.TryGetValue(auditLogAction.TargetId.Value, out DiscordThreadChannel? t)) + { + target = t; + } + if (target is null) + { + target = (DiscordThreadChannel) await this._client.Cache.TryGetChannelAsync(targetId); + } + DiscordAuditLogThreadEventEntry entry = new() { - Target = - threads.TryGetValue(auditLogAction.TargetId!.Value, - out DiscordThreadChannel? channel) - ? channel - : new DiscordThreadChannel() { Id = auditLogAction.TargetId.Value, Discord = guild.Discord }, + Target = new CachedEntity(targetId, target) }; foreach (AuditLogActionChange change in auditLogAction.Changes) @@ -658,42 +705,42 @@ internal static DiscordAuditLogThreadEventEntry ParseThreadUpdateEntry(DiscordGu switch (change.Key.ToLowerInvariant()) { case "name": - entry.Name = PropertyChange.From(change); + entry.Name = PropertyChange.From(change); break; case "type": - entry.Type = PropertyChange.From(change); + entry.Type = PropertyChange.From(change); break; case "archived": - entry.Archived = PropertyChange.From(change); + entry.Archived = PropertyChange.From(change); break; case "auto_archive_duration": - entry.AutoArchiveDuration = PropertyChange.From(change); + entry.AutoArchiveDuration = PropertyChange.From(change); break; case "invitable": - entry.Invitable = PropertyChange.From(change); + entry.Invitable = PropertyChange.From(change); break; case "locked": - entry.Locked = PropertyChange.From(change); + entry.Locked = PropertyChange.From(change); break; case "rate_limit_per_user": - entry.PerUserRateLimit = PropertyChange.From(change); + entry.PerUserRateLimit = PropertyChange.From(change); break; case "flags": - entry.Flags = PropertyChange.From(change); + entry.Flags = PropertyChange.From(change); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in thread update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in thread update: {Key}", change.Key); } @@ -711,15 +758,26 @@ internal static DiscordAuditLogThreadEventEntry ParseThreadUpdateEntry(DiscordGu /// which should be parsed /// Dictionary of to populate entry with event entities /// - private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction, IDictionary events) + private async Task ParseGuildScheduledEventUpdateEntryAsync + ( + AuditLogAction auditLogAction, + IDictionary events + ) { + ulong targetId = auditLogAction.TargetId.Value; + DiscordScheduledGuildEvent? target = null; + if (events.TryGetValue(targetId, out DiscordScheduledGuildEvent? t)) + { + target = t; + } + if (target is null) + { + this._guild?._scheduledEvents.TryGetValue(targetId, out target); + } + DiscordAuditLogGuildScheduledEventEntry entry = new() { - Target = - events.TryGetValue(auditLogAction.TargetId!.Value, out DiscordScheduledGuildEvent? ta) - ? ta - : new DiscordScheduledGuildEvent() { Id = auditLogAction.Id, Discord = guild.Discord }, + Target = new CachedEntity(auditLogAction.TargetId.Value, target) }; foreach (AuditLogActionChange change in auditLogAction.Changes) @@ -727,30 +785,42 @@ private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventU switch (change.Key.ToLowerInvariant()) { case "name": - entry.Name = PropertyChange.From(change); + entry.Name = PropertyChange.From(change); break; case "channel_id": - - ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out ulong newChannelId); ulong.TryParse(change.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong oldChannelId); + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out ulong newChannelId); - entry.Channel = new PropertyChange + DiscordChannel? oldChannel = null; + CachedEntity? cachedOldChannel = null; + if (change.OldValue is not null) { - Before = - guild.GetChannel(newChannelId) ?? new DiscordChannel - { - Id = change.OldValueUlong, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(newChannelId) ?? new DiscordChannel + oldChannel = await this._client.Cache.TryGetChannelAsync(oldChannelId); + if (oldChannel is null) { - Id = change.NewValueUlong, - Discord = guild.Discord, - GuildId = guild.Id + this._guild?.Channels.TryGetValue(oldChannelId, out oldChannel); } + cachedOldChannel = new CachedEntity(oldChannelId, oldChannel); + } + + DiscordChannel? newChannel = null; + CachedEntity? cachedNewChannel = null; + if (change.NewValue is not null) + { + newChannel = await this._client.Cache.TryGetChannelAsync(newChannelId); + if (newChannel is null) + { + this._guild?.Channels.TryGetValue(newChannelId, out newChannel); + } + cachedNewChannel = new CachedEntity(newChannelId, newChannel); + } + + entry.Channel = new PropertyChange>() + { + Before = cachedOldChannel ?? default, + After = cachedNewChannel ?? default }; break; @@ -759,7 +829,7 @@ private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventU break; case "entity_type": - entry.Type = PropertyChange.From(change); + entry.Type = PropertyChange.From(change); break; case "image_hash": @@ -771,18 +841,18 @@ private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventU break; case "privacy_level": - entry.PrivacyLevel = PropertyChange.From(change); + entry.PrivacyLevel = PropertyChange.From(change); break; case "status": - entry.Status = PropertyChange.From(change); + entry.Status = PropertyChange.From(change); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in scheduled event update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in scheduled event update: {Key}", change.Key); } @@ -799,10 +869,14 @@ private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventU /// which is the parent of the entry /// which should be parsed /// - internal static async Task ParseGuildUpdateAsync(DiscordGuild guild, - AuditLogAction auditLogAction) + internal async Task ParseGuildUpdateAsync(AuditLogAction auditLogAction) { - DiscordAuditLogGuildEntry entry = new() { Target = guild }; + this._guild ??= await this._client.Cache.TryGetGuildAsync(this._guildId); + + CachedEntity cachedGuild = new CachedEntity(_guildId, _guild); + + + DiscordAuditLogGuildEntry entry = new() { Target = cachedGuild}; ulong before, after; foreach (AuditLogActionChange? change in auditLogAction.Changes) @@ -814,58 +888,94 @@ internal static async Task ParseGuildUpdateAsync(Disc break; case "owner_id": - entry.OwnerChange = new PropertyChange + + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out before); + ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out after); + + DiscordMember? memberBefore = null; + CachedEntity cachedMemberBefore = default; + if (change.OldValue is not null) { - Before = guild._members != null && guild._members.TryGetValue( - change.OldValueUlong, - out DiscordMember? oldMember) - ? oldMember - : await guild.GetMemberAsync(change.OldValueUlong), - After = guild._members != null && guild._members.TryGetValue(change.NewValueUlong, - out DiscordMember? newMember) - ? newMember - : await guild.GetMemberAsync(change.NewValueUlong) - }; + memberBefore = await this._client.Cache.TryGetMemberAsync(before, this._guildId); + if (memberBefore is null) + { + this._guild?._members.TryGetValue(before, out memberBefore); + } + + cachedMemberBefore = new CachedEntity(before, memberBefore); + } + + DiscordMember? memberAfter = null; + CachedEntity cachedMemberAfter = default; + if (change.NewValue is not null) + { + memberAfter = await this._client.Cache.TryGetMemberAsync(after, this._guildId); + if (memberAfter is null) + { + this._guild?._members.TryGetValue(after, out memberAfter); + } + + cachedMemberAfter = new CachedEntity(after, memberAfter); + } + + entry.OwnerChange = + PropertyChange>.From(cachedMemberBefore, cachedMemberAfter); break; case "icon_hash": entry.IconChange = new PropertyChange { Before = change.OldValueString != null - ? $"https://cdn.discordapp.com/icons/{guild.Id}/{change.OldValueString}.webp" + ? $"https://cdn.discordapp.com/icons/{this._guildId}/{change.OldValueString}.webp" : null, After = change.OldValueString != null - ? $"https://cdn.discordapp.com/icons/{guild.Id}/{change.NewValueString}.webp" + ? $"https://cdn.discordapp.com/icons/{this._guildId}/{change.NewValueString}.webp" : null }; break; case "verification_level": - entry.VerificationLevelChange = PropertyChange.From(change); + entry.VerificationLevelChange = PropertyChange.From(change); break; case "afk_channel_id": - + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out before); ulong.TryParse(change.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out after); - - entry.AfkChannelChange = new PropertyChange + + DiscordChannel? channelBefore = null; + CachedEntity cachedChannelBefore = default; + if (change.OldValue is not null) { - Before = guild.GetChannel(before) ?? new DiscordChannel + channelBefore = await this._client.Cache.TryGetChannelAsync(before); + if (channelBefore is null) { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(after) ?? new DiscordChannel + this._guild?.Channels.TryGetValue(before, out channelBefore); + } + + cachedChannelBefore = new CachedEntity(before, channelBefore); + } + + DiscordChannel? channelAfter = null; + CachedEntity cachedChannelAfter = default; + if (change.NewValue is not null) + { + channelAfter = await this._client.Cache.TryGetChannelAsync(after); + if (channelAfter is null) { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id + this._guild?.Channels.TryGetValue(after, out channelAfter); } - }; + + cachedChannelAfter = new CachedEntity(after, channelAfter); + } + + entry.AfkChannelChange = + PropertyChange>.From(cachedChannelBefore, + cachedChannelAfter); break; case "widget_channel_id": @@ -873,38 +983,51 @@ internal static async Task ParseGuildUpdateAsync(Disc CultureInfo.InvariantCulture, out before); ulong.TryParse(change.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out after); - - entry.EmbedChannelChange = new PropertyChange + + channelBefore = null; + cachedChannelBefore = default; + if (change.OldValue is not null) { - Before = guild.GetChannel(before) ?? new DiscordChannel + channelBefore = await this._client.Cache.TryGetChannelAsync(before); + if (channelBefore is null) { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(after) ?? new DiscordChannel + this._guild?.Channels.TryGetValue(before, out channelBefore); + } + + cachedChannelBefore = new CachedEntity(before, channelBefore); + } + + channelAfter = null; + cachedChannelAfter = default; + if (change.NewValue is not null) + { + channelAfter = await this._client.Cache.TryGetChannelAsync(after); + if (channelAfter is null) { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id + this._guild?.Channels.TryGetValue(after, out channelAfter); } - }; + + cachedChannelAfter = new CachedEntity(after, channelAfter); + } + + entry.EmbedChannelChange = + PropertyChange>.From(cachedChannelBefore, cachedChannelAfter); break; case "splash_hash": entry.SplashChange = new PropertyChange { Before = change.OldValueString != null - ? $"https://cdn.discordapp.com/splashes/{guild.Id}/{change.OldValueString}.webp?size=2048" + ? $"https://cdn.discordapp.com/splashes/{this._guildId}/{change.OldValueString}.webp?size=2048" : null, After = change.NewValueString != null - ? $"https://cdn.discordapp.com/splashes/{guild.Id}/{change.NewValueString}.webp?size=2048" + ? $"https://cdn.discordapp.com/splashes/{this._guildId}/{change.NewValueString}.webp?size=2048" : null }; break; case "default_message_notifications": - entry.NotificationSettingsChange = PropertyChange.From(change); + entry.NotificationSettingsChange = PropertyChange.From(change); break; case "system_channel_id": @@ -912,41 +1035,55 @@ internal static async Task ParseGuildUpdateAsync(Disc CultureInfo.InvariantCulture, out before); ulong.TryParse(change.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out after); - - entry.SystemChannelChange = new PropertyChange + + channelBefore = null; + cachedChannelBefore = default; + if (change.OldValue is not null) { - Before = guild.GetChannel(before) ?? new DiscordChannel + channelBefore = await this._client.Cache.TryGetChannelAsync(before); + if (channelBefore is null) { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(after) ?? new DiscordChannel + this._guild?.Channels.TryGetValue(before, out channelBefore); + } + + cachedChannelBefore = new CachedEntity(before, channelBefore); + } + + channelAfter = null; + cachedChannelAfter = default; + if (change.NewValue is not null) + { + channelAfter = await this._client.Cache.TryGetChannelAsync(after); + if (channelAfter is null) { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id + this._guild?.Channels.TryGetValue(after, out channelAfter); } - }; + + cachedChannelAfter = new CachedEntity(after, channelAfter); + } + + entry.SystemChannelChange = + PropertyChange>.From(cachedChannelBefore, + cachedChannelAfter); break; case "explicit_content_filter": - entry.ExplicitContentFilterChange = PropertyChange.From(change); + entry.ExplicitContentFilterChange = PropertyChange.From(change); break; case "mfa_level": - entry.MfaLevelChange = PropertyChange.From(change); + entry.MfaLevelChange = PropertyChange.From(change); break; case "region": - entry.RegionChange = PropertyChange.From(change); + entry.RegionChange = PropertyChange.From(change); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in guild update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in guild update: {Key}", change.Key); } @@ -963,16 +1100,15 @@ internal static async Task ParseGuildUpdateAsync(Disc /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild, AuditLogAction auditLogAction) + internal async Task ParseChannelEntryAsync(AuditLogAction auditLogAction) { + ulong channelId = auditLogAction.TargetId.Value; + DiscordChannel? channel = await this._client.Cache.TryGetChannelAsync(channelId); + CachedEntity cachedChannel = new CachedEntity(channelId, channel); + DiscordAuditLogChannelEntry entry = new() { - Target = guild.GetChannel(auditLogAction.TargetId!.Value) ?? new DiscordChannel - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord, - GuildId = guild.Id - } + Target = cachedChannel }; foreach (AuditLogActionChange? change in auditLogAction.Changes) @@ -984,7 +1120,7 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild break; case "type": - entry.TypeChange = PropertyChange.From(change); + entry.TypeChange = PropertyChange.From(change); break; case "permission_overwrites": @@ -993,7 +1129,7 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild .Select(jObject => jObject.ToDiscordObject())? .Select(overwrite => { - overwrite.Discord = guild.Discord; + overwrite.Discord = this._client; return overwrite; }); @@ -1001,7 +1137,7 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild .Select(jObject => jObject.ToDiscordObject())? .Select(overwrite => { - overwrite.Discord = guild.Discord; + overwrite.Discord = this._client; return overwrite; }); @@ -1025,23 +1161,23 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild break; case "nsfw": - entry.NsfwChange = PropertyChange.From(change); + entry.NsfwChange = PropertyChange.From(change); break; case "bitrate": - entry.BitrateChange = PropertyChange.From(change); + entry.BitrateChange = PropertyChange.From(change); break; case "rate_limit_per_user": - entry.PerUserRateLimitChange = PropertyChange.From(change); + entry.PerUserRateLimitChange = PropertyChange.From(change); break; case "user_limit": - entry.UserLimit = PropertyChange.From(change); + entry.UserLimit = PropertyChange.From(change); break; case "flags": - entry.Flags = PropertyChange.From(change); + entry.Flags = PropertyChange.From(change); break; case "available_tags": @@ -1049,7 +1185,7 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild .Select(jObject => jObject.ToDiscordObject())? .Select(forumTag => { - forumTag.Discord = guild.Discord; + forumTag.Discord = this._client; return forumTag; }); @@ -1057,7 +1193,7 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild .Select(jObject => jObject.ToDiscordObject())? .Select(forumTag => { - forumTag.Discord = guild.Discord; + forumTag.Discord = this._client; return forumTag; }); @@ -1066,10 +1202,10 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in channel update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in channel update: {Key}", change.Key); } @@ -1086,16 +1222,36 @@ internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogOverwriteEntry ParseOverwriteEntry(DiscordGuild guild, - AuditLogAction auditLogAction) + internal async Task ParseOverwriteEntryAsync(AuditLogAction auditLogAction) { + ulong overwriteId = auditLogAction.Options.Id; + ulong channelId = auditLogAction.TargetId.Value; + + this._guild ??= await this._client.Cache.TryGetGuildAsync(this._guildId); + + DiscordChannel? channel = await this._client.Cache.TryGetChannelAsync(channelId); + if (channel is null) + { + this._guild?.Channels.TryGetValue(channelId, out channel); + } + + DiscordOverwrite? overwrite = channel? + .PermissionOverwrites + .FirstOrDefault(xo => xo.Id == overwriteId); + + if (overwrite is null) + { + overwrite = this._guild?.GetChannel(channelId)?._permissionOverwrites.FirstOrDefault(xo => xo.Id == overwriteId);; + } + + CachedEntity cachedOverwrite = new CachedEntity(overwriteId, overwrite); + CachedEntity cachedChannel = new CachedEntity(channelId, channel); + + DiscordAuditLogOverwriteEntry entry = new() { - Target = guild - .GetChannel(auditLogAction.TargetId!.Value) - .PermissionOverwrites - .FirstOrDefault(xo => xo.Id == auditLogAction.Options.Id), - Channel = guild.GetChannel(auditLogAction.TargetId.Value) + Target = cachedOverwrite, + Channel = cachedChannel }; foreach (AuditLogActionChange? change in auditLogAction.Changes) @@ -1103,11 +1259,11 @@ internal static DiscordAuditLogOverwriteEntry ParseOverwriteEntry(DiscordGuild g switch (change.Key.ToLowerInvariant()) { case "deny": - entry.DeniedPermissions = PropertyChange.From(change); + entry.DeniedPermissions = PropertyChange.From(change); break; case "allow": - entry.AllowedPermissions = PropertyChange.From(change); + entry.AllowedPermissions = PropertyChange.From(change); break; case "type": @@ -1115,14 +1271,14 @@ internal static DiscordAuditLogOverwriteEntry ParseOverwriteEntry(DiscordGuild g break; case "id": - entry.TargetIdChange = PropertyChange.From(change); + entry.TargetIdChange = PropertyChange.From(change); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in overwrite update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in overwrite update: {Key}", change.Key); } @@ -1139,14 +1295,19 @@ internal static DiscordAuditLogOverwriteEntry ParseOverwriteEntry(DiscordGuild g /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogMemberUpdateEntry ParseMemberUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction) + internal async Task ParseMemberUpdateEntryAsync(AuditLogAction auditLogAction) { + ulong memberId = auditLogAction.TargetId.Value; + DiscordMember? member = await this._client.Cache.TryGetMemberAsync(memberId, this._guildId); + if (member is null) + { + this._guild?._members.TryGetValue(memberId, out member); + } + CachedEntity cachedMember = new (memberId, member); + DiscordAuditLogMemberUpdateEntry entry = new() { - Target = guild._members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? roleUpdMember) - ? roleUpdMember - : new DiscordMember { Id = auditLogAction.TargetId.Value, Discord = guild.Discord, _guild_id = guild.Id } + Target = cachedMember }; foreach (AuditLogActionChange? change in auditLogAction.Changes) @@ -1158,35 +1319,45 @@ internal static DiscordAuditLogMemberUpdateEntry ParseMemberUpdateEntry(DiscordG break; case "deaf": - entry.DeafenChange = PropertyChange.From(change); + entry.DeafenChange = PropertyChange.From(change); break; case "mute": - entry.MuteChange = PropertyChange.From(change); + entry.MuteChange = PropertyChange.From(change); break; case "communication_disabled_until": - entry.TimeoutChange = PropertyChange.From(change); + entry.TimeoutChange = PropertyChange.From(change); break; case "$add": + List> addedRoles = + change.NewValues? + .Select(xo => (ulong)xo["id"]) + .Select(x => new CachedEntity(x, this._guild?.GetRole(x))) + .ToList(); + entry.AddedRoles = - new ReadOnlyCollection(change.NewValues.Select(xo => (ulong)xo["id"]!) - .Select(guild.GetRole).ToList()); + new ReadOnlyCollection>(addedRoles); break; case "$remove": + List> removedRoles = + change.NewValues? + .Select(xo => (ulong)xo["id"]) + .Select(x => new CachedEntity(x, this._guild?.GetRole(x))) + .ToList(); + entry.RemovedRoles = - new ReadOnlyCollection(change.NewValues.Select(xo => (ulong)xo["id"]!) - .Select(guild.GetRole).ToList()); + new ReadOnlyCollection>(removedRoles); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in member update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in member update: {Key}", change.Key); } @@ -1203,13 +1374,14 @@ internal static DiscordAuditLogMemberUpdateEntry ParseMemberUpdateEntry(DiscordG /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogRoleUpdateEntry ParseRoleUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction) + internal DiscordAuditLogRoleUpdateEntry ParseRoleUpdateEntry(AuditLogAction auditLogAction) { + ulong roleId = auditLogAction.TargetId.Value; + DiscordRole? discordRole = this._guild?.GetRole(roleId); + DiscordAuditLogRoleUpdateEntry entry = new() { - Target = guild.GetRole(auditLogAction.TargetId!.Value) ?? - new DiscordRole { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + Target = new(roleId, discordRole) }; foreach (AuditLogActionChange? change in auditLogAction.Changes) @@ -1221,29 +1393,30 @@ internal static DiscordAuditLogRoleUpdateEntry ParseRoleUpdateEntry(DiscordGuild break; case "color": - entry.ColorChange = PropertyChange.From(change); + entry.ColorChange = PropertyChange.From(change); break; case "permissions": - entry.PermissionChange = PropertyChange.From(change); + entry.PermissionChange = PropertyChange.From(change); break; case "position": - entry.PositionChange = PropertyChange.From(change); + entry.PositionChange = PropertyChange.From(change); break; case "mentionable": - entry.MentionableChange = PropertyChange.From(change); + entry.MentionableChange = PropertyChange.From(change); break; case "hoist": - entry.HoistChange = PropertyChange.From(change); break; + entry.HoistChange = PropertyChange.From(change); + break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in role update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in role update: {Key}", change.Key); } @@ -1260,19 +1433,19 @@ internal static DiscordAuditLogRoleUpdateEntry ParseRoleUpdateEntry(DiscordGuild /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) + internal async Task ParseInviteUpdateEntryAsync(AuditLogAction auditLogAction) { DiscordAuditLogInviteEntry entry = new(); DiscordInvite invite = new() { - Discord = guild.Discord, + Discord = this._client, Guild = new DiscordInviteGuild { - Discord = guild.Discord, - Id = guild.Id, - Name = guild.Name, - SplashHash = guild.SplashHash + Discord = this._client, + Id = this._guildId, + Name = this._guild?.Name, + SplashHash = _guild?.SplashHash } }; @@ -1284,7 +1457,7 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g switch (change.Key.ToLowerInvariant()) { case "max_age": - entry.MaxAgeChange = PropertyChange.From(change); + entry.MaxAgeChange = PropertyChange.From(change); break; case "code": @@ -1294,30 +1467,57 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g break; case "temporary": - entry.TemporaryChange = new PropertyChange - { - Before = change.OldValue != null ? (bool?)change.OldValue : null, - After = change.NewValue != null ? (bool?)change.NewValue : null - }; + entry.TemporaryChange = PropertyChange.From(change); break; case "inviter_id": - boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + ulong.TryParse(change.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulongBefore); - boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulongAfter); - - entry.InviterChange = new PropertyChange + + DiscordUser? memberBefore = null; + CachedEntity cachedMemberBefore = default; + if (change.OldValue is not null) { - Before = guild._members.TryGetValue(ulongBefore, out DiscordMember? propBeforeMember) - ? propBeforeMember - : new DiscordMember { Id = ulongBefore, Discord = guild.Discord, _guild_id = guild.Id }, - After = guild._members.TryGetValue(ulongAfter, out DiscordMember? propAfterMember) - ? propAfterMember - : new DiscordMember { Id = ulongBefore, Discord = guild.Discord, _guild_id = guild.Id } - }; + memberBefore = await this._client.Cache.TryGetMemberAsync(ulongBefore, this._guildId); + if (memberBefore is null) + { + if (this._guild?._members.TryGetValue(ulongBefore, out DiscordMember? guildMember) ?? false) + { + memberBefore = guildMember; + } + } + if (memberBefore is null) + { + memberBefore = await this._client.Cache.TryGetUserAsync(ulongBefore); + } + cachedMemberBefore = new CachedEntity(ulongBefore, memberBefore); + } + + DiscordUser? memberAfter = null; + CachedEntity cachedMemberAfter = default; + if (change.NewValue is not null) + { + memberAfter = await this._client.Cache.TryGetMemberAsync(ulongAfter, this._guildId); + if (memberAfter is null) + { + if (this._guild?._members.TryGetValue(ulongAfter, out DiscordMember? guildMember) ?? false) + { + memberAfter = guildMember; + } + } + if (memberAfter is null) + { + memberAfter = await this._client.Cache.TryGetUserAsync(ulongAfter); + } + cachedMemberAfter = new CachedEntity(ulongAfter, memberAfter); + } + + entry.InviterChange = + PropertyChange>.From(cachedMemberBefore, cachedMemberAfter); break; case "channel_id": @@ -1327,25 +1527,51 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulongAfter); - - entry.ChannelChange = new PropertyChange + + DiscordChannel? channelBefore = null; + CachedEntity cachedChannelBefore = default; + if (change.OldValue is not null) { - Before = boolBefore - ? guild.GetChannel(ulongBefore) ?? - new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } - : null, - After = boolAfter - ? guild.GetChannel(ulongAfter) ?? - new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } - : null - }; + channelBefore = await this._client.Cache.TryGetChannelAsync(ulongBefore); + if (channelBefore is null) + { + this._guild?.Channels.TryGetValue(ulongBefore, out channelBefore); + } + cachedChannelBefore = new CachedEntity(ulongBefore, channelBefore); + } + + DiscordChannel? channelAfter = null; + CachedEntity cachedChannelAfter = default; + if (change.NewValue is not null) + { + channelAfter = await this._client.Cache.TryGetChannelAsync(ulongAfter); + if (channelAfter is null) + { + this._guild?.Channels.TryGetValue(ulongAfter, out channelAfter); + } + cachedChannelAfter = new CachedEntity(ulongAfter, channelAfter); + } + + entry.ChannelChange = + PropertyChange>.From(cachedChannelBefore, cachedChannelAfter); - DiscordChannel? channel = entry.ChannelChange.Before ?? entry.ChannelChange.After; + DiscordChannel? channel = null; + ulong? channelId = null; + if (entry.ChannelChange?.Before.IsDefined(out CachedEntity optionalChannel) ?? false) + { + optionalChannel.TryGetCachedValue(out channel); + } + channelId = channel?.Id; + if (entry.ChannelChange?.After.IsDefined(out optionalChannel) ?? false) + { + optionalChannel.TryGetCachedValue(out channel); + } + ChannelType? channelType = channel?.Type; invite.Channel = new DiscordInviteChannel { - Discord = guild.Discord, - Id = boolBefore ? ulongBefore : ulongAfter, + Discord = this._client, + Id = channel?.Id ?? 0, Name = channel?.Name ?? "", Type = channelType != null ? channelType.Value : ChannelType.Unknown }; @@ -1359,11 +1585,8 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g CultureInfo.InvariantCulture, out intAfter); - entry.UsesChange = new PropertyChange - { - Before = boolBefore ? (int?)intBefore : null, - After = boolAfter ? (int?)intAfter : null - }; + entry.UsesChange = PropertyChange.From(boolBefore ? intBefore : null, + boolAfter ? (int?)intAfter : null); break; case "max_uses": @@ -1374,7 +1597,7 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g CultureInfo.InvariantCulture, out intAfter); - entry.MaxUsesChange = new PropertyChange + entry.MaxUsesChange = new PropertyChange { Before = boolBefore ? (int?)intBefore : null, After = boolAfter ? (int?)intAfter : null @@ -1382,10 +1605,10 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in invite update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in invite update: {Key}", change.Key); } @@ -1404,18 +1627,18 @@ internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild g /// which should be parsed /// Dictionary of to populate entry with webhook entities /// - internal static DiscordAuditLogWebhookEntry ParseWebhookUpdateEntry + internal async Task ParseWebhookUpdateEntryAsync ( - DiscordGuild guild, AuditLogAction auditLogAction, IDictionary webhooks ) { + ulong webhookId = auditLogAction.TargetId.Value; + DiscordWebhook? webhook = webhooks.TryGetValue(webhookId, out DiscordWebhook? w) ? w : null; + DiscordAuditLogWebhookEntry entry = new() { - Target = webhooks.TryGetValue(auditLogAction.TargetId!.Value, out DiscordWebhook? webhook) - ? webhook - : new DiscordWebhook { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + Target = new CachedEntity(webhookId, webhook) }; ulong ulongBefore, ulongAfter; @@ -1433,43 +1656,64 @@ IDictionary webhooks CultureInfo.InvariantCulture, out ulongBefore); boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulongAfter); + + DiscordChannel? channelBefore = null; + CachedEntity? cachedChannelBefore = null; + if (change.OldValue is not null) + { + channelBefore = await this._client.Cache.TryGetChannelAsync(ulongBefore); + cachedChannelBefore = new CachedEntity(ulongBefore, channelBefore); + } + + DiscordChannel? channelAfter = null; + CachedEntity? cachedChannelAfter = null; + if (change.NewValue is not null) + { + channelAfter = await this._client.Cache.TryGetChannelAsync(ulongAfter); + cachedChannelAfter = new CachedEntity(ulongAfter, channelAfter); + } - entry.ChannelChange = new PropertyChange + entry.ChannelChange = new PropertyChange> { - Before = - boolBefore - ? guild.GetChannel(ulongBefore) ?? new DiscordChannel - { - Id = ulongBefore, - Discord = guild.Discord, - GuildId = guild.Id - } - : null, - After = boolAfter - ? guild.GetChannel(ulongAfter) ?? - new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } - : null + Before = cachedChannelBefore ?? default, + After = cachedChannelAfter ?? default }; break; case "type": - entry.TypeChange = PropertyChange.From(change); + entry.TypeChange = PropertyChange.From(change); break; case "avatar_hash": entry.AvatarHashChange = PropertyChange.From(change); break; - case "application_id" - : //Why the fuck does discord send this as a string if it's supposed to be a snowflake - entry.ApplicationIdChange = PropertyChange.From(change); + case "application_id": + ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out ulongBefore); + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out ulongAfter); + + ulong? applicationIdBefore = null; + if (change.OldValue is not null) + { + applicationIdBefore = ulongBefore; + } + + ulong? applicationIdAfter = null; + if (change.NewValue is not null) + { + applicationIdAfter = ulongAfter; + } + + entry.ApplicationIdChange = PropertyChange.From(applicationIdBefore ?? default, applicationIdAfter ?? default); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in webhook update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in webhook update: {Key}", change.Key); } @@ -1486,13 +1730,13 @@ IDictionary webhooks /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogStickerEntry ParseStickerUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) + internal DiscordAuditLogStickerEntry ParseStickerUpdateEntry(AuditLogAction auditLogAction) { + ulong targetId = auditLogAction.TargetId.Value; + DiscordMessageSticker? sticker = this._guild?._stickers.TryGetValue(targetId, out sticker) ?? false ? sticker : null; DiscordAuditLogStickerEntry entry = new() { - Target = guild._stickers.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMessageSticker? sticker) - ? sticker - : new DiscordMessageSticker { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + Target = new CachedEntity(targetId, sticker) }; foreach (AuditLogActionChange change in auditLogAction.Changes) @@ -1512,11 +1756,11 @@ internal static DiscordAuditLogStickerEntry ParseStickerUpdateEntry(DiscordGuild break; case "guild_id": - entry.GuildIdChange = PropertyChange.From(change); + entry.GuildIdChange = PropertyChange.From(change); break; case "available": - entry.AvailabilityChange = PropertyChange.From(change); + entry.AvailabilityChange = PropertyChange.From(change); break; case "asset": @@ -1524,22 +1768,22 @@ internal static DiscordAuditLogStickerEntry ParseStickerUpdateEntry(DiscordGuild break; case "id": - entry.IdChange = PropertyChange.From(change); + entry.IdChange = PropertyChange.From(change); break; case "type": - entry.TypeChange = PropertyChange.From(change); + entry.TypeChange = PropertyChange.From(change); break; case "format_type": - entry.FormatChange = PropertyChange.From(change); + entry.FormatChange = PropertyChange.From(change); break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in sticker update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in sticker update: {Key}", change.Key); } @@ -1556,7 +1800,7 @@ internal static DiscordAuditLogStickerEntry ParseStickerUpdateEntry(DiscordGuild /// which is the parent of the entry /// which should be parsed /// - internal static DiscordAuditLogIntegrationEntry ParseIntegrationUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) + internal DiscordAuditLogIntegrationEntry ParseIntegrationUpdateEntry(AuditLogAction auditLogAction) { DiscordAuditLogIntegrationEntry entry = new(); @@ -1565,15 +1809,15 @@ internal static DiscordAuditLogIntegrationEntry ParseIntegrationUpdateEntry(Disc switch (change.Key.ToLowerInvariant()) { case "enable_emoticons": - entry.EnableEmoticons = PropertyChange.From(change); + entry.EnableEmoticons = PropertyChange.From(change); break; case "expire_behavior": - entry.ExpireBehavior = PropertyChange.From(change); + entry.ExpireBehavior = PropertyChange.From(change); break; case "expire_grace_period": - entry.ExpireBehavior = PropertyChange.From(change); + entry.ExpireBehavior = PropertyChange.From(change); break; case "name": @@ -1585,10 +1829,10 @@ internal static DiscordAuditLogIntegrationEntry ParseIntegrationUpdateEntry(Disc break; default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) + if (this._client.Configuration.LogUnknownAuditlogs) { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in integration update: {Key} - this should be reported to library developers", + this._client.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in integration update: {Key}", change.Key); } diff --git a/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs b/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs index abded8da2c..a7f864f4c0 100644 --- a/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs @@ -1,33 +1,17 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - namespace DSharpPlus.Entities.AuditLogs; +using Caching; + /// -/// Represents an audit log entry. +/// Represents an audit log entry. All fields of type will be null if the property was not changed. /// public abstract class DiscordAuditLogEntry : SnowflakeObject { + /// + /// Gets the entry's guild. + /// + public CachedEntity Guild { get; internal set; } + /// /// Gets the entry's action type. /// @@ -36,7 +20,7 @@ public abstract class DiscordAuditLogEntry : SnowflakeObject /// /// Gets the user responsible for the action. /// - public DiscordUser? UserResponsible { get; internal set; } + public CachedEntity UserResponsible { get; internal set; } /// /// Gets the reason defined in the action. diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs index 7127355f94..cf3e25f42a 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - using System.Collections.Generic; namespace DSharpPlus.Entities.AuditLogs; diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs index 3151774446..e4eb23289d 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -33,7 +12,7 @@ public sealed class DiscordAuditLogAutoModerationExecutedEntry : DiscordAuditLog /// /// User that was affected by the rule /// - public DiscordUser TargetUser { get; internal set; } = default!; + public CachedEntity TargetUser { get; internal set; } /// /// Type of the trigger that was executed @@ -43,5 +22,5 @@ public sealed class DiscordAuditLogAutoModerationExecutedEntry : DiscordAuditLog /// /// Channel where the rule was executed /// - public DiscordChannel Channel { get; internal set; } = default!; + public CachedEntity Channel { get; internal set; } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs index 34d2e92809..e8fc465321 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs @@ -1,87 +1,64 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - using System.Collections.Generic; +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogAutoModerationRuleEntry : DiscordAuditLogEntry { - /// /// Id of the rule /// - public PropertyChange RuleId { get; internal set; } + public PropertyChange? RuleId { get; internal set; } /// /// Id of the guild where the rule was changed /// - public PropertyChange GuildId { get; internal set; } + public PropertyChange? GuildId { get; internal set; } /// /// Name of the rule /// - public PropertyChange Name { get; internal set; } + public PropertyChange? Name { get; internal set; } /// /// Id of the user that created the rule /// - public PropertyChange CreatorId { get; internal set; } + public PropertyChange? CreatorId { get; internal set; } /// /// Indicates in what event context a rule should be checked. /// - public PropertyChange EventType { get; internal set; } + public PropertyChange? EventType { get; internal set; } /// /// Characterizes the type of content which can trigger the rule. /// - public PropertyChange TriggerType { get; internal set; } + public PropertyChange? TriggerType { get; internal set; } /// /// Additional data used to determine whether a rule should be triggered. /// - public PropertyChange TriggerMetadata { get; internal set; } + public PropertyChange? TriggerMetadata { get; internal set; } /// /// Actions which will execute when the rule is triggered. /// - public PropertyChange?> Actions { get; internal set; } + public PropertyChange>? Actions { get; internal set; } /// /// Whether the rule is enabled or not. /// - public PropertyChange Enabled { get; internal set; } + public PropertyChange? Enabled { get; internal set; } /// /// Roles that should not be affected by the rule /// - public PropertyChange?> ExemptRoles { get; internal set; } + public PropertyChange>>? ExemptRoles { get; internal set; } /// /// Channels that should not be affected by the rule /// - public PropertyChange?> ExemptChannels { get; internal set; } + public PropertyChange>>? ExemptChannels { get; internal set; } /// /// List of trigger keywords that were added to the rule diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs index 650f838086..0bd37c003c 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,7 +7,7 @@ public sealed class DiscordAuditLogBanEntry : DiscordAuditLogEntry /// /// Gets the banned member. /// - public DiscordMember Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } internal DiscordAuditLogBanEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs index f4570a9533..951739e102 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,5 +7,5 @@ public sealed class DiscordAuditLogBotAddEntry : DiscordAuditLogEntry /// /// Gets the bot that has been added to the guild. /// - public DiscordUser TargetBot { get; internal set; } = default!; + public CachedEntity TargetBot { get; internal set; } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs index 0358838e5b..6a738c422e 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs @@ -1,27 +1,5 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - using System.Collections.Generic; +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -30,48 +8,48 @@ public sealed class DiscordAuditLogChannelEntry : DiscordAuditLogEntry /// /// Gets the affected channel. /// - public DiscordChannel Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of channel's name change. /// - public PropertyChange NameChange { get; internal set; } + public PropertyChange? NameChange { get; internal set; } /// /// Gets the description of channel's type change. /// - public PropertyChange TypeChange { get; internal set; } + public PropertyChange? TypeChange { get; internal set; } /// /// Gets the description of channel's nsfw flag change. /// - public PropertyChange NsfwChange { get; internal set; } + public PropertyChange? NsfwChange { get; internal set; } /// /// Gets the description of channel's bitrate change. /// - public PropertyChange BitrateChange { get; internal set; } + public PropertyChange? BitrateChange { get; internal set; } /// /// Gets the description of channel permission overwrites' change. /// - public PropertyChange> OverwriteChange { get; internal set; } + public PropertyChange>? OverwriteChange { get; internal set; } /// /// Gets the description of channel's topic change. /// - public PropertyChange TopicChange { get; internal set; } + public PropertyChange? TopicChange { get; internal set; } /// /// Gets the description of channel's slow mode timeout change. /// - public PropertyChange PerUserRateLimitChange { get; internal set; } + public PropertyChange? PerUserRateLimitChange { get; internal set; } - public PropertyChange UserLimit { get; internal set; } + public PropertyChange? UserLimit { get; internal set; } - public PropertyChange Flags { get; internal set; } + public PropertyChange? Flags { get; internal set; } - public PropertyChange> AvailableTags { get; internal set; } + public PropertyChange>? AvailableTags { get; internal set; } internal DiscordAuditLogChannelEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs index 0304cedbe5..74c59a53db 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,7 +7,7 @@ public sealed class DiscordAuditLogEmojiEntry : DiscordAuditLogEntry /// /// Gets the affected emoji. /// - public DiscordEmoji Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of emoji's name change. diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs index bf0f1cc654..0129e97873 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs @@ -1,94 +1,74 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; +/// public sealed class DiscordAuditLogGuildEntry : DiscordAuditLogEntry { /// /// Gets the affected guild. /// - public DiscordGuild Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of guild name's change. /// - public PropertyChange NameChange { get; internal set; } + public PropertyChange? NameChange { get; internal set; } /// /// Gets the description of owner's change. /// - public PropertyChange OwnerChange { get; internal set; } + public PropertyChange>? OwnerChange { get; internal set; } /// /// Gets the description of icon's change. /// - public PropertyChange IconChange { get; internal set; } + public PropertyChange? IconChange { get; internal set; } /// /// Gets the description of verification level's change. /// - public PropertyChange VerificationLevelChange { get; internal set; } + public PropertyChange? VerificationLevelChange { get; internal set; } /// /// Gets the description of afk channel's change. /// - public PropertyChange AfkChannelChange { get; internal set; } + public PropertyChange>? AfkChannelChange { get; internal set; } /// /// Gets the description of widget channel's change. /// - public PropertyChange EmbedChannelChange { get; internal set; } + public PropertyChange>? EmbedChannelChange { get; internal set; } /// /// Gets the description of notification settings' change. /// - public PropertyChange NotificationSettingsChange { get; internal set; } + public PropertyChange? NotificationSettingsChange { get; internal set; } /// /// Gets the description of system message channel's change. /// - public PropertyChange SystemChannelChange { get; internal set; } + public PropertyChange>? SystemChannelChange { get; internal set; } /// /// Gets the description of explicit content filter settings' change. /// - public PropertyChange ExplicitContentFilterChange { get; internal set; } + public PropertyChange? ExplicitContentFilterChange { get; internal set; } /// /// Gets the description of guild's mfa level change. /// - public PropertyChange MfaLevelChange { get; internal set; } + public PropertyChange? MfaLevelChange { get; internal set; } /// /// Gets the description of invite splash's change. /// - public PropertyChange SplashChange { get; internal set; } + public PropertyChange? SplashChange { get; internal set; } /// /// Gets the description of the guild's region change. /// - public PropertyChange RegionChange { get; internal set; } + public PropertyChange? RegionChange { get; internal set; } internal DiscordAuditLogGuildEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs index 6b95f7f219..edd5d04744 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,47 +7,47 @@ public sealed class DiscordAuditLogGuildScheduledEventEntry : DiscordAuditLogEnt /// /// Gets a change in the event's name /// - public PropertyChange Name { get; internal set; } + public PropertyChange? Name { get; internal set; } /// /// Gets the target event. Note that this will only have the ID specified if it is not cached. /// - public DiscordScheduledGuildEvent Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the channel the event was changed to. /// - public PropertyChange Channel { get; internal set; } + public PropertyChange>? Channel { get; internal set; } /// /// Gets the description change of the event. /// - public PropertyChange Description { get; internal set; } + public PropertyChange? Description { get; internal set; } /// /// Gets the change of type for the event. /// - public PropertyChange Type { get; internal set; } + public PropertyChange? Type { get; internal set; } /// /// Gets the change in image hash. /// - public PropertyChange ImageHash { get; internal set; } + public PropertyChange? ImageHash { get; internal set; } /// /// Gets the change in event location, if it's an external event. /// - public PropertyChange Location { get; internal set; } + public PropertyChange? Location { get; internal set; } /// /// Gets change in privacy level. /// - public PropertyChange PrivacyLevel { get; internal set; } + public PropertyChange? PrivacyLevel { get; internal set; } /// /// Gets the change in status. /// - public PropertyChange Status { get; internal set; } + public PropertyChange? Status { get; internal set; } public DiscordAuditLogGuildScheduledEventEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs index fdabe76965..5b4957640f 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogIntegrationEntry : DiscordAuditLogEntry @@ -28,25 +5,25 @@ public sealed class DiscordAuditLogIntegrationEntry : DiscordAuditLogEntry /// /// Gets the description of emoticons' change. /// - public PropertyChange EnableEmoticons { get; internal set; } + public PropertyChange? EnableEmoticons { get; internal set; } /// /// Gets the description of expire grace period's change. /// - public PropertyChange ExpireGracePeriod { get; internal set; } + public PropertyChange? ExpireGracePeriod { get; internal set; } /// /// Gets the description of expire behavior change. /// - public PropertyChange ExpireBehavior { get; internal set; } + public PropertyChange? ExpireBehavior { get; internal set; } /// /// Gets the type of the integration. /// - public PropertyChange Type { get; internal set; } + public PropertyChange? Type { get; internal set; } /// /// Gets the name of the integration. /// - public PropertyChange Name { get; internal set; } + public PropertyChange? Name { get; internal set; } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs index 3658cc718e..ed884f4b1a 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -33,37 +12,37 @@ public sealed class DiscordAuditLogInviteEntry : DiscordAuditLogEntry /// /// Gets the description of invite's max age change. /// - public PropertyChange MaxAgeChange { get; internal set; } + public PropertyChange? MaxAgeChange { get; internal set; } /// /// Gets the description of invite's code change. /// - public PropertyChange CodeChange { get; internal set; } + public PropertyChange? CodeChange { get; internal set; } /// /// Gets the description of invite's temporariness change. /// - public PropertyChange TemporaryChange { get; internal set; } + public PropertyChange? TemporaryChange { get; internal set; } /// /// Gets the description of invite's inviting member change. /// - public PropertyChange InviterChange { get; internal set; } + public PropertyChange>? InviterChange { get; internal set; } /// /// Gets the description of invite's target channel change. /// - public PropertyChange ChannelChange { get; internal set; } + public PropertyChange>? ChannelChange { get; internal set; } /// /// Gets the description of invite's use count change. /// - public PropertyChange UsesChange { get; internal set; } + public PropertyChange? UsesChange { get; internal set; } /// /// Gets the description of invite's max use count change. /// - public PropertyChange MaxUsesChange { get; internal set; } + public PropertyChange? MaxUsesChange { get; internal set; } internal DiscordAuditLogInviteEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs index 3912f9e8dd..7572fd496c 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,7 +7,7 @@ public sealed class DiscordAuditLogKickEntry : DiscordAuditLogEntry /// /// Gets the kicked member. /// - public DiscordMember Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } internal DiscordAuditLogKickEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs index 2e065f7b43..c5cc64d119 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogMemberDisconnectEntry : DiscordAuditLogEntry diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs index eddecff642..87bb09a8ab 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,7 +7,7 @@ public sealed class DiscordAuditLogMemberMoveEntry : DiscordAuditLogEntry /// /// Gets the channel the members were moved in. /// - public DiscordChannel Channel { get; internal set; } = default!; + public CachedEntity Channel { get; internal set; } /// /// Gets the amount of users that were moved out from the voice channel. diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs index e7bec179ed..99eaa40f18 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs @@ -1,66 +1,45 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - using System; using System.Collections.Generic; +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; + public sealed class DiscordAuditLogMemberUpdateEntry : DiscordAuditLogEntry { /// /// Gets the affected member. /// - public DiscordMember Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of member's nickname change. /// - public PropertyChange NicknameChange { get; internal set; } + public PropertyChange? NicknameChange { get; internal set; } /// /// Gets the roles that were removed from the member. /// - public IReadOnlyList? RemovedRoles { get; internal set; } + public IReadOnlyList>? RemovedRoles { get; internal set; } /// /// Gets the roles that were added to the member. /// - public IReadOnlyList? AddedRoles { get; internal set; } + public IReadOnlyList>? AddedRoles { get; internal set; } /// /// Gets the description of member's mute status change. /// - public PropertyChange MuteChange { get; internal set; } + public PropertyChange? MuteChange { get; internal set; } /// /// Gets the description of member's deaf status change. /// - public PropertyChange DeafenChange { get; internal set; } + public PropertyChange? DeafenChange { get; internal set; } /// /// Gets the change in a user's timeout status /// - public PropertyChange TimeoutChange { get; internal set; } + public PropertyChange? TimeoutChange { get; internal set; } internal DiscordAuditLogMemberUpdateEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs index fec0b95acb..b333c72e1f 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs @@ -1,44 +1,23 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogMessageEntry : DiscordAuditLogEntry { /// - /// Gets the affected User. + /// Gets the affected User. This can be null if this entry was created by bulk deleting messages. /// - public DiscordUser Target { get; internal set; } = default!; + public CachedEntity? Target { get; internal set; } /// - /// Gets the affected Member. This is null if the action was performed on a user that is not in the member cache. + /// Gets the affected Member. /// - public DiscordMember? Member => Channel.Guild.Members.TryGetValue(Target.Id, out DiscordMember? member) ? member : null; + public CachedEntity Member; /// /// Gets the channel in which the action occurred. /// - public DiscordChannel Channel { get; internal set; } = default!; + public CachedEntity Channel { get; internal set; } /// /// Gets the number of messages that were affected. diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs index 309a0186d3..fbf27858c8 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,17 +7,17 @@ public sealed class DiscordAuditLogMessagePinEntry : DiscordAuditLogEntry /// /// Gets the affected message's user. /// - public DiscordUser Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the channel the message is in. /// - public DiscordChannel Channel { get; internal set; } = default!; + public CachedEntity Channel { get; internal set; } /// /// Gets the message the pin action was for. /// - public DiscordMessage Message { get; internal set; } = default!; + public CachedEntity Message { get; internal set; } internal DiscordAuditLogMessagePinEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs index 87e7f7b318..9a745d85cc 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs @@ -1,59 +1,38 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogOverwriteEntry : DiscordAuditLogEntry { /// - /// Gets the affected overwrite. Null if the overwrite was deleted or not in cache. + /// Gets the affected overwrite. /// - public DiscordOverwrite? Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the channel for which the overwrite was changed. /// - public DiscordChannel Channel { get; internal set; } = default!; + public CachedEntity Channel { get; internal set; } /// /// Gets the description of overwrite's allow value change. /// - public PropertyChange AllowedPermissions { get; internal set; } + public PropertyChange? AllowedPermissions { get; internal set; } /// /// Gets the description of overwrite's deny value change. /// - public PropertyChange DeniedPermissions { get; internal set; } + public PropertyChange? DeniedPermissions { get; internal set; } /// /// Gets the description of overwrite's type change. /// - public PropertyChange Type { get; internal set; } + public PropertyChange? Type { get; internal set; } /// /// Gets the description of overwrite's target id change. /// - public PropertyChange TargetIdChange { get; internal set; } + public PropertyChange? TargetIdChange { get; internal set; } internal DiscordAuditLogOverwriteEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs index feec344b42..f9c5ce4cd1 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogPruneEntry : DiscordAuditLogEntry diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs index 5e3b6650bf..38b4b4402d 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs @@ -1,3 +1,5 @@ +using DSharpPlus.Caching; + namespace DSharpPlus.Entities.AuditLogs; public sealed class DiscordAuditLogRoleUpdateEntry : DiscordAuditLogEntry @@ -5,37 +7,37 @@ public sealed class DiscordAuditLogRoleUpdateEntry : DiscordAuditLogEntry /// /// Gets the affected role. /// - public DiscordRole Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of role's name change. /// - public PropertyChange NameChange { get; internal set; } + public PropertyChange? NameChange { get; internal set; } /// /// Gets the description of role's color change. /// - public PropertyChange ColorChange { get; internal set; } + public PropertyChange? ColorChange { get; internal set; } /// /// Gets the description of role's permission set change. /// - public PropertyChange PermissionChange { get; internal set; } + public PropertyChange? PermissionChange { get; internal set; } /// /// Gets the description of the role's position change. /// - public PropertyChange PositionChange { get; internal set; } + public PropertyChange? PositionChange { get; internal set; } /// /// Gets the description of the role's mentionability change. /// - public PropertyChange MentionableChange { get; internal set; } + public PropertyChange? MentionableChange { get; internal set; } /// /// Gets the description of the role's hoist status change. /// - public PropertyChange HoistChange { get; internal set; } + public PropertyChange? HoistChange { get; internal set; } internal DiscordAuditLogRoleUpdateEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs index 20997453fa..4b7ef82515 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,52 +7,52 @@ public sealed class DiscordAuditLogStickerEntry : DiscordAuditLogEntry /// /// Gets the affected sticker. /// - public DiscordMessageSticker Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of sticker's name change. /// - public PropertyChange NameChange { get; internal set; } + public PropertyChange? NameChange { get; internal set; } /// /// Gets the description of sticker's description change. /// - public PropertyChange DescriptionChange { get; internal set; } + public PropertyChange? DescriptionChange { get; internal set; } /// /// Gets the description of sticker's tags change. /// - public PropertyChange TagsChange { get; internal set; } + public PropertyChange? TagsChange { get; internal set; } /// /// Gets the description of sticker's tags change. /// - public PropertyChange AssetChange { get; internal set; } + public PropertyChange? AssetChange { get; internal set; } /// /// Gets the description of sticker's guild id change. /// - public PropertyChange GuildIdChange { get; internal set; } + public PropertyChange? GuildIdChange { get; internal set; } /// /// Gets the description of sticker's availability change. /// - public PropertyChange AvailabilityChange { get; internal set; } + public PropertyChange? AvailabilityChange { get; internal set; } /// /// Gets the description of sticker's id change. /// - public PropertyChange IdChange { get; internal set; } + public PropertyChange? IdChange { get; internal set; } /// /// Gets the description of sticker's type change. /// - public PropertyChange TypeChange { get; internal set; } + public PropertyChange? TypeChange { get; internal set; } /// /// Gets the description of sticker's format change. /// - public PropertyChange FormatChange { get; internal set; } + public PropertyChange? FormatChange { get; internal set; } internal DiscordAuditLogStickerEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs index 9c5e797f68..0c0a8d025d 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,47 +7,47 @@ public sealed class DiscordAuditLogThreadEventEntry : DiscordAuditLogEntry /// /// Gets the target thread. /// - public DiscordThreadChannel Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets a change in the thread's name. /// - public PropertyChange Name { get; internal set; } + public PropertyChange? Name { get; internal set; } /// /// Gets a change in channel type. /// - public PropertyChange Type { get; internal set; } + public PropertyChange? Type { get; internal set; } /// /// Gets a change in the thread's archived status. /// - public PropertyChange Archived { get; internal set; } + public PropertyChange? Archived { get; internal set; } /// /// Gets a change in the thread's auto archive duration. /// - public PropertyChange AutoArchiveDuration { get; internal set; } + public PropertyChange? AutoArchiveDuration { get; internal set; } /// /// Gets a change in the threads invitibility /// - public PropertyChange Invitable { get; internal set; } + public PropertyChange? Invitable { get; internal set; } /// /// Gets a change in the thread's locked status /// - public PropertyChange Locked { get; internal set; } + public PropertyChange? Locked { get; internal set; } /// /// Gets a change in the thread's slowmode setting /// - public PropertyChange PerUserRateLimit { get; internal set; } + public PropertyChange? PerUserRateLimit { get; internal set; } /// /// Gets a change in channel flags /// - public PropertyChange Flags { get; internal set; } + public PropertyChange? Flags { get; internal set; } internal DiscordAuditLogThreadEventEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs index b281608cb9..bee9e384b7 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs @@ -1,25 +1,4 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. +using DSharpPlus.Caching; namespace DSharpPlus.Entities.AuditLogs; @@ -28,32 +7,32 @@ public sealed class DiscordAuditLogWebhookEntry : DiscordAuditLogEntry /// /// Gets the affected webhook. /// - public DiscordWebhook Target { get; internal set; } = default!; + public CachedEntity Target { get; internal set; } /// /// Gets the description of webhook's name change. /// - public PropertyChange NameChange { get; internal set; } + public PropertyChange? NameChange { get; internal set; } /// /// Gets the description of webhook's target channel change. /// - public PropertyChange ChannelChange { get; internal set; } + public PropertyChange>? ChannelChange { get; internal set; } /// /// Gets the description of webhook's type change. /// - public PropertyChange TypeChange { get; internal set; } + public PropertyChange? TypeChange { get; internal set; } /// /// Gets the description of webhook's avatar change. /// - public PropertyChange AvatarHashChange { get; internal set; } + public PropertyChange? AvatarHashChange { get; internal set; } /// /// Gets the change in application ID. /// - public PropertyChange ApplicationIdChange { get; internal set; } + public PropertyChange? ApplicationIdChange { get; internal set; } internal DiscordAuditLogWebhookEntry() { } } diff --git a/DSharpPlus/Entities/AuditLogs/PropertyChange.cs b/DSharpPlus/Entities/AuditLogs/PropertyChange.cs index 763b7ddf29..5e3d7cd8f3 100644 --- a/DSharpPlus/Entities/AuditLogs/PropertyChange.cs +++ b/DSharpPlus/Entities/AuditLogs/PropertyChange.cs @@ -1,26 +1,3 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2024 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - using DSharpPlus.Net.Abstractions; namespace DSharpPlus.Entities.AuditLogs; @@ -34,26 +11,26 @@ public readonly record struct PropertyChange /// /// The property's value before it was changed. /// - public T? Before { get; internal init; } + public Optional Before { get; internal init; } /// /// The property's value after it was changed. /// - public T? After { get; internal init; } + public Optional After { get; internal init; } /// /// Create a new from the given before and after values. /// - public static PropertyChange From(object? before, object? after) => + public static PropertyChange From(T before, T after) => new() { - Before = before is not null and T ? (T)before : default, - After = after is not null and T ? (T)after : default + Before = before is T? (T)before : default(Optional), + After = after is T? (T)after : default(Optional) }; /// /// Create a new from the given change using before and after values. /// internal static PropertyChange From(AuditLogActionChange change) => - From(change.OldValue, change.NewValue); + From((T?) change.OldValue, (T?) change.NewValue); } diff --git a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs b/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs index 770b767745..f88b4fd7e0 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using DSharpPlus.Caching; using DSharpPlus.Net.Models; - using Newtonsoft.Json; namespace DSharpPlus.Entities; @@ -16,10 +16,33 @@ public class DiscordAutoModerationRule : SnowflakeObject internal ulong GuildId { get; set; } /// - /// Gets the guild which the rule is in. + /// Gets the guild which the rule belongs to. /// - [JsonIgnore] - public DiscordGuild? Guild => this.Discord.Guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; + /// Whether to include approximate presence and member counts in the returned guild. + /// Whether to skip the cache and always excute a REST request + /// Thrown when Discord is unable to process the request. + /// Thrown when the guild is not found. + public async ValueTask GetGuildAsync(bool skipCache = false, bool withCounts = false) + { + if (Discord is DiscordClient dc) + { + return await dc.GetGuildAsync(this.GuildId, skipCache, withCounts); + } + + if (skipCache) + { + return await this.Discord.ApiClient.GetGuildAsync(this.GuildId, withCounts); + } + + DiscordGuild? guild = await this.Discord.Cache.TryGet(ICacheKey.ForGuild(this.GuildId)); + if (guild is not null) + { + return guild; + } + + return await this.Discord.ApiClient.GetGuildAsync(this.GuildId, withCounts); + } + /// /// Gets the rule name. @@ -33,8 +56,30 @@ public class DiscordAutoModerationRule : SnowflakeObject /// /// Gets the user that created the rule. /// - [JsonIgnore] - public DiscordUser? Creator => this.Discord.TryGetCachedUserInternal(this.CreatorId, out DiscordUser creator) ? creator : null; + /// Whether to skip the cache and always excute a REST request + /// Thrown when Discord is unable to process the request. + /// Thrown when the user is not found. + public async ValueTask GetCreatorAsync(bool skipCache = false) + { + if (Discord is DiscordClient dc) + { + return await dc.GetUserAsync(this.CreatorId, skipCache); + } + + if (skipCache) + { + return await this.Discord.ApiClient.GetUserAsync(this.CreatorId); + } + + + DiscordUser? user = await this.Discord.Cache.TryGetUserAsync(this.CreatorId); + if (user is not null) + { + return user; + } + + return await this.Discord.ApiClient.GetUserAsync(this.CreatorId); + } /// /// Gets the rule event type. diff --git a/DSharpPlus/Entities/Channel/DiscordChannel.cs b/DSharpPlus/Entities/Channel/DiscordChannel.cs index 3c9775e8eb..1bcf7388dd 100644 --- a/DSharpPlus/Entities/Channel/DiscordChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordChannel.cs @@ -12,6 +12,8 @@ namespace DSharpPlus.Entities; +using Caching; + /// /// Represents a discord channel. /// @@ -33,9 +35,19 @@ public class DiscordChannel : SnowflakeObject, IEquatable /// /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. /// - [JsonIgnore] - public DiscordChannel Parent - => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; + public async Task GetParentAsync() + { + if (!this.ParentId.HasValue) + { + return null; + } + + DiscordGuild? guild = await this.GetGuildAsync(); + + return guild is not null + ? await guild.GetChannelAsync(this.ParentId.Value) + : null; + } /// /// Gets the name of this channel. @@ -79,9 +91,21 @@ public bool IsThread /// /// Gets the guild to which this channel belongs. /// - [JsonIgnore] - public DiscordGuild Guild - => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out DiscordGuild? guild) ? guild : null; + public async Task GetGuildAsync(bool skipCache = false) + { + if (!this.GuildId.HasValue) + { + return null; + } + + if (skipCache) + { + return await this.Discord.ApiClient.GetGuildAsync(GuildId.Value, null); + } + + DiscordGuild? cachedGuild = await this.Discord.Cache.TryGetGuildAsync(this.GuildId.GetValueOrDefault()); + return cachedGuild ?? await this.Discord.ApiClient.GetGuildAsync(this.GuildId.Value, null); + } /// /// Gets a collection of permission overwrites for this channel. @@ -147,47 +171,77 @@ public IReadOnlyList PermissionOverwrites public string Mention => Formatter.Mention(this); + /// - /// Gets this channel's children. This applies only to channel categories. + /// Gets the guild-specific default message notifications for this channel. /// - [JsonIgnore] - public IReadOnlyList Children + /// + /// This function is only valid for guild categories. + /// + public async Task> GetChildrenAsync() { - get + if (this.Type != ChannelType.Category) { - return !this.IsCategory - ? throw new ArgumentException("Only channel categories contain children.") - : this.Guild._channels.Values.Where(e => e.ParentId == this.Id).ToList(); + throw new ArgumentException("Only channel categories can have children."); } + + DiscordGuild? guild = await this.GetGuildAsync(); + + return guild is not null + ? guild._channels.Values.Where(xc => xc.ParentId == this.Id) + : []; } /// /// Gets this channel's threads. This applies only to text and news channels. /// - [JsonIgnore] - public IReadOnlyList Threads + public async Task> GetThreadsAsync() { - get + if (this.Type is not (ChannelType.Text or ChannelType.News or ChannelType.GuildForum)) { - return this.Type is not (ChannelType.Text or ChannelType.News or ChannelType.GuildForum) - ? throw new ArgumentException("Only text channels can have threads.") - : this.Guild._threads.Values.Where(e => e.ParentId == this.Id).ToArray(); + throw new ArgumentException("Only text channels can have threads."); } + + DiscordGuild? guild = await this.GetGuildAsync(); + + return guild is not null + ? guild._threads.Values.Where(e => e.ParentId == this.Id).ToArray() + : []; + } /// /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). /// - [JsonIgnore] - public virtual IReadOnlyList Users + public async IAsyncEnumerable GetUsersAsync() { - get + if (this.GuildId is null) + { + throw new InvalidOperationException("Cannot query users outside of guild channels."); + } + + DiscordGuild? guild = await this.GetGuildAsync(); + + if (this.Type is not (ChannelType.Voice or ChannelType.Stage)) + { + + foreach (DiscordMember member in guild!.Members.Values) + { + if ((await this.PermissionsForMemberAsync(member) & Permissions.AccessChannels) == Permissions.AccessChannels) + { + yield return member; + } + } + } + else { - return this.Guild == null - ? throw new InvalidOperationException("Cannot query users outside of guild channels.") - : (IReadOnlyList)(this.Type == ChannelType.Voice || this.Type == ChannelType.Stage - ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() - : this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList()); + foreach (DiscordMember member in guild!.Members.Values) + { + if (member.VoiceState?.ChannelId == this.Id) + { + yield return member; + } + } } } @@ -315,10 +369,34 @@ public async Task SendMessageAsync(Action /// When this event ends. External events require an end time. /// The created event. /// - public Task CreateGuildEventAsync(string name, string description, ScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end) - => this.Type is not (ChannelType.Voice or ChannelType.Stage) ? throw new InvalidOperationException("Events can only be created on voice an stage chnanels") : - this.Guild.CreateEventAsync(name, description, this.Id, this.Type is ChannelType.Stage ? ScheduledGuildEventType.StageInstance : ScheduledGuildEventType.VoiceChannel, privacyLevel, start, end); + public async Task CreateGuildEventAsync(string name, string description, ScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end) + { + if (this.GuildId is null) + { + throw new InvalidOperationException("Events can only be created on guild channels"); + } + if(this.Type is not (ChannelType.Voice or ChannelType.Stage)) + { + throw new InvalidOperationException("Events can only be created on voice an stage chnanels"); + } + + ScheduledGuildEventType type = this.Type is ChannelType.Stage + ? ScheduledGuildEventType.StageInstance + : ScheduledGuildEventType.VoiceChannel; + + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(start, DateTimeOffset.Now); + + if (end != null && end <= start) + { + throw new ArgumentOutOfRangeException("The end time for an event must be after the start time."); + } + + DiscordScheduledGuildEventMetadata? metadata = null; + + return await this.Discord.ApiClient.CreateScheduledGuildEventAsync(this.GuildId.Value, name, description, start, type, privacyLevel, metadata, end, this.Id); + + } // Please send memes to Naamloos#2887 at discord <3 thank you /// @@ -342,13 +420,23 @@ public async Task DeleteAsync(string reason = null) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task CloneAsync(string reason = null) + public async Task CloneAsync(string? reason = null) { - if (this.Guild == null) + if (this.GuildId is null) { throw new InvalidOperationException("Non-guild channels cannot be cloned."); } + if (this.Type is not (ChannelType.Text or ChannelType.Voice or ChannelType.Category or ChannelType.News or ChannelType.Stage or ChannelType.GuildForum)) + { + throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(this.Type)); + } + + if (this.Type == ChannelType.Category && this.ParentId.HasValue) + { + throw new ArgumentException("Cannot specify parent of a channel category.", nameof(ParentId)); + } + List ovrs = new(); foreach (DiscordOverwrite ovr in this._permissionOverwrites) { @@ -370,7 +458,27 @@ public async Task CloneAsync(string reason = null) perUserRateLimit = Optional.FromNoValue(); } - return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNSFW, perUserRateLimit, this.QualityMode, this.Position, reason); + // technically you can create news/store channels but not always + return await this.Discord.ApiClient.CreateGuildChannelAsync + ( + this.GuildId.Value, + this.Name, + this.Type, + this.ParentId, + this.Topic, + bitrate, + userLimit, + ovrs, + this.IsNSFW, + perUserRateLimit, + this.QualityMode, + this.Position, + reason, + null, + null, + null, + null + ); } /// @@ -383,16 +491,16 @@ public async Task CloneAsync(string reason = null) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - /// Cached message objects will not be returned if is set to zero, if the client does not have the or intents, or if the discord client is a . public async Task GetMessageAsync(ulong id, bool skipCache = false) { - return !skipCache - && this.Discord.Configuration.MessageCacheSize > 0 - && this.Discord is DiscordClient dc - && dc.MessageCache != null - && dc.MessageCache.TryGet(id, out DiscordMessage? msg) - ? msg - : await this.Discord.ApiClient.GetMessageAsync(this.Id, id); + if (skipCache) + { + return await this.Discord.ApiClient.GetMessageAsync(this.Id, id); + } + + DiscordMessage? msg = await this.Discord.Cache.TryGetMessageAsync(id); + msg ??= await this.Discord.ApiClient.GetMessageAsync(this.Id, id); + return msg; } /// @@ -446,14 +554,11 @@ await this.Discord.ApiClient.ModifyChannelAsync /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task ModifyPositionAsync(int position, string reason = null, bool? lockPermissions = null, ulong? parentId = null) + public async Task ModifyPositionAsync(int position, string? reason = null, bool? lockPermissions = null, ulong? parentId = null) { - if (this.Guild is null) - { - throw new InvalidOperationException("Cannot modify order of non-guild channels."); - } + DiscordGuild? guild = await this.GetGuildAsync() ?? throw new InvalidOperationException("Cannot modify position of a channel that is not in a guild."); - DiscordChannel[] chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); + DiscordChannel[] chns = guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); RestGuildChannelReorderPayload[] pmds = new RestGuildChannelReorderPayload[chns.Length]; for (int i = 0; i < chns.Length; i++) { @@ -466,7 +571,7 @@ public async Task ModifyPositionAsync(int position, string reason = null, bool? }; } - await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); + await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(guild.Id, pmds, reason); } /// @@ -479,7 +584,7 @@ public async Task ModifyPositionAsync(int position, string reason = null, bool? /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public IAsyncEnumerable GetMessagesBeforeAsync(ulong before, int limit = 100) - => this.GetMessagesInternalAsync(limit, before, null, null); + => this.GetMessagesInternalAsync(limit, before); /// /// Returns a list of messages after a certain message. @@ -491,7 +596,7 @@ public IAsyncEnumerable GetMessagesBeforeAsync(ulong before, int /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public IAsyncEnumerable GetMessagesAfterAsync(ulong after, int limit = 100) - => this.GetMessagesInternalAsync(limit, null, after, null); + => this.GetMessagesInternalAsync(limit, null, after); /// /// Returns a list of messages around a certain message. @@ -514,7 +619,7 @@ public IAsyncEnumerable GetMessagesAroundAsync(ulong around, int /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public IAsyncEnumerable GetMessagesAsync(int limit = 100) => - this.GetMessagesInternalAsync(limit, null, null, null); + this.GetMessagesInternalAsync(limit); private async IAsyncEnumerable GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null) { @@ -538,8 +643,7 @@ private async IAsyncEnumerable GetMessagesInternalAsync(int limi { throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); } - - List msgs = new(limit); + int remaining = limit; ulong? last = null; bool isbefore = before != null; @@ -639,7 +743,7 @@ public async Task DeleteMessagesAsync(IReadOnlyList message { throw new ArgumentException("You need to specify at least one message to delete."); } - else if (count == 1) + if (count == 1) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, messages[0].Id, reason); return 1; @@ -659,7 +763,7 @@ public async Task DeleteMessagesAsync(IReadOnlyList message if (message.ChannelId != this.Id) { throw new ArgumentException( - $"You cannot delete messages from channel {message.Channel.Name} through channel {this.Name}!"); + $"You cannot delete messages from channel {message.Channel.Key} through channel {this.Id}!"); } else if (message.Timestamp < DateTimeOffset.UtcNow.AddDays(-14)) { @@ -748,7 +852,7 @@ public async Task DeleteMessageAsync(DiscordMessage message, string reason = nul /// Thrown when Discord is unable to process the request. public async Task> GetInvitesAsync() { - return this.Guild == null + return !this.GuildId.HasValue ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") : await this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); } @@ -756,8 +860,8 @@ public async Task> GetInvitesAsync() /// /// Create a new invite object /// - /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. - /// Max number of uses or 0 for unlimited. Defaults to 0 + /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. + /// Max number of uses or 0 for unlimited. Defaults to 0 /// Whether this invite only grants temporary membership. Defaults to false. /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites) /// Reason for audit logs. @@ -769,8 +873,8 @@ public async Task> GetInvitesAsync() /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, string reason = null, InviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) - => await this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); + public async Task CreateInviteAsync(int maxAge = 86400, int maxUses = 0, bool temporary = false, bool unique = false, string reason = null, InviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) + => await this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); /// /// Adds a channel permission overwrite for specified member. @@ -915,8 +1019,8 @@ public async Task PlaceMemberAsync(DiscordMember member) throw new ArgumentException("Cannot place a member in a non-voice channel!"); // be a little more angry, let em learn!!1 } - await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default, - default, this.Id, default, null); + await this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId!.Value, member.Id, default, default, default, + default, this.Id); } /// @@ -1017,7 +1121,7 @@ public async Task DeleteStageInstanceAsync(string reason = null) /// /// Member to calculate permissions for. /// Calculated permissions for a given member. - public Permissions PermissionsFor(DiscordMember mbr) + public async Task PermissionsForMemberAsync(DiscordMember mbr) { // future note: might be able to simplify @everyone role checks to just check any role ... but I'm not sure // xoxo, ~uwx @@ -1037,15 +1141,23 @@ public Permissions PermissionsFor(DiscordMember mbr) // If this is a thread, calculate on the parent; doing this on a thread does not work. // if (this.IsThread) { - return this.Parent.PermissionsFor(mbr); + DiscordChannel? parent = await this.GetParentAsync(); + return await parent!.PermissionsForMemberAsync(mbr); } - if (this.IsPrivate || this.Guild == null) + if (this.IsPrivate || this.GuildId is null) { return Permissions.None; } - if (this.Guild.OwnerId == mbr.Id) + DiscordGuild? guild = await this.GetGuildAsync(); + + if (guild is null) + { + return Permissions.None; + } + + if (guild.OwnerId == mbr.Id) { return PermissionMethods.FULL_PERMS; } @@ -1053,7 +1165,7 @@ public Permissions PermissionsFor(DiscordMember mbr) Permissions perms; // assign @everyone permissions - DiscordRole everyoneRole = this.Guild.EveryoneRole; + DiscordRole everyoneRole = guild.EveryoneRole; perms = everyoneRole.Permissions; // roles that member is in @@ -1106,19 +1218,27 @@ public Permissions PermissionsFor(DiscordMember mbr) /// /// Role to calculate permissions for. /// Calculated permissions for a given role. - public Permissions PermissionsFor(DiscordRole role) + public async Task PermissionsForRoleAsync(DiscordRole role) { if (this.IsThread) { - return this.Parent.PermissionsFor(role); + DiscordChannel? parent = await this.GetParentAsync(); + return await parent!.PermissionsForRoleAsync(role); } - - if (this.IsPrivate || this.Guild is null) + + if (this.IsPrivate || this.GuildId is null) { return Permissions.None; } - - if (role._guild_id != this.Guild.Id) + + DiscordGuild? guild = await this.GetGuildAsync(); + + if (guild is null) + { + return Permissions.None; + } + + if (role._guild_id != guild.Id) { throw new ArgumentException("Given role does not belong to this channel's guild."); } @@ -1126,7 +1246,7 @@ public Permissions PermissionsFor(DiscordRole role) Permissions perms; // assign @everyone permissions - DiscordRole everyoneRole = this.Guild.EveryoneRole; + DiscordRole everyoneRole = guild.EveryoneRole; perms = everyoneRole.Permissions; // add role permissions @@ -1215,7 +1335,13 @@ public async Task CreateThreadAsync(DiscordMessage message } DiscordThreadChannel threadChannel = await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Id, message.Id, name, archiveAfter, reason); - this.Guild._threads.AddOrUpdate(threadChannel.Id, threadChannel, (key, old) => threadChannel); + + if (this.GuildId.HasValue) + { + DiscordGuild? guild = await this.Discord.Cache.TryGetGuildAsync(this.GuildId.Value); + guild?._threads.AddOrUpdate(threadChannel.Id, threadChannel, (key, old) => threadChannel); + } + return threadChannel; } @@ -1246,7 +1372,12 @@ public async Task CreateThreadAsync(string name, AutoArchi } DiscordThreadChannel threadChannel = await this.Discord.ApiClient.CreateThreadAsync(this.Id, name, archiveAfter, threadType, reason); - this.Guild._threads.AddOrUpdate(threadChannel.Id, threadChannel, (key, old) => threadChannel); + if (this.GuildId.HasValue) + { + DiscordGuild? guild = await this.Discord.Cache.TryGetGuildAsync(this.GuildId.Value); + guild?._threads.AddOrUpdate(threadChannel.Id, threadChannel, (key, old) => threadChannel); + } + return threadChannel; } diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs index 0137e73ffc..8a59ab535a 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; - +using DSharpPlus.Caching; using Newtonsoft.Json; namespace DSharpPlus.Entities; @@ -15,40 +15,34 @@ namespace DSharpPlus.Entities; public class DiscordMessage : SnowflakeObject, IEquatable { internal DiscordMessage() - { - string gid = this.Channel is DiscordDmChannel ? "@me" : this.Channel?.GuildId?.ToString(CultureInfo.InvariantCulture) ?? "@me"; - string cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); - string mid = this.Id.ToString(CultureInfo.InvariantCulture); - - this.JumpLink = new Uri($"https://discord.com/channels/{gid}/{cid}/{mid}"); - } + { } internal DiscordMessage(DiscordMessage other) : this() { this.Discord = other.Discord; - this._attachments = new List(other._attachments); - this._embeds = new List(other._embeds); + this._attachments = [..other._attachments]; + this._embeds = [..other._embeds]; if (other._mentionedChannels is not null) { - this._mentionedChannels = new List(other._mentionedChannels); + this._mentionedChannels = [..other._mentionedChannels]; } if (other._mentionedRoles is not null) { - this._mentionedRoles = new List(other._mentionedRoles); + this._mentionedRoles = [..other._mentionedRoles]; } if (other._mentionedRoleIds is not null) { - this._mentionedRoleIds = new List(other._mentionedRoleIds); + this._mentionedRoleIds = [..other._mentionedRoleIds]; } - this._mentionedUsers = new List(other._mentionedUsers); - this._reactions = new List(other._reactions); - this._stickers = new List(other._stickers); + this._mentionedUsers = [..other._mentionedUsers]; + this._reactions = [..other._reactions]; + this._stickers = [..other._stickers]; this.Author = other.Author; this.ChannelId = other.ChannelId; @@ -67,14 +61,8 @@ internal DiscordMessage(DiscordMessage other) /// Gets the channel in which the message was sent. /// [JsonIgnore] - public DiscordChannel? Channel - { - get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._channel; - internal set => this._channel = value; - } - - private DiscordChannel? _channel; - + public CachedEntity Channel { get; internal set; } + /// /// Gets the ID of the channel in which the message was sent. /// @@ -149,7 +137,7 @@ public IReadOnlyList MentionedUsers /// [JsonIgnore] public IReadOnlyList MentionedRoles - => this._mentionedRoles; + => this._mentionedRoles ?? new List(); [JsonIgnore] internal List _mentionedRoles = []; @@ -197,14 +185,6 @@ public IReadOnlyList Reactions [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] internal List _reactions = []; - /* - /// - /// Gets the nonce sent with the message, if the message was sent by the client. - /// - [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] - public ulong? Nonce { get; internal set; } - */ - /// /// Gets whether the message is pinned. /// @@ -238,13 +218,6 @@ public IReadOnlyList Reactions [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] internal InternalDiscordMessageReference? _internalReference { get; set; } - /// - /// Gets the original message reference from the crossposted message. - /// - [JsonIgnore] - public DiscordMessageReference? Reference - => this._internalReference.HasValue ? this?.InternalBuildMessageReference() : null; - /// /// Gets the bitwise flags for this message. /// @@ -258,12 +231,6 @@ public DiscordMessageReference? Reference public bool? WebhookMessage => this.WebhookId != null; - /// - /// Gets the jump link to this message. - /// - [JsonIgnore] - public Uri JumpLink { get; internal set; } - /// /// Gets stickers for this message. /// @@ -295,68 +262,6 @@ public IReadOnlyList? Stickers [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? ApplicationId { get; internal set; } - internal DiscordMessageReference InternalBuildMessageReference() - { - DiscordClient client = (DiscordClient)this.Discord; - ulong? guildId = this._internalReference?.GuildId; - ulong? channelId = this._internalReference?.ChannelId; - ulong? messageId = this._internalReference?.MessageId; - - DiscordMessageReference reference = new(); - - if (guildId.HasValue) - { - reference.Guild = client!._guilds.TryGetValue(guildId.Value, out DiscordGuild? g) - ? g - : new DiscordGuild - { - Id = guildId.Value, - Discord = client - }; - } - - DiscordChannel channel = client.InternalGetCachedChannel(channelId!.Value); - - if (channel is null) - { - reference.Channel = new DiscordChannel - { - Id = channelId.Value, - Discord = client - }; - - if (guildId.HasValue) - { - reference.Channel.GuildId = guildId.Value; - } - } - - else - { - reference.Channel = channel; - } - - if (client.MessageCache != null && client.MessageCache.TryGet(messageId!.Value, out DiscordMessage? msg)) - { - reference.Message = msg; - } - else - { - reference.Message = new DiscordMessage - { - ChannelId = this.ChannelId, - Discord = client - }; - - if (messageId.HasValue) - { - reference.Message.Id = messageId.Value; - } - } - - return reference; - } - private IMention[] GetMentions() { List mentions = []; @@ -379,9 +284,14 @@ private IMention[] GetMentions() return [.. mentions]; } - internal void PopulateMentions() + internal async Task PopulateMentionsAsync() { - DiscordGuild? guild = this.Channel?.Guild; + this.Channel.TryGetCachedValue(out DiscordChannel? channel); + DiscordGuild? guild = null; + if (channel is not null) + { + guild = channel.GuildId.HasValue ? await this.Discord.Cache.TryGetGuildAsync(channel.GuildId.Value) : null; + } this._mentionedUsers ??= []; this._mentionedRoles ??= []; this._mentionedChannels ??= []; @@ -393,12 +303,17 @@ internal void PopulateMentions() { // Assign the Discord instance and update the user cache. usr.Discord = this.Discord; - this.Discord.UpdateUserCache(usr); - - if (guild is not null && usr is not DiscordMember && guild._members.TryGetValue(usr.Id, out DiscordMember? cachedMember)) + await this.Discord.Cache.AddUserAsync(usr); + + if (guild is not null && usr is not DiscordMember ) { + DiscordMember? member = await this.Discord.Cache.TryGetMemberAsync(usr.Id, guild.Id); + if(member is null && guild._members.TryGetValue(usr.Id, out DiscordMember? cachedMember)) + { + member = cachedMember; + } // If the message is from a guild, but a discord member isn't provided, try to get the discord member out of guild members cache. - mentionedUsers.Add(cachedMember); + mentionedUsers.Add(member); } else { @@ -412,14 +327,74 @@ internal void PopulateMentions() if (guild is not null && !string.IsNullOrWhiteSpace(this.Content)) { - this._mentionedChannels = this._mentionedChannels.Union(Utilities.GetChannelMentions(this).Select(guild.GetChannel)).ToList(); - this._mentionedRoles = this._mentionedRoles.Union(this._mentionedRoleIds.Select(guild.GetRole)).ToList(); + this._mentionedChannels = this._mentionedChannels.Union(Utilities.GetChannelMentions(this).Select(x => guild.Channels[x])).ToList(); + this._mentionedRoles = this._mentionedRoles.Union(this._mentionedRoleIds.Select(guild.GetRole)).Where(x => x is not null).ToList()!; //uncomment if this breaks //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); //this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); } } + + /// + /// Gets the jump link to this message. This is null when the channel is not cached. + /// + public async ValueTask TryGetJumpLinkAsync() + { + DiscordChannel? channel = await this.Discord.Cache.TryGetChannelAsync(this.ChannelId); + if (channel is null) + { + return null; + } + string gid = channel is DiscordDmChannel ? "@me" : channel.GuildId?.ToString(CultureInfo.InvariantCulture) ?? "@me"; + string cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); + string mid = this.Id.ToString(CultureInfo.InvariantCulture); + + return new Uri($"https://discord.com/channels/{gid}/{cid}/{mid}"); + } + + /// + /// Gets the referenced message. + /// + /// + /// Referenced message, or null if the message is not a reply. + /// + public async Task GetReferencedMessageAsync() + { + if (this._internalReference is null) + { + return null; + } + + ulong? guildId = this._internalReference?.GuildId; + ulong? channelId = this._internalReference?.ChannelId; + ulong? messageId = this._internalReference?.MessageId; + + DiscordMessageReference reference = new DiscordMessageReference(); + + if (guildId.HasValue) + { + DiscordGuild? cachedGuild = await this.Discord.Cache.TryGetGuildAsync(guildId.Value); + CachedEntity guild = new(guildId.Value, cachedGuild); + reference.Guild = guild; + } + + if (channelId.HasValue) + { + DiscordChannel? channel = await this.Discord.Cache.TryGetChannelAsync(channelId.Value); + CachedEntity cachedChannel = new(channelId.Value, channel); + reference.Channel = cachedChannel; + } + + if (messageId.HasValue) + { + DiscordMessage? message = await this.Discord.Cache.TryGetMessageAsync(messageId.Value); + CachedEntity cachedMessage = new(messageId.Value, message); + reference.Message = cachedMessage; + } + + return reference; + } /// /// Edits the message. @@ -630,9 +605,15 @@ public async Task RespondAsync(Action act /// Thrown when Discord is unable to process the request. public async Task CreateThreadAsync(string name, AutoArchiveDuration archiveAfter, string? reason = null) { - return this.Channel?.Type is not ChannelType.Text and not ChannelType.News - ? throw new InvalidOperationException("Threads can only be created within text or news channels.") - : await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Channel.Id, this.Id, name, archiveAfter, reason); + if (this.Channel.TryGetCachedValue(out DiscordChannel? channel)) + { + if (channel.Type != ChannelType.Text && channel.Type != ChannelType.News) + { + throw new InvalidOperationException("Threads can only be created within text or news channels."); + } + } + + return await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Channel.Key, this.Id, name, archiveAfter, reason); } /// @@ -672,6 +653,7 @@ public async Task DeleteOwnReactionAsync(DiscordEmoji emoji) public async Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string? reason = null) => await this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); + //TODO: Make it a IAsyncEnumerable and "inline" this method /// /// Gets users that reacted with this emoji. /// @@ -728,7 +710,7 @@ private async Task> GetReactionsInternalAsync(Discord do { int fetchSize = remaining > 100 ? 100 : remaining; - IReadOnlyList fetch = await this.Discord.ApiClient.GetReactionsAsync(this.ChannelId, this.Id, emoji.ToReactionString(), last, fetchSize); + IReadOnlyList fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Key, this.Id, emoji.ToReactionString(), last, fetchSize); remaining -= fetch.Count; diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs index 1205ef29a7..c4fe8bcc16 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using Newtonsoft.Json; namespace DSharpPlus.Entities; @@ -10,20 +11,20 @@ public class DiscordMessageReference /// /// Gets the original message. /// - public DiscordMessage Message { get; internal set; } = default!; + public CachedEntity Message { get; internal set; } /// /// Gets the channel of the original message. /// - public DiscordChannel Channel { get; internal set; } = default!; + public CachedEntity Channel { get; internal set; } /// /// Gets the guild of the original message. /// - public DiscordGuild? Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } public override string ToString() - => $"Guild: {this.Guild?.Id ?? 0}, Channel: {this.Channel.Id}, Message: {this.Message.Id}"; + => $"Guild: {this.Guild.Key}, Channel: {this.Channel.Key}, Message: {this.Message.Key}"; internal DiscordMessageReference() { } } diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs index 7f2b6cfe1c..707098e5b1 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs @@ -4,6 +4,8 @@ namespace DSharpPlus.Entities; +using System.Threading.Tasks; + /// /// Represents a Discord Sticker. /// @@ -42,7 +44,15 @@ public class DiscordMessageSticker : SnowflakeObject, IEquatable /// Gets the guild associated with this sticker, if any. /// - public DiscordGuild Guild => (this.Discord as DiscordClient)!.InternalGetCachedGuild(this.GuildId); + public async ValueTask GetGuildAsync(bool withCounts = false, bool skipCache = false) + { + if (this.GuildId is null) + { + return null; + } + + return await this.Discord.GetGuildAsync(this.GuildId!.Value, withCounts, skipCache); + } public string StickerUrl => $"https://cdn.discordapp.com/stickers/{this.Id}{this.GetFileTypeExtension()}"; diff --git a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs b/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs index 6c7700bb67..0bfba2b0c9 100644 --- a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs +++ b/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs @@ -15,7 +15,7 @@ public sealed class DiscordStageInstance : SnowflakeObject /// [JsonIgnore] public DiscordGuild Guild - => this.Discord.Guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; + => this.Discord._guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; /// /// Gets the id of the guild this stage instance is in. diff --git a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs index b864413b52..74b0dd6665 100644 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs +++ b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs @@ -45,7 +45,7 @@ public DiscordChannel Thread /// [JsonIgnore] public DiscordGuild Guild - => this.Discord.Guilds.TryGetValue(this._guild_id, out DiscordGuild? guild) ? guild : null; + => this.Discord._guildIds.TryGetValue(this._guild_id, out DiscordGuild? guild) ? guild : null; [JsonIgnore] internal ulong _guild_id; diff --git a/DSharpPlus/Entities/Emoji/DiscordEmoji.cs b/DSharpPlus/Entities/Emoji/DiscordEmoji.cs index 833437a9c8..a269357ff5 100644 --- a/DSharpPlus/Entities/Emoji/DiscordEmoji.cs +++ b/DSharpPlus/Entities/Emoji/DiscordEmoji.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Globalization; using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Caching; using Newtonsoft.Json; namespace DSharpPlus.Entities; @@ -22,11 +23,10 @@ public partial class DiscordEmoji : SnowflakeObject, IEquatable /// Gets IDs the roles this emoji is enabled for. /// [JsonIgnore] - public IReadOnlyList Roles => this._rolesLazy.Value; + public IReadOnlyList Roles => this._roles; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] internal List _roles; - private readonly Lazy> _rolesLazy; /// /// Gets whether this emoji requires colons to use. @@ -69,7 +69,7 @@ public string Url [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] public bool IsAvailable { get; internal set; } - internal DiscordEmoji() => this._rolesLazy = new Lazy>(() => new ReadOnlyCollection(this._roles)); + internal DiscordEmoji() { } /// /// Gets emoji's name in non-Unicode format (eg. :thinking: instead of the Unicode representation of the emoji). @@ -221,54 +221,26 @@ public static bool TryFromUnicode(string unicodeEntity, out DiscordEmoji emoji) => TryFromUnicode(null, unicodeEntity, out emoji); /// - /// Creates an emoji object from a guild emote. + /// Tries to get an emoji from a guild by its ID. /// /// to attach to the object. /// Id of the emote. /// Create object. - public static DiscordEmoji FromGuildEmote(BaseDiscordClient client, ulong id) + public static async ValueTask TryFromGuildEmoteAsync(BaseDiscordClient client, ulong id) { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - - foreach (DiscordGuild guild in client.Guilds.Values) - { - if (guild.Emojis.TryGetValue(id, out DiscordEmoji? found)) - { - return found; - } - } - - throw new KeyNotFoundException("Given emote was not found."); - } - - /// - /// Attempts to create an emoji object from a guild emote. - /// - /// to attach to the object. - /// Id of the emote. - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out DiscordEmoji emoji) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } + ArgumentNullException.ThrowIfNull(client, nameof(client)); - foreach (DiscordGuild guild in client.Guilds.Values) + foreach (ulong guildId in client._guildIds) { - if (guild.Emojis.TryGetValue(id, out emoji)) + DiscordGuild? guild = await client.Cache.TryGetGuildAsync(guildId); + if (guild is not null && guild.Emojis.TryGetValue(id, out DiscordEmoji? emoji)) { - return true; + return emoji; } } - emoji = null; - return false; - } + return null; + } //TODO: This is a behavior change, anounce in the PR /// /// Creates an emoji object from emote name that includes colons (eg. :thinking:). This method also supports @@ -279,17 +251,12 @@ public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out Dis /// Name of the emote to find, including colons (eg. :thinking:). /// Should guild emojis be included in the search. /// Create object. - public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true) + public static async ValueTask FromNameAsync(BaseDiscordClient client, string name, bool includeGuilds = true) { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - else if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); - } - else if (name.Length < 2 || name[0] != ':' || name[name.Length - 1] != ':') + ArgumentNullException.ThrowIfNull(client, nameof(client)); + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + + if (name.Length < 2 || name[0] != ':' || name[name.Length - 1] != ':') { throw new ArgumentException("Invalid emoji name specified. Ensure the emoji name starts and ends with ':'", nameof(name)); } @@ -301,12 +268,13 @@ public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool else if (includeGuilds) { name = name.Substring(1, name.Length - 2); // remove colons - foreach (DiscordGuild guild in client.Guilds.Values) + foreach (ulong guildId in client._guildIds) { - DiscordEmoji? found = guild.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); - if (found != null) + DiscordGuild? guild = await client.Cache.TryGetGuildAsync(guildId); + DiscordEmoji? emoji = guild?.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); + if (emoji is not null) { - return found; + return emoji; } } } @@ -314,18 +282,6 @@ public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool throw new ArgumentException("Invalid emoji name specified.", nameof(name)); } - /// - /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also - /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild - /// emoji (still specified by :name:). - /// - /// to attach to the object. - /// Name of the emote to find, including colons (eg. :thinking:). - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromName(BaseDiscordClient client, string name, out DiscordEmoji emoji) - => TryFromName(client, name, true, out emoji); - /// /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild @@ -336,38 +292,35 @@ public static bool TryFromName(BaseDiscordClient client, string name, out Discor /// Should guild emojis be included in the search. /// Resulting object. /// Whether the operation was successful. - public static bool TryFromName(BaseDiscordClient client, string name, bool includeGuilds, out DiscordEmoji emoji) + public static async ValueTask TryFromNameAsync(BaseDiscordClient client, string name, bool includeGuilds = true) { - if (client == null) + ArgumentNullException.ThrowIfNull(client, nameof(client)); + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + + // Checks if the emoji name is invalid + if (name.Length < 2 || name[0] != ':' || name[name.Length - 1] != ':') { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - // Checks if the emoji name is null - else if (string.IsNullOrWhiteSpace(name) || name.Length < 2 || name[0] != ':' || name[name.Length - 1] != ':') - { - emoji = null; - return false; // invalid name + return null; // invalid name } if (UnicodeEmojis.TryGetValue(name, out string? unicodeEntity)) { - emoji = new DiscordEmoji { Discord = client, Name = unicodeEntity }; - return true; + return new DiscordEmoji { Discord = client, Name = unicodeEntity }; } else if (includeGuilds) { name = name.Substring(1, name.Length - 2); // remove colons - foreach (DiscordGuild guild in client.Guilds.Values) + foreach (ulong guildId in client._guildIds) { - emoji = guild.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); - if (emoji != null) + DiscordGuild? guild = await client.Cache.TryGetGuildAsync(guildId); + DiscordEmoji? emoji = guild?.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); + if (emoji is not null) { - return true; + return emoji; } } } - - emoji = null; - return false; + + return null; } } diff --git a/DSharpPlus/Entities/Guild/DiscordGuild.cs b/DSharpPlus/Entities/Guild/DiscordGuild.cs index 1c5e0350f9..fa9853e065 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuild.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuild.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; - +using DSharpPlus.Caching; using DSharpPlus.Entities.AuditLogs; using DSharpPlus.EventArgs; using DSharpPlus.Exceptions; @@ -13,7 +13,6 @@ using DSharpPlus.Net.Abstractions; using DSharpPlus.Net.Models; using DSharpPlus.Net.Serialization; - using Newtonsoft.Json; namespace DSharpPlus.Entities; @@ -39,7 +38,7 @@ public class DiscordGuild : SnowflakeObject, IEquatable /// Gets the guild icon's url. /// [JsonIgnore] - public string IconUrl + public string? IconUrl => this.GetIconUrl(ImageFormat.Auto, 1024); /// @@ -52,7 +51,7 @@ public string IconUrl /// Gets the guild splash's url. /// [JsonIgnore] - public string SplashUrl + public string? SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"https://cdn.discordapp.com/splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.jpg" : null; /// @@ -65,7 +64,7 @@ public string SplashUrl /// Gets the guild discovery splash's url. /// [JsonIgnore] - public string DiscoverySplashUrl + public string? DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"https://cdn.discordapp.com/discovery-splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.jpg" : null; /// @@ -87,15 +86,6 @@ public string DiscoverySplashUrl [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? Permissions { get; set; } - /// - /// Gets the guild's owner. - /// - [JsonIgnore] - public DiscordMember Owner - => this.Members.TryGetValue(this.OwnerId, out DiscordMember? owner) - ? owner - : this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).GetAwaiter().GetResult(); - /// /// Gets the guild's voice region ID. /// @@ -115,13 +105,6 @@ public DiscordVoiceRegion VoiceRegion [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong _afkChannelId { get; set; } = 0; - /// - /// Gets the guild's AFK voice channel. - /// - [JsonIgnore] - public DiscordChannel AfkChannel - => this.GetChannel(this._afkChannelId); - /// /// Gets the guild's AFK timeout. /// @@ -159,8 +142,8 @@ public DiscordChannel AfkChannel /// Gets the channel where system messages (such as boost and welcome messages) are sent. /// [JsonIgnore] - public DiscordChannel SystemChannel => this._systemChannelId.HasValue - ? this.GetChannel(this._systemChannelId.Value) + public Task? SystemChannel => this._systemChannelId.HasValue + ? this.GetChannelAsync(this._systemChannelId.Value) : null; /// @@ -176,7 +159,9 @@ public DiscordChannel AfkChannel /// Gets the guild's safety alerts channel. /// [JsonIgnore] - public DiscordChannel? SafetyAlertsChannel => this.SafetyAlertsChannelId is not null ? this.GetChannel(this.SafetyAlertsChannelId.Value) : null; + public Task? SafetyAlertsChannel => this.SafetyAlertsChannelId is not null + ? this.GetChannelAsync(this.SafetyAlertsChannelId.Value) + : null; /// /// Gets whether this guild's widget is enabled. @@ -191,8 +176,8 @@ public DiscordChannel AfkChannel /// Gets the widget channel for this guild. /// [JsonIgnore] - public DiscordChannel WidgetChannel => this._widgetChannelId.HasValue - ? this.GetChannel(this._widgetChannelId.Value) + public Task? WidgetChannel => this._widgetChannelId.HasValue + ? this.GetChannelAsync(this._widgetChannelId.Value) : null; [JsonProperty("rules_channel_id")] @@ -203,8 +188,8 @@ public DiscordChannel AfkChannel /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] - public DiscordChannel RulesChannel => this._rulesChannelId.HasValue - ? this.GetChannel(this._rulesChannelId.Value) + public Task? RulesChannel => this._rulesChannelId.HasValue + ? this.GetChannelAsync(this._rulesChannelId.Value) : null; [JsonProperty("public_updates_channel_id")] @@ -215,8 +200,8 @@ public DiscordChannel AfkChannel /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] - public DiscordChannel PublicUpdatesChannel => this._publicUpdatesChannelId.HasValue - ? this.GetChannel(this._publicUpdatesChannelId.Value) + public Task? PublicUpdatesChannel => this._publicUpdatesChannelId.HasValue + ? this.GetChannelAsync(this._publicUpdatesChannelId.Value) : null; /// @@ -315,8 +300,7 @@ public IReadOnlyDictionary ScheduledEvents /// [JsonProperty("max_presences")] public int? MaxPresences { get; internal set; } - -#pragma warning disable CS1734 + /// /// Gets the approximate number of members in this guild, when using and having set to true. /// @@ -328,7 +312,6 @@ public IReadOnlyDictionary ScheduledEvents /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } -#pragma warning restore CS1734 /// /// Gets the maximum amount of users allowed per video channel. @@ -350,7 +333,7 @@ public IReadOnlyDictionary ScheduledEvents /// /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. /// - [JsonIgnore] // TODO overhead of => vs Lazy? it's a struct + [JsonIgnore] public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this._members); [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] @@ -393,7 +376,7 @@ public DiscordMember CurrentMember /// Gets the @everyone role for this guild. /// [JsonIgnore] - public DiscordRole EveryoneRole + public DiscordRole? EveryoneRole => this.GetRole(this.Id); [JsonIgnore] @@ -496,13 +479,21 @@ internal DiscordGuild() #region Guild Methods + + /// + /// Gets the guilds owner. + /// + /// Whether to always make a REST request and update the member cache. + /// The member object of the guild owner + public Task GetOwnerAsync(bool skipCache = false) => this.GetMemberAsync(this.OwnerId, skipCache); + /// /// Gets guild's icon URL, in requested format and size. /// /// The image format of the icon to get. /// The maximum size of the icon. Must be a power of two, minimum 16, maximum 4096. /// The URL of the guild's icon. - public string GetIconUrl(ImageFormat imageFormat, ushort imageSize = 1024) + public string? GetIconUrl(ImageFormat imageFormat, ushort imageSize = 1024) { if (string.IsNullOrWhiteSpace(this.IconHash)) @@ -1169,6 +1160,20 @@ public Task DeleteAllChannelsAsync() return Task.WhenAll(tasks); } + + public ValueTask GetAfkChannelAsync(bool skipCache = false) + { + + var channel = this.Channels.Values.FirstOrDefault(xc => xc.Id == this.AfkChannelId); + if (channel != null) + { + this._afkChannel = channel; + return new(channel); + } + + return new(this.Discord.ApiClient.GetChannelAsync(this.AfkChannelId, this.Id)); + } + /// /// Estimates the number of users to be pruned. /// @@ -1444,7 +1449,7 @@ public async IAsyncEnumerable GetAllMembersAsync() { DiscordUser user = new(transportMember.User) { Discord = this.Discord }; - user = this.Discord.UpdateUserCache(user); + await this.Discord.Cache.AddIfNotPresentAsync(user, user.GetCacheKey()); DiscordMember member = new (transportMember) { Discord = this.Discord, _guild_id = this.Id }; yield return member; @@ -1531,17 +1536,38 @@ public async Task CreateRoleAsync(string name = null, Permissions? /// ID of the role to get. /// Requested role. /// Thrown when Discord is unable to process the request. - public DiscordRole GetRole(ulong id) + public DiscordRole? GetRole(ulong id) => this._roles.TryGetValue(id, out DiscordRole? role) ? role : null; /// /// Gets a channel from this guild by its ID. /// /// ID of the channel to get. + /// Whether to skip the cache check and request the channel via REST. /// Requested channel. /// Thrown when Discord is unable to process the request. - public DiscordChannel GetChannel(ulong id) - => this._channels != null && this._channels.TryGetValue(id, out DiscordChannel? channel) ? channel : null; + public async Task GetChannelAsync(ulong id, bool skipCache = false){ + + if (skipCache) + { + return await this.Discord.ApiClient.GetChannelAsync(id); + } + + if (this._channels is not null && this._channels.TryGetValue(id, out DiscordChannel? channel)) + { + return channel; + } + + channel = await this.Discord.Cache.TryGetChannelAsync(id); + if (channel is not null) + { + return channel; + } + + channel = await this.Discord.ApiClient.GetChannelAsync(id); + return channel; + } + /// /// Gets audit log entries for this guild. @@ -1560,6 +1586,8 @@ public async IAsyncEnumerable GetAuditLogsAsync DiscordAuditLogActionType? actionType = null ) { + AuditLogParser auditLogParser = new(this.Discord, this.Id, this); + //Get all entries from api int entriesAcquiredLastCall = 1, totalEntriesCollected = 0, remainingEntries = 100; ulong last = 0; @@ -1579,7 +1607,7 @@ public async IAsyncEnumerable GetAuditLogsAsync if (entriesAcquiredLastCall > 0) { last = guildAuditLog.Entries.Last().Id; - IAsyncEnumerable parsedEntries = AuditLogParser.ParseAuditLogToEntriesAsync(this, guildAuditLog); + IAsyncEnumerable parsedEntries = auditLogParser.ParseAuditLogToEntriesAsync(guildAuditLog); await foreach (DiscordAuditLogEntry discordAuditLogEntry in parsedEntries) { yield return discordAuditLogEntry; diff --git a/DSharpPlus/Entities/Guild/DiscordMember.cs b/DSharpPlus/Entities/Guild/DiscordMember.cs index 57302d650a..8d8daa15a8 100644 --- a/DSharpPlus/Entities/Guild/DiscordMember.cs +++ b/DSharpPlus/Entities/Guild/DiscordMember.cs @@ -142,7 +142,7 @@ public DiscordColor Color /// [JsonIgnore] public DiscordVoiceState VoiceState - => this.Discord.Guilds[this._guild_id].VoiceStates.TryGetValue(this.Id, out DiscordVoiceState? voiceState) ? voiceState : null; + => this.Discord._guildIds[this._guild_id].VoiceStates.TryGetValue(this.Id, out DiscordVoiceState? voiceState) ? voiceState : null; [JsonIgnore] internal ulong _guild_id = 0; @@ -152,7 +152,7 @@ public DiscordVoiceState VoiceState /// [JsonIgnore] public DiscordGuild Guild - => this.Discord.Guilds[this._guild_id]; + => this.Discord._guildIds[this._guild_id]; /// /// Gets whether this member is the Guild owner. diff --git a/DSharpPlus/Entities/Guild/DiscordRole.cs b/DSharpPlus/Entities/Guild/DiscordRole.cs index e4a4aefd25..079b16a297 100644 --- a/DSharpPlus/Entities/Guild/DiscordRole.cs +++ b/DSharpPlus/Entities/Guild/DiscordRole.cs @@ -106,7 +106,7 @@ public string Mention /// Thrown when Discord is unable to process the request. public async Task ModifyPositionAsync(int position, string reason = null) { - DiscordRole[] roles = this.Discord.Guilds[this._guild_id].Roles.Values.OrderByDescending(xr => xr.Position).ToArray(); + DiscordRole[] roles = this.Discord._guildIds[this._guild_id].Roles.Values.OrderByDescending(xr => xr.Position).ToArray(); RestGuildRoleReorderPayload[] pmds = new RestGuildRoleReorderPayload[roles.Length]; for (int i = 0; i < roles.Length; i++) { diff --git a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs index 1895108a5b..d86638ee6b 100644 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs +++ b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs @@ -33,16 +33,15 @@ public sealed class DiscordScheduledGuildEvent : SnowflakeObject public DateTimeOffset? EndTime { get; internal set; } /// - /// The guild this event is scheduled for. + /// The guild this event is scheduled for. This is null if the guild is not in cache. /// - [JsonIgnore] - public DiscordGuild Guild => (this.Discord as DiscordClient)!.InternalGetCachedGuild(this.GuildId); + [JsonIgnore] public DiscordGuild? Guild { get; internal set; } /// /// The channel this event is scheduled for, if applicable. /// [JsonIgnore] - public DiscordChannel? Channel => this.ChannelId.HasValue ? this.Guild.GetChannel(this.ChannelId.Value) : null; + public DiscordChannel? Channel { get; internal set; } /// /// The id of the channel this event is scheduled in, if any. @@ -54,7 +53,7 @@ public sealed class DiscordScheduledGuildEvent : SnowflakeObject /// The id of the guild this event is scheduled for. /// [JsonProperty("guild_id")] - public ulong GuildId { get; set; } + public ulong GuildId { get; internal set; } /// /// The user that created this event. diff --git a/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs b/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs index b192d635e8..1d9be797ce 100644 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs +++ b/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using DSharpPlus.Caching; using Newtonsoft.Json; namespace DSharpPlus.Entities; @@ -9,7 +10,7 @@ namespace DSharpPlus.Entities; public class DiscordWidget : SnowflakeObject { [JsonIgnore] - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } /// /// Gets the guild's name. diff --git a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs b/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs index 89c363e989..19dff00a0a 100644 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs +++ b/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs @@ -2,12 +2,23 @@ namespace DSharpPlus.Entities; +using System.Threading.Tasks; +using Caching; + /// /// Represents a Discord guild's widget settings. /// public class DiscordWidgetSettings { - internal DiscordGuild Guild { get; set; } + /// + /// DicordClient associated with this entity. + /// + public BaseDiscordClient Discord { get; internal set; } + + /// + /// Guild associated with this entity. + /// + public CachedEntity Guild { get; internal set; } /// /// Gets the guild's widget channel id. @@ -18,9 +29,19 @@ public class DiscordWidgetSettings /// /// Gets the guild's widget channel. /// - public DiscordChannel Channel - => this.Guild?.GetChannel(this.ChannelId); - + public async ValueTask GetChannelAsync() + { + DiscordChannel? cachedChannel = await this.Discord.Cache.TryGetChannelAsync(this.ChannelId); + if (cachedChannel is not null) + { + return cachedChannel; + } + else + { + return await this.Discord.GetChannelAsync(this.ChannelId); + } + } + /// /// Gets if the guild's widget is enabled. /// diff --git a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs index 04724169a6..2b28900771 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs @@ -31,8 +31,9 @@ public sealed class DiscordInteraction : SnowflakeObject /// Gets the guild that invoked this interaction. /// [JsonIgnore] + //TODO apply caching public DiscordGuild Guild - => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); + => (this.Discord as DiscordClient).GetCachedGuild(this.GuildId); /// /// Gets the Id of the channel that invoked this interaction. @@ -45,7 +46,7 @@ public DiscordGuild Guild /// [JsonIgnore] public DiscordChannel Channel - => (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThread(this.ChannelId) ?? (this.Guild == null ? new DiscordDmChannel { Id = this.ChannelId, Type = ChannelType.Private, Discord = this.Discord, Recipients = new DiscordUser[] { this.User } } : null); + => (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThreadAsync(this.ChannelId) ?? (this.Guild == null ? new DiscordDmChannel { Id = this.ChannelId, Type = ChannelType.Private, Discord = this.Discord, Recipients = new DiscordUser[] { this.User } } : null); /// /// Gets the user that invoked this interaction. diff --git a/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs b/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs index 3c7af15c84..24bb636bee 100644 --- a/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs +++ b/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs @@ -25,8 +25,9 @@ public class DiscordGuildApplicationCommandPermissions : SnowflakeObject /// Gets the guild. /// [JsonIgnore] + //TODO apply caching public DiscordGuild Guild - => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); + => (this.Discord as DiscordClient).GetCachedGuild(this.GuildId); /// /// Gets the permissions for the application command in the guild. diff --git a/DSharpPlus/Entities/User/DiscordPresence.cs b/DSharpPlus/Entities/User/DiscordPresence.cs index dfcc873e8a..dd74d54a45 100644 --- a/DSharpPlus/Entities/User/DiscordPresence.cs +++ b/DSharpPlus/Entities/User/DiscordPresence.cs @@ -4,6 +4,9 @@ namespace DSharpPlus.Entities; +using System.Threading.Tasks; +using Caching; + /// /// Represents a user presence. /// @@ -15,13 +18,18 @@ public sealed class DiscordPresence // "The user object within this event can be partial, the only field which must be sent is the id field, everything else is optional." [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] internal TransportUser InternalUser { get; set; } + + /// + /// Gets the id of the user that owns this presence. + /// + [JsonIgnore] + public ulong UserId => this.InternalUser.Id; /// /// Gets the user that owns this presence. /// - [JsonIgnore] - public DiscordUser User - => this.Discord.GetCachedOrEmptyUserInternal(this.InternalUser.Id); + public ValueTask GetUserAsync(bool skipCache = false) + => this.Discord.GetUserAsync(this.UserId, skipCache); /// /// Gets the user's current activity. @@ -55,9 +63,8 @@ public DiscordUser User /// /// Gets the guild for which this presence was set. /// - [JsonIgnore] - public DiscordGuild Guild - => this.GuildId != 0 ? this.Discord._guilds[this.GuildId] : null; + public ValueTask GetGuildAsync(bool withCounts = false, bool skipCache = false) + => this.Discord.GetGuildAsync(this.GuildId, withCounts, skipCache); /// /// Gets this user's platform-dependent status. diff --git a/DSharpPlus/Entities/User/DiscordUser.cs b/DSharpPlus/Entities/User/DiscordUser.cs index 43d7bad56d..092ac77a36 100644 --- a/DSharpPlus/Entities/User/DiscordUser.cs +++ b/DSharpPlus/Entities/User/DiscordUser.cs @@ -8,6 +8,8 @@ namespace DSharpPlus.Entities; +using Caching; + /// /// Represents a Discord user. /// @@ -186,13 +188,8 @@ public bool IsCurrent /// Thrown when Discord is unable to process the request. public Task UnbanAsync(DiscordGuild guild, string reason = null) => guild.UnbanMemberAsync(this, reason); - - /// - /// Gets this user's presence. - /// - [JsonIgnore] - public DiscordPresence Presence - => this.Discord is DiscordClient dc ? dc.Presences.TryGetValue(this.Id, out DiscordPresence? presence) ? presence : null : null; + + public ValueTask GetPresenceAsync() => this.Discord.Cache.TryGetUserPresenceAsync(this.Id); /// /// Gets the user's avatar URL, in requested format and size. diff --git a/DSharpPlus/Entities/Voice/DiscordVoiceState.cs b/DSharpPlus/Entities/Voice/DiscordVoiceState.cs index a22e3ca48b..59d8fc6f8f 100644 --- a/DSharpPlus/Entities/Voice/DiscordVoiceState.cs +++ b/DSharpPlus/Entities/Voice/DiscordVoiceState.cs @@ -1,6 +1,7 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Caching; using DSharpPlus.Net.Abstractions; using Newtonsoft.Json; @@ -19,48 +20,18 @@ public class DiscordVoiceState [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? GuildId { get; init; } - /// - /// Gets the guild associated with this voice state. - /// - [JsonIgnore] - public DiscordGuild? Guild => this.GuildId is not null ? this.Discord.Guilds[this.GuildId.Value] : this.Channel?.Guild; - /// /// Gets ID of the channel this user is connected to. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? ChannelId { get; init; } - /// - /// Gets the channel this user is connected to. - /// - [JsonIgnore] - public DiscordChannel? Channel => (this.ChannelId.HasValue && this.ChannelId.Value != 0) ? this.Discord.InternalGetCachedChannel(this.ChannelId.Value) : null; - /// /// Gets ID of the user to which this voice state belongs. /// [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong UserId { get; init; } - /// - /// Gets the user associated with this voice state. - /// This can be cast to a if this voice state was in a guild. - /// - [JsonIgnore] - public DiscordUser User - { - get - { - if (this.Guild is not null && this.Guild._members.TryGetValue(this.UserId, out DiscordMember? member)) - { - return member; - } - - return this.Discord.GetCachedOrEmptyUserInternal(this.UserId); - } - } - /// /// Gets ID of the session of this voice state. /// @@ -114,24 +85,7 @@ public DiscordUser User /// [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset? RequestToSpeakTimestamp { get; internal init; } - - /// - /// Gets the member this voice state belongs to. - /// - [JsonIgnore, NotNullIfNotNull(nameof(Guild))] - public DiscordMember? Member - { - get - { - if (this.Guild is not null && this.Guild.Members.TryGetValue(this.TransportMember.User.Id, out DiscordMember? member)) - { - return member; - } - - return new DiscordMember(this.TransportMember) { Discord = this.Discord }; - } - } - + [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] internal TransportMember TransportMember { get; init; } @@ -148,5 +102,77 @@ internal DiscordVoiceState(DiscordMember member) // Values not filled out are values that are not known from a DiscordMember } - public override string ToString() => $"{this.UserId.ToString(CultureInfo.InvariantCulture)} in {(this.GuildId ?? this.Channel?.GuildId)?.ToString(CultureInfo.InvariantCulture)}"; + public override string ToString() => $"{this.UserId.ToString(CultureInfo.InvariantCulture)} in channel {ChannelId ?? 0}"; + + /// + /// Gets the guild this voicestate belongs to. + /// Setting to true will always make a REST request. + /// + /// Whether to include approximate presence and member counts in the returned guild. + /// Whether to skip the cache and always excute a REST request + /// The requested Guild. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the voicestate does not belong the the guild + public ValueTask GetGuildAsync(bool skipCache = false, bool withCounts = false) + { + if (!GuildId.HasValue) + { + throw new InvalidOperationException("Voicestate does not belong to a guild"); + } + return Discord.GetGuildAsync(GuildId.Value, withCounts, skipCache); + } + + /// + /// Gets the channel this voicestate belongs to. + /// + /// Whether to skip the cache and always execute a REST request + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// This exception is only listed because Discord documents a voice state without a channel being possible, but not when or why + public ValueTask GetChannelAsync(bool skipCache = false) + { + if (!ChannelId.HasValue) + { + throw new InvalidOperationException("Voicestate does not belong to a guild"); + } + + return Discord.GetChannelAsync(ChannelId.Value, skipCache); + } + + /// + /// Gets the user to which this voicestate belongs + /// + /// Whether to skip the cache and always execute a REST request + /// Thrown when the user does not exist. + /// Thrown when Discord is unable to process the request. + public ValueTask GetUserAsync(bool skipCache = false) + => Discord.GetUserAsync(UserId, skipCache); + + /// + /// Gets the member to which this voicestate belongs + /// + /// Whether to skip the cache and always execute a REST request + /// Thrown when the member does not exist. + /// Thrown when Discord is unable to process the request. + public async ValueTask GetMemberAsync(bool skipCache = false) + { + if (!GuildId.HasValue) + { + throw new InvalidOperationException("Voicestate does not belong to a guild"); + } + + if (!skipCache) + { + DiscordMember? member = await this.Discord.Cache.TryGetMemberAsync(this.UserId, this.GuildId.Value); + if (member is not null) + { + return member; + } + } + + return await Discord.ApiClient.GetGuildMemberAsync(GuildId.Value, UserId); + } } diff --git a/DSharpPlus/EventArgs/Channel/ChannelCreateEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelCreateEventArgs.cs index 9f1ec15ef4..af5176147d 100644 --- a/DSharpPlus/EventArgs/Channel/ChannelCreateEventArgs.cs +++ b/DSharpPlus/EventArgs/Channel/ChannelCreateEventArgs.cs @@ -1,4 +1,5 @@ using DSharpPlus.Entities; +using DSharpPlus.Caching; namespace DSharpPlus.EventArgs; @@ -13,9 +14,9 @@ public class ChannelCreateEventArgs : DiscordEventArgs public DiscordChannel Channel { get; internal set; } /// - /// Gets the guild in which the channel was created. + /// Gets the guild in which the channel was created. This field is null if the channel has no guild. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity? Guild { get; internal set; } internal ChannelCreateEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Channel/ChannelPinsUpdateEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelPinsUpdateEventArgs.cs index e6af8bed9e..8b71cb76c8 100644 --- a/DSharpPlus/EventArgs/Channel/ChannelPinsUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Channel/ChannelPinsUpdateEventArgs.cs @@ -1,4 +1,5 @@ using System; +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -9,14 +10,14 @@ namespace DSharpPlus.EventArgs; public class ChannelPinsUpdateEventArgs : DiscordEventArgs { /// - /// Gets the guild in which the update occurred. + /// Gets the guild in which the update occurred. This field is null if the channel is not a guild channel. /// - public DiscordGuild Guild { get; internal set; } - + public CachedEntity? Guild { get; internal set; } + /// /// Gets the channel in which the update occurred. /// - public DiscordChannel Channel { get; internal set; } + public CachedEntity Channel { get; internal set; } /// /// Gets the timestamp of the latest pin. diff --git a/DSharpPlus/EventArgs/Channel/ChannelUpdateEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelUpdateEventArgs.cs index 4bc0829d93..5b272c2830 100644 --- a/DSharpPlus/EventArgs/Channel/ChannelUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Channel/ChannelUpdateEventArgs.cs @@ -15,12 +15,19 @@ public class ChannelUpdateEventArgs : DiscordEventArgs /// /// Gets the pre-update channel. /// - public DiscordChannel ChannelBefore { get; internal set; } + /// This value comes from cache and is null if its not present in cache + public DiscordChannel? ChannelBefore { get; internal set; } /// /// Gets the guild in which the update occurred. /// - public DiscordGuild Guild { get; internal set; } + /// This value comes from cache and could be null + public DiscordGuild? Guild { get; internal set; } + + /// + /// Gets the ID of the guild in which the update occurred. + /// + public ulong GuildId { get; internal set; } internal ChannelUpdateEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddEventArgs.cs b/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddEventArgs.cs index fce9688845..c5dfcc78ad 100644 --- a/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,12 +11,13 @@ public class GuildBanAddEventArgs : DiscordEventArgs /// /// Gets the member that was banned. /// - public DiscordMember Member { get; internal set; } + public CachedEntity Member { get; internal set; } /// /// Gets the guild this member was banned in. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } + internal GuildBanAddEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemoveEventArgs.cs b/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemoveEventArgs.cs index 17190a28af..e0d0209285 100644 --- a/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemoveEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemoveEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,12 +11,13 @@ public class GuildBanRemoveEventArgs : DiscordEventArgs /// /// Gets the member that just got unbanned. /// - public DiscordMember Member { get; internal set; } + public CachedEntity Member { get; internal set; } /// /// Gets the guild this member was unbanned in. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } + internal GuildBanRemoveEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs index 52b08becec..4bbdc6dcef 100644 --- a/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs @@ -3,6 +3,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + public class GuildAuditLogCreatedEventArgs : DiscordEventArgs { /// @@ -13,7 +15,7 @@ public class GuildAuditLogCreatedEventArgs : DiscordEventArgs /// /// Guild where audit log entry was created. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } internal GuildAuditLogCreatedEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs index 30783fb218..4a3153cc26 100644 --- a/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs @@ -13,6 +13,5 @@ public class GuildDownloadCompletedEventArgs : DiscordEventArgs /// public IReadOnlyDictionary Guilds { get; } - internal GuildDownloadCompletedEventArgs(IReadOnlyDictionary guilds) - : base() => this.Guilds = guilds; + internal GuildDownloadCompletedEventArgs(IReadOnlyDictionary guilds) => this.Guilds = guilds; } diff --git a/DSharpPlus/EventArgs/Guild/GuildEmojisUpdateEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildEmojisUpdateEventArgs.cs index a2c35e5fc0..f61a5fb98e 100644 --- a/DSharpPlus/EventArgs/Guild/GuildEmojisUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildEmojisUpdateEventArgs.cs @@ -12,16 +12,16 @@ public class GuildEmojisUpdateEventArgs : DiscordEventArgs /// Gets the list of emojis after the change. /// public IReadOnlyDictionary EmojisAfter { get; internal set; } - + /// - /// Gets the list of emojis before the change. + /// Gets the list of emojis before the change. This field is null if the guild was not in cache. /// - public IReadOnlyDictionary EmojisBefore { get; internal set; } + public IReadOnlyDictionary? EmojisBefore { get; internal set; } /// - /// Gets the guild in which the update occurred. + /// Gets the guild in which the update occurred. This field is null if the guild was not in cache. /// - public DiscordGuild Guild { get; internal set; } + public DiscordGuild? Guild { get; internal set; } internal GuildEmojisUpdateEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdateEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdateEventArgs.cs index ea3d52707b..34110098e2 100644 --- a/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdateEventArgs.cs @@ -8,9 +8,14 @@ namespace DSharpPlus.EventArgs; public class GuildIntegrationsUpdateEventArgs : DiscordEventArgs { /// - /// Gets the guild that had its integrations updated. + /// Gets the guild that had its integrations updated. This field is null if the guild was not in cache. /// - public DiscordGuild Guild { get; internal set; } + public DiscordGuild? Guild { get; internal set; } + + /// + /// Gets the id of the guild that had its integrations updated. + /// + public ulong GuildId { get; internal set; } internal GuildIntegrationsUpdateEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/GuildUpdateEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildUpdateEventArgs.cs index cc7d15d323..e556910d5c 100644 --- a/DSharpPlus/EventArgs/Guild/GuildUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildUpdateEventArgs.cs @@ -8,9 +8,9 @@ namespace DSharpPlus.EventArgs; public class GuildUpdateEventArgs : DiscordEventArgs { /// - /// Gets the guild before it was updated. + /// Gets the guild before it was updated. This property might be null if the guild wasn't cached. /// - public DiscordGuild GuildBefore { get; internal set; } + public DiscordGuild? GuildBefore { get; internal set; } /// /// Gets the guild after it was updated. diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddEventArgs.cs index 09f0bce7cf..8ac46e6682 100644 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -15,7 +16,8 @@ public class GuildMemberAddEventArgs : DiscordEventArgs /// /// Gets the guild the member was added to. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } + internal GuildMemberAddEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemoveEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemoveEventArgs.cs index 2b792014bb..7bdc0d3e61 100644 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemoveEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemoveEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,12 +11,12 @@ public class GuildMemberRemoveEventArgs : DiscordEventArgs /// /// Gets the guild the member was removed from. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } /// /// Gets the member that was removed. /// - public DiscordMember Member { get; internal set; } + public CachedEntity Member { get; internal set; } internal GuildMemberRemoveEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdateEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdateEventArgs.cs index 49605cbb01..17825f54f3 100644 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdateEventArgs.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -11,9 +12,10 @@ namespace DSharpPlus.EventArgs; public class GuildMemberUpdateEventArgs : DiscordEventArgs { /// - /// Gets the guild in which the update occurred. + /// Gets the guild in which the update occurred. This field is null if the guild was not in cache. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } + /// /// Get the member with post-update info @@ -21,9 +23,9 @@ public class GuildMemberUpdateEventArgs : DiscordEventArgs public DiscordMember MemberAfter { get; internal set; } /// - /// Get the member with pre-update info + /// Get the member with pre-update info. This field is null if the guild was not in cache. /// - public DiscordMember MemberBefore { get; internal set; } + public DiscordMember? MemberBefore { get; internal set; } /// /// Gets a collection containing post-update roles. diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkEventArgs.cs index 631c4c1fa8..338af197ac 100644 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkEventArgs.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -11,8 +12,8 @@ public class GuildMembersChunkEventArgs : DiscordEventArgs /// /// Gets the guild that requested this chunk. /// - public DiscordGuild Guild { get; internal set; } - + public CachedEntity Guild { get; internal set; } + /// /// Gets the collection of members returned from this chunk. /// @@ -41,7 +42,7 @@ public class GuildMembersChunkEventArgs : DiscordEventArgs /// /// Gets the unique string used to identify the request, if specified. /// - public string Nonce { get; set; } + public string? Nonce { get; set; } internal GuildMembersChunkEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreateEventArgs.cs b/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreateEventArgs.cs index 6bb24721bc..ad3c0274b2 100644 --- a/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreateEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,7 +11,8 @@ public class GuildRoleCreateEventArgs : DiscordEventArgs /// /// Gets the guild in which the role was created. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } + /// /// Gets the role that was created. diff --git a/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeleteEventArgs.cs b/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeleteEventArgs.cs index 91cc13c40f..ad5769eeb6 100644 --- a/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeleteEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeleteEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,12 +11,13 @@ public class GuildRoleDeleteEventArgs : DiscordEventArgs /// /// Gets the guild in which the role was deleted. /// - public DiscordGuild Guild { get; internal set; } - + public CachedEntity Guild { get; internal set; } + /// /// Gets the role that was deleted. /// - public DiscordRole Role { get; internal set; } + public CachedEntity? Role { get; internal set; } + internal GuildRoleDeleteEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdateEventArgs.cs b/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdateEventArgs.cs index 7e47d66db7..1b161591e8 100644 --- a/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdateEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,7 +11,12 @@ public class GuildRoleUpdateEventArgs : DiscordEventArgs /// /// Gets the guild in which the update occurred. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } + + /// + /// Gets the id of the guild in which the update occurred. + /// + public ulong GuildId { get; internal set; } /// /// Gets the post-update role. @@ -18,9 +24,9 @@ public class GuildRoleUpdateEventArgs : DiscordEventArgs public DiscordRole RoleAfter { get; internal set; } /// - /// Gets the pre-update role. + /// Gets the pre-update role. This value is null if the role was created. /// - public DiscordRole RoleBefore { get; internal set; } + public DiscordRole? RoleBefore { get; internal set; } internal GuildRoleUpdateEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdateEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdateEventArgs.cs index f030f53c57..4b3f3cae42 100644 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdateEventArgs.cs @@ -9,7 +9,7 @@ public class ScheduledGuildEventUpdateEventArgs : DiscordEventArgs /// /// The event before the update, or null if it wasn't cached. /// - public DiscordScheduledGuildEvent EventBefore { get; internal set; } + public DiscordScheduledGuildEvent? EventBefore { get; internal set; } /// /// The event after the update. diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddEventArgs.cs index d842f0fd67..1598787a4d 100644 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddEventArgs.cs @@ -9,12 +9,22 @@ public class ScheduledGuildEventUserAddEventArgs : DiscordEventArgs /// /// The guild the event is scheduled for. /// - public DiscordGuild Guild => this.Event.Guild; + public DiscordGuild? Guild { get; internal set; } + + /// + /// The id of the guild the event is scheduled for. + /// + public ulong GuildId { get; internal set; } /// /// The event that was subscribed to. /// - public DiscordScheduledGuildEvent Event { get; internal set; } + public DiscordScheduledGuildEvent? Event { get; internal set; } + + /// + /// The id of the event that was subscribed to. + /// + public ulong EventId { get; internal set; } /// /// The user that subscribed to the event. diff --git a/DSharpPlus/EventArgs/Invite/InviteCreateEventArgs.cs b/DSharpPlus/EventArgs/Invite/InviteCreateEventArgs.cs index 654e6a7a65..2cb5d1c97a 100644 --- a/DSharpPlus/EventArgs/Invite/InviteCreateEventArgs.cs +++ b/DSharpPlus/EventArgs/Invite/InviteCreateEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,12 +11,12 @@ public sealed class InviteCreateEventArgs : DiscordEventArgs /// /// Gets the guild that created the invite. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } /// - /// Gets the channel that the invite is for. + /// Gets the channel that the invite is for. This value is null if the channel wasn't cached. /// - public DiscordChannel Channel { get; internal set; } + public CachedEntity Channel { get; internal set; } /// /// Gets the created invite. diff --git a/DSharpPlus/EventArgs/Invite/InviteDeleteEventArgs.cs b/DSharpPlus/EventArgs/Invite/InviteDeleteEventArgs.cs index 4b5485f57d..6ce2f8a560 100644 --- a/DSharpPlus/EventArgs/Invite/InviteDeleteEventArgs.cs +++ b/DSharpPlus/EventArgs/Invite/InviteDeleteEventArgs.cs @@ -1,3 +1,4 @@ +using DSharpPlus.Caching; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -10,12 +11,12 @@ public sealed class InviteDeleteEventArgs : DiscordEventArgs /// /// Gets the guild that deleted the invite. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } /// /// Gets the channel that the invite was for. /// - public DiscordChannel Channel { get; internal set; } + public CachedEntity Channel { get; internal set; } /// /// Gets the deleted invite. diff --git a/DSharpPlus/EventArgs/Message/MessageBulkDeleteEventArgs.cs b/DSharpPlus/EventArgs/Message/MessageBulkDeleteEventArgs.cs index d093cb7193..6507bb0858 100644 --- a/DSharpPlus/EventArgs/Message/MessageBulkDeleteEventArgs.cs +++ b/DSharpPlus/EventArgs/Message/MessageBulkDeleteEventArgs.cs @@ -14,14 +14,24 @@ public class MessageBulkDeleteEventArgs : DiscordEventArgs public IReadOnlyList Messages { get; internal set; } /// - /// Gets the channel in which the deletion occurred. + /// Gets the channel in which the deletion occurred. This value is null if the channel was not in cache. /// - public DiscordChannel Channel { get; internal set; } + public DiscordChannel? Channel { get; internal set; } + + /// + /// Gets the id of the channel in which the deletion occurred. + /// + public ulong ChannelId { get; internal set; } /// - /// Gets the guild in which the deletion occurred. + /// Gets the guild in which the deletion occurred. This value is null if the guild was not in cache. + /// + public DiscordGuild? Guild { get; internal set; } + + /// + /// Gets the id of the guild in which the deletion occurred. This value is null if the the messages was not deleted in a guild. /// - public DiscordGuild Guild { get; internal set; } + public ulong? GuildId { get; internal set; } internal MessageBulkDeleteEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Message/MessageDeleteEventArgs.cs b/DSharpPlus/EventArgs/Message/MessageDeleteEventArgs.cs index 53babd8c6d..eb505eb35d 100644 --- a/DSharpPlus/EventArgs/Message/MessageDeleteEventArgs.cs +++ b/DSharpPlus/EventArgs/Message/MessageDeleteEventArgs.cs @@ -2,6 +2,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + /// /// Represents arguments for event. /// @@ -10,17 +12,17 @@ public class MessageDeleteEventArgs : DiscordEventArgs /// /// Gets the message that was deleted. /// - public DiscordMessage Message { get; internal set; } + public CachedEntity Message { get; internal set; } /// /// Gets the channel this message belonged to. /// - public DiscordChannel Channel { get; internal set; } + public CachedEntity Channel { get; internal set; } /// - /// Gets the guild this message belonged to. + /// Gets the guild this message belonged to. This property is null for DM channels. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity? Guild { get; internal set; } internal MessageDeleteEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs index a4413693e4..6dea21d6fb 100644 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs +++ b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddEventArgs.cs @@ -2,6 +2,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + /// /// Represents arguments for event. /// @@ -10,28 +12,23 @@ public class MessageReactionAddEventArgs : DiscordEventArgs /// /// Gets the message for which the update occurred. /// - public DiscordMessage Message { get; internal set; } + public CachedEntity Message { get; internal set; } /// /// Gets the channel to which this message belongs. /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; + public CachedEntity Channel { get; internal set; } /// - /// Gets the guild in which the reaction was added. + /// Gets the guild in which the reaction was added. This value is null if message was in DMs. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity? Guild { get; internal set; } /// /// Gets the user who created the reaction. - /// This can be cast to a if the reaction was in a guild. + /// This can be a if the reaction was in a guild and the member was in cache. /// - public DiscordUser User { get; internal set; } + public CachedEntity User { get; internal set; } /// /// Gets the emoji used for this reaction. diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEmojiEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEmojiEventArgs.cs index 4be9e2f751..0955e347e0 100644 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEmojiEventArgs.cs +++ b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEmojiEventArgs.cs @@ -2,6 +2,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + /// /// Represents arguments for /// @@ -10,17 +12,17 @@ public sealed class MessageReactionRemoveEmojiEventArgs : DiscordEventArgs /// /// Gets the channel the removed reactions were in. /// - public DiscordChannel Channel { get; internal set; } + public CachedEntity Channel { get; internal set; } /// - /// Gets the guild the removed reactions were in. + /// Gets the guild the removed reactions were in. This value is null if the message was in DMs. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity? Guild { get; internal set; } /// /// Gets the message that had the removed reactions. /// - public DiscordMessage Message { get; internal set; } + public CachedEntity Message { get; internal set; } /// /// Gets the emoji of the reaction that was removed. diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEventArgs.cs index 0051547da9..ba8db6fb6d 100644 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEventArgs.cs +++ b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemoveEventArgs.cs @@ -2,6 +2,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + /// /// Represents arguments for event. /// @@ -10,27 +12,22 @@ public class MessageReactionRemoveEventArgs : DiscordEventArgs /// /// Gets the message for which the update occurred. /// - public DiscordMessage Message { get; internal set; } + public CachedEntity Message { get; internal set; } /// /// Gets the channel to which this message belongs. /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; + public CachedEntity Channel { get; internal set; } /// - /// Gets the users whose reaction was removed. + /// Gets the users whose reaction was removed. This can be a if the reaction was in a guild and the member was in cache. /// - public DiscordUser User { get; internal set; } + public CachedEntity User { get; internal set; } /// - /// Gets the guild in which the reaction was deleted. + /// Gets the guild in which the reaction was deleted. This value is null if message was in DMs. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity? Guild { get; internal set; } /// /// Gets the emoji used for this reaction. diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearEventArgs.cs index 8c6cc6b6e8..0b4138d8cb 100644 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearEventArgs.cs +++ b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearEventArgs.cs @@ -2,6 +2,9 @@ namespace DSharpPlus.EventArgs; +using System.Threading.Channels; +using Caching; + /// /// Represents arguments for event. /// @@ -10,23 +13,18 @@ public class MessageReactionsClearEventArgs : DiscordEventArgs /// /// Gets the message for which the update occurred. /// - public DiscordMessage Message { get; internal set; } + public CachedEntity Message { get; internal set; } /// /// Gets the channel to which this message belongs. /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; + public CachedEntity Channel { get; internal set; } /// - /// Gets the guild in which the reactions were cleared. - /// - public DiscordGuild Guild - => this.Channel.Guild; + /// Gets the guild in which the reactions were cleared. This value is null if message was in DMs. + /// + public CachedEntity? Guild { get; internal set; } + internal MessageReactionsClearEventArgs() : base() { } } diff --git a/DSharpPlus/EventArgs/User/PresenceUpdateEventArgs.cs b/DSharpPlus/EventArgs/User/PresenceUpdateEventArgs.cs index 05e06a71ce..0bee336240 100644 --- a/DSharpPlus/EventArgs/User/PresenceUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/User/PresenceUpdateEventArgs.cs @@ -2,6 +2,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + /// /// Represents arguments for event. /// @@ -10,7 +12,7 @@ public class PresenceUpdateEventArgs : DiscordEventArgs /// /// Gets the user whose presence was updated. /// - public DiscordUser User { get; internal set; } + public CachedEntity User { get; internal set; } /// /// Gets the user's new game. @@ -23,9 +25,9 @@ public class PresenceUpdateEventArgs : DiscordEventArgs public UserStatus Status { get; internal set; } /// - /// Gets the user's old presence. + /// Gets the user's old presence. This is null if the user was not cached prior to presence update. /// - public DiscordPresence PresenceBefore { get; internal set; } + public DiscordPresence? PresenceBefore { get; internal set; } /// /// Gets the user's new presence. diff --git a/DSharpPlus/EventArgs/Voice/VoiceServerUpdateEventArgs.cs b/DSharpPlus/EventArgs/Voice/VoiceServerUpdateEventArgs.cs index a9d55baa03..2261b61d3a 100644 --- a/DSharpPlus/EventArgs/Voice/VoiceServerUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Voice/VoiceServerUpdateEventArgs.cs @@ -2,6 +2,8 @@ namespace DSharpPlus.EventArgs; +using Caching; + /// /// Represents arguments for event. /// @@ -10,7 +12,7 @@ public class VoiceServerUpdateEventArgs : DiscordEventArgs /// /// Gets the guild for which the update occurred. /// - public DiscordGuild Guild { get; internal set; } + public CachedEntity Guild { get; internal set; } /// /// Gets the new voice endpoint. diff --git a/DSharpPlus/EventArgs/Voice/VoiceStateUpdateEventArgs.cs b/DSharpPlus/EventArgs/Voice/VoiceStateUpdateEventArgs.cs index 99f8b9cb14..ab7ead1602 100644 --- a/DSharpPlus/EventArgs/Voice/VoiceStateUpdateEventArgs.cs +++ b/DSharpPlus/EventArgs/Voice/VoiceStateUpdateEventArgs.cs @@ -23,9 +23,9 @@ public class VoiceStateUpdateEventArgs : DiscordEventArgs public DiscordChannel Channel { get; internal set; } /// - /// Gets the voice state pre-update. + /// Gets the voice state pre-update. This is null if the previous state was not cached. /// - public DiscordVoiceState Before { get; internal set; } + public DiscordVoiceState? Before { get; internal set; } /// /// Gets the voice state post-update. diff --git a/DSharpPlus/IMessageCacheProvider.cs b/DSharpPlus/IMessageCacheProvider.cs index 657c260717..e69de29bb2 100644 --- a/DSharpPlus/IMessageCacheProvider.cs +++ b/DSharpPlus/IMessageCacheProvider.cs @@ -1,27 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -using DSharpPlus.Entities; - -namespace DSharpPlus; -public interface IMessageCacheProvider -{ - /// - /// Add a object to the cache. - /// - /// The object to add to the cache. - void Add(DiscordMessage message); - - /// - /// Remove the object associated with the message ID from the cache. - /// - /// The ID of the message to remove from the cache. - void Remove(ulong messageId); - - /// - /// Try to get a object associated with the message ID from the cache. - /// - /// The ID of the message to retrieve from the cache. - /// The object retrieved from the cache, if it exists; null otherwise. - /// if the message can be retrieved from the cache, otherwise. - bool TryGet(ulong messageId, [NotNullWhen(true)] out DiscordMessage? message); -} diff --git a/DSharpPlus/MessageCache.cs b/DSharpPlus/MessageCache.cs deleted file mode 100644 index 4601efe8a0..0000000000 --- a/DSharpPlus/MessageCache.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; -using Microsoft.Extensions.Caching.Memory; - -namespace DSharpPlus; -internal class MessageCache : IMessageCacheProvider -{ - private readonly MemoryCache _cache; - private readonly MemoryCacheEntryOptions _entryOptions; - - internal MessageCache(int capacity) - { - _cache = new MemoryCache(new MemoryCacheOptions() - { - SizeLimit = capacity, - }); - - _entryOptions = new MemoryCacheEntryOptions() - { - Size = 1, - }; - } - - /// - public void Add(DiscordMessage message) => _cache.Set(message.Id, message, _entryOptions); - - /// - public void Remove(ulong messageId) => _cache.Remove(messageId); - - /// - public bool TryGet(ulong messageId, out DiscordMessage? message) => _cache.TryGetValue(messageId, out message); -} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportGuild.cs b/DSharpPlus/Net/Abstractions/Transport/TransportGuild.cs new file mode 100644 index 0000000000..80167ed64e --- /dev/null +++ b/DSharpPlus/Net/Abstractions/Transport/TransportGuild.cs @@ -0,0 +1,288 @@ +namespace DSharpPlus.Net.Abstractions; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Entities; +using Newtonsoft.Json; +using Serialization; + +internal sealed record TransportGuild +{ + /// + /// Gets the ID of this object. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong Id { get; internal set; } + + /// + /// Gets the guild's name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + internal string? Name { get; set; } + + /// + /// Gets the guild icon's hash. + /// + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + internal string? IconHash { get; set; } + + /// + /// Gets the guild splash's hash. + /// + [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] + internal string? SplashHash { get; set; } + + /// + /// Gets the guild discovery splash's hash. + /// + [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] + internal string? DiscoverySplashHash { get; set; } + + /// + /// Gets the preferred locale of this guild. + /// This is used for server discovery and notices from Discord. Defaults to en-US. + /// + [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] + internal string? PreferredLocale { get; set; } + + /// + /// Gets the ID of the guild's owner. + /// + [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? OwnerId { get; set; } + + /// + /// Gets permissions for the user in the guild (does not include channel overrides) + /// + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + internal Permissions? Permissions { get; set; } + + /// + /// Gets the guild's voice region ID. + /// + [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] + internal string? VoiceRegionId { get; set; } + + /// + /// Gets the guild's AFK voice channel ID. + /// + [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? AfkChannelId { get; set; } + + /// + /// Gets the guild's AFK timeout. + /// + [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] + internal int? AfkTimeout { get; set; } + + /// + /// Gets the guild's verification level. + /// + [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] + internal VerificationLevel? VerificationLevel { get; set; } + + /// + /// Gets the guild's default notification settings. + /// + [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] + internal DefaultMessageNotifications? DefaultMessageNotifications { get; set; } + + /// + /// Gets the guild's explicit content filter settings. + /// + [JsonProperty("explicit_content_filter")] + internal ExplicitContentFilter? ExplicitContentFilter { get; set; } + + /// + /// Gets the guild's nsfw level. + /// + [JsonProperty("nsfw_level")] + internal NsfwLevel? NsfwLevel { get; set; } + + [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] + internal ulong? SystemChannelId { get; set; } + + /// + /// Gets the settings for this guild's system channel. + /// + [JsonProperty("system_channel_flags")] + internal SystemChannelFlags? SystemChannelFlags { get; set; } + + [JsonProperty("safety_alerts_channel_id")] + internal ulong? SafetyAlertsChannelId { get; set; } + + /// + /// Gets whether this guild's widget is enabled. + /// + [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] + internal bool? WidgetEnabled { get; set; } + + [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? WidgetChannelId { get; set; } + + [JsonProperty("rules_channel_id")] + internal ulong? RulesChannelId { get; set; } + + [JsonProperty("public_updates_channel_id")] + internal ulong? PublicUpdatesChannelId { get; set; } + + /// + /// Gets the application ID of this guild if it is bot created. + /// + [JsonProperty("application_id")] + internal ulong? ApplicationId { get; set; } + + [JsonProperty("guild_scheduled_events")] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? ScheduledEvents { get; set; } + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? Roles { get; set; } + + [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? Stickers { get; set; } + + [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? Emojis { get; set; } + + /// + /// Gets a collection of this guild's features. + /// + [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] + internal IReadOnlyList? Features { get; set; } + + /// + /// Gets the required multi-factor authentication level for this guild. + /// + [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] + internal MfaLevel? MfaLevel { get; set; } + + /// + /// Gets this guild's join date. + /// + [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] + internal DateTimeOffset? JoinedAt { get; set; } + + /// + /// Gets whether this guild is considered to be a large guild. + /// + [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] + internal bool? IsLarge { get; set; } + + /// + /// Gets whether this guild is unavailable. + /// + [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] + internal bool? IsUnavailable { get; set; } + + /// + /// Gets the total number of members in this guild. + /// + [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] + internal int? MemberCount { get; set; } + + /// + /// Gets the maximum amount of members allowed for this guild. + /// + [JsonProperty("max_members")] + internal int? MaxMembers { get; set; } + + /// + /// Gets the maximum amount of presences allowed for this guild. + /// + [JsonProperty("max_presences")] + internal int? MaxPresences { get; set; } + + /// + /// Gets the approximate number of members in this guild, when using and having set to true. + /// + [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] + internal int? ApproximateMemberCount { get; set; } + + /// + /// Gets the approximate number of presences in this guild, when using and having set to true. + /// + [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] + internal int? ApproximatePresenceCount { get; set; } + + /// + /// Gets the maximum amount of users allowed per video channel. + /// + [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] + internal int? MaxVideoChannelUsers { get; set; } + + [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? VoiceStates { get; set; } + + [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? Members { get; set; } + + [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? Channels { get; set; } + + [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? Threads { get; set; } + + /// + /// Gets whether the current user is the guild's owner. + /// + [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] + internal bool? IsOwner { get; set; } + + /// + /// Gets the vanity URL code for this guild, when applicable. + /// + [JsonProperty("vanity_url_code")] + internal string? VanityUrlCode { get; set; } + + /// + /// Gets the guild description, when applicable. + /// + [JsonProperty("description")] + internal string? Description { get; set; } + + /// + /// Gets this guild's banner hash, when applicable. + /// + [JsonProperty("banner")] + internal string? Banner { get; set; } + + /// + /// Gets this guild's premium tier (Nitro boosting). + /// + [JsonProperty("premium_tier")] + internal PremiumTier? PremiumTier { get; set; } + + /// + /// Gets the amount of members that boosted this guild. + /// + [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] + internal int? PremiumSubscriptionCount { get; set; } + + /// + /// Whether the guild has the boost progress bar enabled. + /// + [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] + internal bool? PremiumProgressBarEnabled { get; set; } + + /// + /// Gets whether this guild is designated as NSFW. + /// + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + internal bool? IsNsfw { get; set; } + + [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary? StageInstances { get; set; } + + internal TransportGuild() + { } +} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs b/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs index 3623171614..df73c0e002 100644 --- a/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs +++ b/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs @@ -2,7 +2,7 @@ namespace DSharpPlus.Net.Abstractions; -internal class TransportUser +internal sealed record TransportUser { [JsonProperty("id")] public ulong Id { get; internal set; } diff --git a/DSharpPlus/Net/Rest/DiscordApiClient.cs b/DSharpPlus/Net/Rest/DiscordApiClient.cs index b96c964c28..f901ca9066 100644 --- a/DSharpPlus/Net/Rest/DiscordApiClient.cs +++ b/DSharpPlus/Net/Rest/DiscordApiClient.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers.Text; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; @@ -9,7 +8,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; - +using DSharpPlus.Caching; using DSharpPlus.Entities; using DSharpPlus.Entities.AuditLogs; using DSharpPlus.Net.Abstractions; @@ -50,14 +49,14 @@ ILogger logger internal DiscordApiClient(RestClient rest) => this._rest = rest; - private DiscordMessage PrepareMessage(JToken msgRaw) + private async ValueTask PrepareMessageAsync(JToken msgRaw) { TransportUser author = msgRaw["author"]!.ToDiscordObject(); DiscordMessage message = msgRaw.ToDiscordObject(); message.Discord = this._discord!; - this.PopulateMessage(author, message); + await this.PopulateMessageAsync(author, message); JToken? referencedMsg = msgRaw["referenced_message"]; @@ -65,39 +64,36 @@ private DiscordMessage PrepareMessage(JToken msgRaw) { TransportUser referencedAuthor = referencedMsg["author"]!.ToDiscordObject(); message.ReferencedMessage.Discord = this._discord!; - this.PopulateMessage(referencedAuthor, message.ReferencedMessage); + await this.PopulateMessageAsync(referencedAuthor, message.ReferencedMessage); } - if (message.Channel is not null) + if (message.Channel.HasCachedValue) { return message; } - message.Channel = !message._guildId.HasValue - ? new DiscordDmChannel - { - Id = message.ChannelId, - Discord = this._discord!, - Type = ChannelType.Private - } - : new DiscordChannel - { - Id = message.ChannelId, - GuildId = message._guildId, - Discord = this._discord! - }; + DiscordChannel? cachedChannel = await this._discord!.Cache.TryGet(ICacheKey.ForChannel(message.ChannelId)); + + CachedEntity channel = new(message.ChannelId, cachedChannel); + message.Channel = channel; return message; } - private void PopulateMessage(TransportUser author, DiscordMessage ret) + private async Task PopulateMessageAsync(TransportUser author, DiscordMessage ret) { - DiscordGuild? guild = ret.Channel?.Guild; + ret.Channel.TryGetCachedValue(out DiscordChannel? channel); + channel ??= await this._discord!.Cache.TryGetChannelAsync(ret.Channel.Key); + DiscordGuild? guild = null; + if (channel?.GuildId is not null) + { + guild = await this._discord!.Cache.TryGetGuildAsync(channel.GuildId.Value); + } //If this is a webhook, it shouldn't be in the user cache. if (author.IsBot && int.Parse(author.Discriminator) == 0) { - ret.Author = new(author) + ret.Author = new DiscordUser(author) { Discord = this._discord! }; @@ -105,27 +101,26 @@ private void PopulateMessage(TransportUser author, DiscordMessage ret) else { // get and cache the user - if (!this._discord!.UserCache.TryGetValue(author.Id, out DiscordUser? user)) - { - user = new DiscordUser(author) + DiscordUser? user = await this._discord!.Cache.TryGet(ICacheKey.ForUser(author.Id)); + + user ??= new DiscordUser(author) { - Discord = this._discord + Discord = this._discord! }; - } - this._discord.UserCache[author.Id] = user; + await this._discord.Cache.AddUserAsync(user); // get the member object if applicable, if not set the message author to an user if (guild is not null) { - if (!guild.Members.TryGetValue(author.Id, out DiscordMember? member)) - { - member = new(user) + DiscordMember? member = + await this._discord.Cache.TryGet(ICacheKey.ForMember(author.Id, guild.Id)); + + member ??= new DiscordMember(user) { Discord = this._discord, _guild_id = guild.Id }; - } ret.Author = member; } @@ -135,7 +130,7 @@ private void PopulateMessage(TransportUser author, DiscordMessage ret) } } - ret.PopulateMentions(); + await ret.PopulateMentionsAsync(); ret._reactions ??= new List(); foreach (DiscordReaction reaction in ret._reactions) @@ -244,7 +239,7 @@ internal async ValueTask> SearchMembersAsync { DiscordUser usr = new(transport.User) { Discord = this._discord! }; - this._discord!.UpdateUserCache(usr); + await this._discord!.Cache.AddUserAsync(usr); members.Add(new DiscordMember(transport) { Discord = this._discord, _guild_id = guildId }); } @@ -271,10 +266,12 @@ ulong userId DiscordBan ban = json.ToDiscordObject(); - if (!this._discord!.TryGetCachedUserInternal(ban.RawUser.Id, out DiscordUser? user)) + DiscordUser? user = await this._discord!.Cache.TryGetUserAsync(ban.RawUser.Id); + + if (user is null) { user = new DiscordUser(ban.RawUser) { Discord = this._discord }; - user = this._discord.UpdateUserCache(user); + await this._discord!.Cache.AddUserAsync(user); } ban.User = user; @@ -491,19 +488,18 @@ internal async ValueTask> GetGuildBansAsync RestResponse res = await this._rest.ExecuteRequestAsync(request); - IEnumerable bansRaw = JsonConvert.DeserializeObject>(res.Response!)! - .Select(xb => + IEnumerable bansRaw = JsonConvert.DeserializeObject>(res.Response!).ToList(); + foreach (DiscordBan discordBan in bansRaw) { - if (!this._discord!.TryGetCachedUserInternal(xb.RawUser.Id, out DiscordUser? user)) + DiscordUser? cachedUser = await this._discord!.Cache.TryGetUserAsync(discordBan.RawUser.Id); + if (cachedUser is null) { - user = new DiscordUser(xb.RawUser) { Discord = this._discord }; - user = this._discord.UpdateUserCache(user); + cachedUser = new DiscordUser(discordBan.RawUser) {Discord = this._discord}; + await this._discord.Cache.AddUserAsync(cachedUser); } - xb.User = user; - return xb; - }); - + discordBan.User = cachedUser; + } ReadOnlyCollection bans = new(new List(bansRaw)); return bans; @@ -836,21 +832,28 @@ ulong guildId DiscordWidget ret = json.ToDiscordObject(); ret.Discord = this._discord!; - ret.Guild = this._discord!.Guilds[guildId]; + DiscordGuild? guild = await this._discord!.Cache.TryGetGuildAsync(guildId); + + ret.Guild = new CachedEntity(guildId, guild); - ret.Channels = ret.Guild is null - ? rawChannels.Select(r => new DiscordChannel - { - Id = (ulong)r["id"]!, - Name = r["name"]!.ToString(), - Position = (int)r["position"]! - }).ToList() - : rawChannels.Select(r => + List channels = new(rawChannels.Count); + foreach (JToken jToken in rawChannels) + { + DiscordChannel channel = jToken.ToDiscordObject(); + + DiscordChannel? cachedChannel = await this._discord!.Cache.TryGetChannelAsync(channel.Id); + if (cachedChannel is not null) { - DiscordChannel c = ret.Guild.GetChannel((ulong)r["id"]!); - c.Position = (int)r["position"]!; - return c; - }).ToList(); + channels.Add(cachedChannel); + continue; + } + + channel.Discord = this._discord!; + channel.GuildId = guildId; + channels.Add(channel); + } + + ret.Channels = channels; return ret; } @@ -870,7 +873,9 @@ ulong guildId RestResponse res = await this._rest.ExecuteRequestAsync(request); DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Guild = this._discord!.Guilds[guildId]; + DiscordGuild? guild = await this._discord!.Cache.TryGetGuildAsync(guildId); + ret.Discord = this._discord!; + ret.Guild = new CachedEntity(guildId, guild); return ret; } @@ -906,7 +911,9 @@ internal async ValueTask ModifyGuildWidgetSettingsAsync RestResponse res = await this._rest.ExecuteRequestAsync(request); DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Guild = this._discord!.Guilds[guildId]; + DiscordGuild? guild = await this._discord!.Cache.TryGetGuildAsync(guildId); + ret.Discord = this._discord!; + ret.Guild = new CachedEntity(guildId, guild); return ret; } @@ -1509,7 +1516,7 @@ internal async ValueTask ModifyChannelAsync List? restOverwrites = null; if (permissionOverwrites is not null) { - restOverwrites = new(); + restOverwrites = new List(); foreach (DiscordOverwriteBuilder ow in permissionOverwrites) { restOverwrites.Add(ow.Build()); @@ -1908,15 +1915,8 @@ ulong channelId }; RestResponse res = await this._rest.ExecuteRequestAsync(request); - DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; - // this is really weird, we should consider doing this better - if (ret.IsThread) - { - ret = JsonConvert.DeserializeObject(res.Response!)!; - } - ret.Discord = this._discord!; foreach (DiscordOverwrite xo in ret._permissionOverwrites) { @@ -1942,7 +1942,7 @@ string reason RestRequest request = new() { Route = $"{Endpoints.CHANNELS}/{channelId}", - Url = new($"{Endpoints.CHANNELS}/{channelId}"), + Url = new string($"{Endpoints.CHANNELS}/{channelId}"), Method = HttpMethod.Delete, Headers = headers }; @@ -1968,7 +1968,7 @@ ulong messageId RestResponse res = await this._rest.ExecuteRequestAsync(request); - DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + DiscordMessage ret = await this.PrepareMessageAsync(JObject.Parse(res.Response!)); return ret; } @@ -2102,7 +2102,7 @@ bool suppressNotifications RestResponse res = await this._rest.ExecuteRequestAsync(request); - DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + DiscordMessage ret = await this.PrepareMessageAsync(JObject.Parse(res.Response!)); return ret; } @@ -2160,7 +2160,7 @@ DiscordMessageBuilder builder RestResponse res = await this._rest.ExecuteRequestAsync(request); - DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + DiscordMessage ret = await this.PrepareMessageAsync(JObject.Parse(res.Response!)); return ret; } @@ -2193,7 +2193,7 @@ DiscordMessageBuilder builder builder.ResetFileStreamPositions(); } - return this.PrepareMessage(JObject.Parse(res.Response!)); + return await this.PrepareMessageAsync(JObject.Parse(res.Response!)); } } @@ -2223,6 +2223,7 @@ internal async ValueTask> GetGuildChannelsAsync(ul foreach (DiscordChannel? ret in channelsRaw) { + ret.Discord = this._discord!; foreach (DiscordOverwrite xo in ret._permissionOverwrites) { xo.Discord = this._discord!; @@ -2278,7 +2279,7 @@ internal async ValueTask> GetChannelMessagesAsync List msgs = new(); foreach (JToken xj in msgsRaw) { - msgs.Add(this.PrepareMessage(xj)); + msgs.Add(await this.PrepareMessageAsync(xj)); } return new ReadOnlyCollection(new List(msgs)); @@ -2302,7 +2303,7 @@ ulong messageId RestResponse res = await this._rest.ExecuteRequestAsync(request); - DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + DiscordMessage ret = await this.PrepareMessageAsync(JObject.Parse(res.Response!)); return ret; } @@ -2386,7 +2387,7 @@ internal async ValueTask EditMessageAsync res = await this._rest.ExecuteRequestAsync(request); } - DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + DiscordMessage ret = await this.PrepareMessageAsync(JObject.Parse(res.Response!)); if (files is not null) { @@ -2642,7 +2643,7 @@ ulong channelId List msgs = new(); foreach (JToken xj in msgsRaw) { - msgs.Add(this.PrepareMessage(xj)); + msgs.Add( await this.PrepareMessageAsync(xj)); } return new ReadOnlyCollection(new List(msgs)); @@ -2787,11 +2788,7 @@ ulong recipientId DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; ret.Discord = this._discord!; - if (this._discord is DiscordClient dc) - { - _ = dc._privateChannels.TryAdd(ret.Id, ret); - } - + await this._discord!.Cache.AddChannelAsync(ret); return ret; } @@ -3468,7 +3465,7 @@ ulong userId { Discord = this._discord! }; - _ = this._discord!.UpdateUserCache(usr); + _ = this._discord!.Cache.AddUserAsync(usr); return new DiscordMember(tm) { @@ -3569,7 +3566,7 @@ internal async ValueTask> GetCurrentUserGuildsAsync IEnumerable guildsRaw = JsonConvert.DeserializeObject>(res.Response!)!; IEnumerable guilds = guildsRaw.Select ( - xug => (this._discord as DiscordClient)?._guilds[xug.Id] + xug => (this._discord as DiscordClient)?._guildIds[xug.Id] ) .Where(static guild => guild is not null)!; return new ReadOnlyCollection(new List(guilds)); @@ -3724,7 +3721,7 @@ internal async ValueTask GetGuildAsync if (this._discord is DiscordClient discordClient) { await discordClient.OnGuildUpdateEventAsync(guildRest, rawMembers); - return discordClient._guilds[guildRest.Id]; + return discordClient._guildIds[guildRest.Id]; } else { @@ -4842,7 +4839,7 @@ internal async ValueTask EditWebhookMessageAsync builder.ResetFileStreamPositions(); } - return this.PrepareMessage(JObject.Parse(res.Response!)); + return await this.PrepareMessageAsync(JObject.Parse(res.Response!)); } internal async ValueTask DeleteWebhookMessageAsync @@ -4973,7 +4970,7 @@ internal async ValueTask> GetReactionsAsync { Discord = this._discord! }; - usr = this._discord!.UpdateUserCache(usr); + await this._discord!.Cache.AddUserAsync(usr); users.Add(usr); } @@ -5049,7 +5046,7 @@ ulong guildId IEnumerable emojisRaw = JsonConvert.DeserializeObject>(res.Response!)!; - this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + this._discord!._guildIds.TryGetValue(guildId, out DiscordGuild? guild); Dictionary users = new(); List emojis = new(); foreach (JObject rawEmoji in emojisRaw) @@ -5097,7 +5094,7 @@ ulong emojiId RestResponse res = await this._rest.ExecuteRequestAsync(request); - this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + this._discord!._guildIds.TryGetValue(guildId, out DiscordGuild? guild); JObject emojiRaw = JObject.Parse(res.Response!); DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); @@ -5152,7 +5149,7 @@ internal async ValueTask CreateGuildEmojiAsync RestResponse res = await this._rest.ExecuteRequestAsync(request); - this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + this._discord!._guildIds.TryGetValue(guildId, out DiscordGuild? guild); JObject emojiRaw = JObject.Parse(res.Response!); DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); @@ -5205,7 +5202,7 @@ internal async ValueTask ModifyGuildEmojiAsync RestResponse res = await this._rest.ExecuteRequestAsync(request); - this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + this._discord!._guildIds.TryGetValue(guildId, out DiscordGuild? guild); JObject emojiRaw = JObject.Parse(res.Response!); DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); @@ -6197,7 +6194,7 @@ public async ValueTask CreateForumPostAsync JToken? msgToken = ret["message"]; ret.Remove("message"); - DiscordMessage msg = this.PrepareMessage(msgToken!); + DiscordMessage msg = await this.PrepareMessageAsync(msgToken!); // We know the return type; deserialize directly. DiscordThreadChannel chn = ret.ToDiscordObject(); chn.Discord = this._discord!; diff --git a/DSharpPlus/RingBuffer.cs b/DSharpPlus/RingBuffer.cs deleted file mode 100644 index c1d5a2a87d..0000000000 --- a/DSharpPlus/RingBuffer.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace DSharpPlus; - -/// -/// A circular buffer collection. -/// -/// Type of elements within this ring buffer. -public class RingBuffer : ICollection -{ - /// - /// Gets the current index of the buffer items. - /// - public int CurrentIndex { get; protected set; } - - /// - /// Gets the capacity of this ring buffer. - /// - public int Capacity { get; protected set; } - - /// - /// Gets the number of items in this ring buffer. - /// - public int Count - => this._reached_end ? this.Capacity : this.CurrentIndex; - - /// - /// Gets whether this ring buffer is read-only. - /// - public bool IsReadOnly - => false; - - /// - /// Gets or sets the internal collection of items. - /// - protected T[] InternalBuffer { get; set; } - private bool _reached_end = false; - - /// - /// Creates a new ring buffer with specified size. - /// - /// Size of the buffer to create. - /// - public RingBuffer(int size) - { - if (size <= 0) - { - throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); - } - - this.CurrentIndex = 0; - this.Capacity = size; - this.InternalBuffer = new T[this.Capacity]; - } - - /// - /// Creates a new ring buffer, filled with specified elements. - /// - /// Elements to fill the buffer with. - /// - /// - public RingBuffer(IEnumerable elements) - : this(elements, 0) - { } - - /// - /// Creates a new ring buffer, filled with specified elements, and starting at specified index. - /// - /// Elements to fill the buffer with. - /// Starting element index. - /// - /// - public RingBuffer(IEnumerable elements, int index) - { - if (elements == null || !elements.Any()) - { - throw new ArgumentException(nameof(elements), "The collection cannot be null or empty."); - } - - this.CurrentIndex = index; - this.InternalBuffer = elements.ToArray(); - this.Capacity = this.InternalBuffer.Length; - - if (this.CurrentIndex >= this.InternalBuffer.Length || this.CurrentIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), "Index must be less than buffer capacity, and greater than zero."); - } - } - - /// - /// Inserts an item into this ring buffer. - /// - /// Item to insert. - public void Add(T item) - { - this.InternalBuffer[this.CurrentIndex++] = item; - - if (this.CurrentIndex == this.Capacity) - { - this.CurrentIndex = 0; - this._reached_end = true; - } - } - - /// - /// Gets first item from the buffer that matches the predicate. - /// - /// Predicate used to find the item. - /// Item that matches the predicate, or default value for the type of the items in this ring buffer, if one is not found. - /// Whether an item that matches the predicate was found or not. - public bool TryGet(Func predicate, out T item) - { - for (int i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) - { - if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) - { - item = this.InternalBuffer[i]; - return true; - } - } - for (int i = 0; i < this.CurrentIndex; i++) - { - if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) - { - item = this.InternalBuffer[i]; - return true; - } - } - - item = default; - return false; - } - - /// - /// Clears this ring buffer and resets the current item index. - /// - public void Clear() - { - for (int i = 0; i < this.InternalBuffer.Length; i++) - { - this.InternalBuffer[i] = default; - } - - this.CurrentIndex = 0; - } - - /// - /// Checks whether given item is present in the buffer. This method is not implemented. Use instead. - /// - /// Item to check for. - /// Whether the buffer contains the item. - /// - public bool Contains(T item) => throw new NotImplementedException("This method is not implemented. Use .Contains(predicate) instead."); - - /// - /// Checks whether given item is present in the buffer using given predicate to find it. - /// - /// Predicate used to check for the item. - /// Whether the buffer contains the item. - public bool Contains(Func predicate) => this.InternalBuffer.Any(predicate); - - /// - /// Copies this ring buffer to target array, attempting to maintain the order of items within. - /// - /// Target array. - /// Index starting at which to copy the items to. - public void CopyTo(T[] array, int index) - { - if (array.Length - index < 1) - { - throw new ArgumentException("Target array is too small to contain the elements from this buffer.", nameof(array)); - } - - int ci = 0; - for (int i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) - { - array[ci++] = this.InternalBuffer[i]; - } - - for (int i = 0; i < this.CurrentIndex; i++) - { - array[ci++] = this.InternalBuffer[i]; - } - } - - /// - /// Removes an item from the buffer. This method is not implemented. Use instead. - /// - /// Item to remove. - /// Whether an item was removed or not. - public bool Remove(T item) => throw new NotImplementedException("This method is not implemented. Use .Remove(predicate) instead."); - - /// - /// Removes an item from the buffer using given predicate to find it. - /// - /// Predicate used to find the item. - /// Whether an item was removed or not. - public bool Remove(Func predicate) - { - for (int i = 0; i < this.InternalBuffer.Length; i++) - { - if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) - { - this.InternalBuffer[i] = default; - return true; - } - } - - return false; - } - - /// - /// Returns an enumerator for this ring buffer. - /// - /// Enumerator for this ring buffer. - public IEnumerator GetEnumerator() - { - return !this._reached_end - ? this.InternalBuffer.AsEnumerable().GetEnumerator() - : this.InternalBuffer.Skip(this.CurrentIndex) - .Concat(this.InternalBuffer.Take(this.CurrentIndex)) - .GetEnumerator(); - } - - /// - /// Returns an enumerator for this ring buffer. - /// - /// Enumerator for this ring buffer. - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); -} diff --git a/docs/articles/beyond_basics/caching.md b/docs/articles/beyond_basics/caching.md new file mode 100644 index 0000000000..2ece2dc97c --- /dev/null +++ b/docs/articles/beyond_basics/caching.md @@ -0,0 +1,48 @@ +--- +uid: articles.beyond_basics.caching +title: DSharpPlus Caching +--- + +# Caching + +DSharpPlus has a built-in caching system which will cache most objects received from Discord. This is done to reduce +the amount of API calls made to Discord, and to provide a more consistent experience for the end user. + +## Configuration + +D#+ caching can be configured via `DiscordConfiguration` object. The following properties are available: + +* `CacheConfiguration` - This config is used by the default in-memory cache. The default in-memory cache only provides + time-based expiration, and is not persistent. This cache is used by default. +* `CacheProvider` - You can use this property to provide your own cache provider. This provider must implement the + `IDiscordCache` interface. You can use this to provide your own cache implementation. + +The CacheConfiguration has a dictionary of types and their respective cache configurations. You can use this to configure +the cache for specific types. If you want a certain type to not be cached, you can simply remove the type from the dictionary. +In your own cache provider, you can simple do nothing in the set and remove method and return `null` if you dont cache +the type. + +## Caching behavior + +D#+ will atempt to cache following types: + +* `DiscordGuild` +* `DiscordChannel` +* `DiscordMessage` +* `DiscordMember` +* `DiscordUser` +* `DiscordPresence` + +## Events + +D#+ will use this cache to provide additional information in events. Most events only contains ids of objects, but +D#+ will attempt to provide the full object if it is available in cache. You will see `CachedEntity` +in this case - this will provide you the id of the object, and the full object if it was available in cache when parsing +the event. + +## Create your own cache provider + +You can create your own cache provider by implementing the `IDiscordCache` interface. D#+ supports asynchronous caching +and will await your cache provider methods. You can use this to provide your own caching implementation. + +