Skip to content

Commit

Permalink
feat: Application Emojis
Browse files Browse the repository at this point in the history
  • Loading branch information
Lulalaby committed Jul 19, 2024
1 parent 5b9ab18 commit 9c8100f
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 39 deletions.
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);

/// <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);

/// <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

0 comments on commit 9c8100f

Please sign in to comment.