Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Application Emojis #525

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions DisCatSharp/Clients/BaseDiscordClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;

using Newtonsoft.Json.Linq;

using Sentry;

namespace DisCatSharp;
Expand Down Expand Up @@ -113,6 +115,11 @@ public IReadOnlyDictionary<string, DiscordVoiceRegion> VoiceRegions
/// </summary>
protected internal ConcurrentDictionary<string, DiscordVoiceRegion> InternalVoiceRegions { get; set; }

/// <summary>
/// Gets the cached application emojis for this client.
/// </summary>
public abstract IReadOnlyDictionary<ulong, DiscordApplicationEmoji> Emojis { get; }

/// <summary>
/// Gets the lazy voice regions.
/// </summary>
Expand Down Expand Up @@ -609,6 +616,18 @@ internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId)
internal bool TryGetCachedUserInternal(ulong userId, [MaybeNullWhen(false)] out DiscordUser user)
=> this.UserCache.TryGetValue(userId, out user);

/// <summary>
/// Updates the cached application emojis.
/// </summary>
/// <param name="rawEmojis">The raw emojis.</param>
internal abstract IReadOnlyDictionary<ulong, DiscordApplicationEmoji> UpdateCachedApplicationEmojis(JArray? rawEmojis);

/// <summary>
/// Updates a cached application emoji.
/// </summary>
/// <param name="emoji">The emoji.</param>
internal abstract DiscordApplicationEmoji UpdateCachedApplicationEmoji(DiscordApplicationEmoji emoji);

/// <summary>
/// Disposes this client.
/// </summary>
Expand Down
187 changes: 171 additions & 16 deletions DisCatSharp/Clients/DiscordClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ public DiscordIntents Intents
/// </summary>
internal readonly ConcurrentDictionary<ulong, DiscordGuild> GuildsInternal = [];

/// <summary>
/// Gets a dictionary of application emojis that this client has. The dictionary's key is the emoji ID.
/// </summary>
public override IReadOnlyDictionary<ulong, DiscordApplicationEmoji> Emojis { get; }

/// <summary>
/// Gets the application emojis.
/// </summary>
internal readonly ConcurrentDictionary<ulong, DiscordApplicationEmoji> EmojisInternal = [];

/// <summary>
/// Gets the websocket latency for this client.
/// </summary>
Expand Down Expand Up @@ -163,6 +173,7 @@ public DiscordClient(DiscordConfiguration config)
this.InternalSetup();

this.Guilds = new ReadOnlyConcurrentDictionary<ulong, DiscordGuild>(this.GuildsInternal);
this.Emojis = new ReadOnlyConcurrentDictionary<ulong, DiscordApplicationEmoji>(this.EmojisInternal);
}

/// <summary>
Expand Down Expand Up @@ -264,6 +275,7 @@ internal void InternalSetup()
this._messagePollVoteRemoved = new("MESSAGE_POLL_VOTE_REMOVED", EventExecutionLimit, this.EventErrorHandler);

this.GuildsInternal.Clear();
this.EmojisInternal.Clear();

this._presencesLazy = new(() => new ReadOnlyDictionary<ulong, DiscordPresence>(this.PresencesInternal));
this._embeddedActivitiesLazy = new(() => new ReadOnlyDictionary<string, DiscordActivity>(this.EmbeddedActivitiesInternal));
Expand Down Expand Up @@ -392,14 +404,20 @@ public async Task ConnectAsync(DiscordActivity? activity = null, UserStatus? sta
throw new("Could not connect to Discord.", cex);
}

if (!this.Configuration.AutoFetchSkuIds)
if (this.Configuration is { AutoFetchSkuIds: false, AutoFetchApplicationEmojis: false })
return;

var skus = await this.ApiClient.GetSkusAsync(this.CurrentApplication.Id).ConfigureAwait(false);
if (!skus.Any())
return;
if (this.Configuration.AutoFetchSkuIds)
{
var skus = await this.ApiClient.GetSkusAsync(this.CurrentApplication.Id).ConfigureAwait(false);
if (!skus.Any())
return;

this.Configuration.SkuId = skus.FirstOrDefault(x => x.Type is SkuType.Subscription)?.Id;
this.Configuration.SkuId = skus.FirstOrDefault(x => x.Type is SkuType.Subscription)?.Id;
}

if (this.Configuration.AutoFetchApplicationEmojis)
await this.ApiClient.GetApplicationEmojisAsync(this.CurrentApplication.Id);

return;

Expand Down Expand Up @@ -615,6 +633,60 @@ public async Task RemoveGuildApplicationCommandsAsync(ulong guildId)
public async Task RemoveGuildApplicationCommandsAsync(DiscordGuild guild)
=> await this.RemoveGuildApplicationCommandsAsync(guild.Id).ConfigureAwait(false);

/// <summary>
/// Gets the application emojis.
/// <param name="fetch">Whether to ignore the cache. Defaults to false.</param>
/// </summary>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public async Task<IReadOnlyList<DiscordApplicationEmoji>> GetApplicationEmojisAsync(bool fetch = false)
=> (fetch ? null : this.InternalGetCachedApplicationEmojis()) ?? await this.ApiClient.GetApplicationEmojisAsync(this.CurrentApplication.Id).ConfigureAwait(false);
Lulalaby marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets an application emoji.
/// </summary>
/// <param name="id">The emoji id.</param>
/// <param name="fetch">Whether to ignore the cache. Defaults to false.</param>
/// <exception cref="NotFoundException">Thrown when the emoji does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public async Task<DiscordApplicationEmoji?> GetApplicationEmojiAsync(ulong id, bool fetch = false)
=> (fetch ? null : this.InternalGetCachedApplicationEmoji(id)) ?? await this.ApiClient.GetApplicationEmojiAsync(this.CurrentApplication.Id, id).ConfigureAwait(false);
Lulalaby marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Creates an application emoji.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="image">The image.</param>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public async Task<DiscordApplicationEmoji> CreateApplicationEmojiAsync(string name, Stream image)
{
var imageb64 = ImageTool.Base64FromStream(image);
return await this.ApiClient.CreateApplicationEmojiAsync(this.CurrentApplication.Id, name, imageb64);
}

/// <summary>
/// Modifies an application emoji.
/// </summary>
/// <param name="id">The emoji id.</param>
/// <param name="name">The name.</param>
/// <exception cref="NotFoundException">Thrown when the emoji does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public Task<DiscordApplicationEmoji> ModifyApplicationEmojiAsync(ulong id, string name)
=> this.ApiClient.ModifyApplicationEmojiAsync(this.CurrentApplication.Id, id, name);

/// <summary>
/// Deletes an application emoji.
/// </summary>
/// <param name="id">The emoji id.</param>
/// <exception cref="NotFoundException">Thrown when the emoji does not exist.</exception>
/// <exception cref="BadRequestException">Thrown when an invalid parameter was provided.</exception>
/// <exception cref="ServerErrorException">Thrown when Discord is unable to process the request.</exception>
public Task DeleteApplicationEmojiAsync(ulong id)
=> this.ApiClient.DeleteApplicationEmojiAsync(this.CurrentApplication.Id, id);

/// <summary>
/// Gets a channel.
/// </summary>
Expand Down Expand Up @@ -1286,29 +1358,44 @@ public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) =
/// </summary>
/// <param name="threadId">The target thread id.</param>
/// <returns>The requested thread.</returns>
internal DiscordThreadChannel InternalGetCachedThread(ulong threadId)
internal DiscordThreadChannel? InternalGetCachedThread(ulong threadId)
{
if (this.Guilds == null)
if (this.GuildsInternal == null || this.GuildsInternal.Count is 0)
return null;

foreach (var guild in this.Guilds.Values)
foreach (var guild in this.GuildsInternal.Values)
if (guild.Threads.TryGetValue(threadId, out var foundThread))
return foundThread;

return null;
}

/// <summary>
/// Gets an internal cached emoji.
/// </summary>
/// <param name="emojiId">The target emoji id.</param>
/// <returns>The requested emoji.</returns>
internal DiscordApplicationEmoji? InternalGetCachedApplicationEmoji(ulong emojiId)
=> this.EmojisInternal is null || this.EmojisInternal.Count is 0 ? null : this.EmojisInternal.GetValueOrDefault(emojiId);

/// <summary>
/// Gets the internal cached emojis.
/// </summary>
/// <returns>The requested emoji.</returns>
internal IReadOnlyList<DiscordApplicationEmoji>? InternalGetCachedApplicationEmojis()
=> this.EmojisInternal is null || this.EmojisInternal.Count is 0 ? null : this.EmojisInternal.Values.ToList();

/// <summary>
/// Gets the internal cached scheduled event.
/// </summary>
/// <param name="scheduledEventId">The target scheduled event id.</param>
/// <returns>The requested scheduled event.</returns>
internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEventId)
internal DiscordScheduledEvent? InternalGetCachedScheduledEvent(ulong scheduledEventId)
{
if (this.Guilds == null)
if (this.GuildsInternal == null || this.GuildsInternal.Count is 0)
return null;

foreach (var guild in this.Guilds.Values)
foreach (var guild in this.GuildsInternal.Values)
if (guild.ScheduledEvents.TryGetValue(scheduledEventId, out var foundScheduledEvent))
return foundScheduledEvent;

Expand All @@ -1323,10 +1410,10 @@ internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEv
/// <returns>The requested channel.</returns>
internal DiscordChannel? InternalGetCachedChannel(ulong channelId, ulong? guildId = null)
{
if (this.Guilds == null)
if (this.GuildsInternal == null || this.GuildsInternal.Count is 0)
return null;

foreach (var guild in this.Guilds.Values)
foreach (var guild in this.GuildsInternal.Values)
if (guild.Channels.TryGetValue(channelId, out var foundChannel))
{
if (guildId.HasValue)
Expand All @@ -1342,9 +1429,9 @@ internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEv
/// </summary>
/// <param name="guildId">The target guild id.</param>
/// <returns>The requested guild.</returns>
internal DiscordGuild InternalGetCachedGuild(ulong? guildId)
internal DiscordGuild? InternalGetCachedGuild(ulong? guildId)
{
if (this.GuildsInternal != null && guildId.HasValue)
if (this.GuildsInternal is { Count: not 0 } && guildId.HasValue)
if (this.GuildsInternal.TryGetValue(guildId.Value, out var guild))
return guild;

Expand Down Expand Up @@ -1402,6 +1489,8 @@ private void UpdateMessage(DiscordMessage message, TransportUser? author, Discor
/// <returns>The updated scheduled event.</returns>
private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
ObjectDisposedException.ThrowIf(this._disposed, this);

if (scheduledEvent != null)
_ = guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) =>
{
Expand Down Expand Up @@ -1432,6 +1521,8 @@ private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent schedul
/// <returns>The updated user.</returns>
private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild? guild, TransportMember? mbr)
{
ObjectDisposedException.ThrowIf(this._disposed, this);

if (mbr is not null && guild is not null)
{
if (mbr.User is not null)
Expand Down Expand Up @@ -1503,7 +1594,7 @@ private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild? gu
/// </summary>
/// <param name="guild">The guild.</param>
/// <param name="rawEvents">The raw events.</param>
private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray? rawEvents)
private void UpdateCachedScheduledEvents(DiscordGuild guild, JArray? rawEvents)
{
ObjectDisposedException.ThrowIf(this._disposed, this);

Expand All @@ -1522,6 +1613,69 @@ private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray? rawEvents)
}
}

/// <summary>
/// Updates the cached application emojis.
/// </summary>
/// <param name="rawEmojis">The raw emojis.</param>
internal override IReadOnlyDictionary<ulong, DiscordApplicationEmoji> UpdateCachedApplicationEmojis(JArray? rawEmojis)
{
ObjectDisposedException.ThrowIf(this._disposed, this);

if (rawEmojis is not null)
{
this.EmojisInternal.Clear();

foreach (var xj in rawEmojis)
{
var xtm = xj.ToDiscordObject<DiscordApplicationEmoji>();

xtm.Discord = this;

if (xtm.User is not null)
{
xtm.User.Discord = this;
this.UpdateUser(xtm.User, null, null, null);
}

this.EmojisInternal[xtm.Id] = xtm;
}
}

return this.Emojis;
}

/// <summary>
/// Updates a cached application emoji.
/// </summary>
/// <param name="emoji">The emoji.</param>
internal override DiscordApplicationEmoji UpdateCachedApplicationEmoji(DiscordApplicationEmoji emoji)
{
ObjectDisposedException.ThrowIf(this._disposed, this);

if (emoji != null)
{
_ = this.EmojisInternal.AddOrUpdate(emoji.Id, emoji, (id, old) =>
{
old.Discord = this;
old.Name = emoji.Name;
old.RolesInternal = emoji.RolesInternal;
old.RequiresColons = emoji.RequiresColons;
old.IsManaged = emoji.IsManaged;
old.IsAnimated = emoji.IsAnimated;
old.IsAvailable = emoji.IsAvailable;
old.User = emoji.User;
return old;
});
if (emoji.User is null)
return emoji;

emoji.User.Discord = this;
this.UpdateUser(emoji.User, null, null, null);
}

return emoji;
}

/// <summary>
/// Updates the cached guild.
/// </summary>
Expand Down Expand Up @@ -1729,6 +1883,7 @@ public override void Dispose()
{ }

this.GuildsInternal.Clear();
this.EmojisInternal.Clear();
this._heartbeatTask?.Dispose();

if (this.Configuration.EnableSentry)
Expand Down
7 changes: 7 additions & 0 deletions DisCatSharp/DiscordConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ public string? Token
/// </summary>
public bool ReconnectIndefinitely { internal get; set; } = false;

/// <summary>
/// <para>Defines that the client should attempt to fetch application emojis on startup.</para>
/// <para>Defaults to <see langword="false"/>.</para>
/// </summary>
public bool AutoFetchApplicationEmojis { internal get; set; } = false;

/// <summary>
/// Sets whether the client should attempt to cache members if exclusively using unprivileged intents.
/// <para>
Expand Down Expand Up @@ -452,5 +458,6 @@ public DiscordConfiguration(DiscordConfiguration other)
this.IncludePrereleaseInUpdateCheck = other.IncludePrereleaseInUpdateCheck;
this.UpdateCheckGitHubToken = other.UpdateCheckGitHubToken;
this.ShowReleaseNotesInUpdateCheck = other.ShowReleaseNotesInUpdateCheck;
this.AutoFetchApplicationEmojis = other.AutoFetchApplicationEmojis;
}
}
Loading
Loading