From 9c8100f1540f4f0f249e44724e4a38695813468c Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Fri, 19 Jul 2024 02:45:24 +0200 Subject: [PATCH] feat: Application Emojis --- DisCatSharp/Clients/BaseDiscordClient.cs | 19 ++ DisCatSharp/Clients/DiscordClient.cs | 187 ++++++++++++++++-- DisCatSharp/DiscordConfiguration.cs | 7 + .../Application/DiscordApplicationEmoji.cs | 45 +++++ DisCatSharp/Entities/Emoji/DiscordEmoji.cs | 56 +++++- .../Rest/RestApplicationPayloads.cs | 25 +++ DisCatSharp/Net/Rest/DiscordApiClient.cs | 157 +++++++++++++-- RELEASENOTES.md | 2 +- 8 files changed, 459 insertions(+), 39 deletions(-) create mode 100644 DisCatSharp/Entities/Application/DiscordApplicationEmoji.cs diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs index f6de30a731..993bf87b84 100644 --- a/DisCatSharp/Clients/BaseDiscordClient.cs +++ b/DisCatSharp/Clients/BaseDiscordClient.cs @@ -20,6 +20,8 @@ using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; + using Sentry; namespace DisCatSharp; @@ -113,6 +115,11 @@ public IReadOnlyDictionary VoiceRegions /// protected internal ConcurrentDictionary InternalVoiceRegions { get; set; } + /// + /// Gets the cached application emojis for this client. + /// + public abstract IReadOnlyDictionary Emojis { get; } + /// /// Gets the lazy voice regions. /// @@ -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); + /// + /// Updates the cached application emojis. + /// + /// The raw emojis. + internal abstract IReadOnlyDictionary UpdateCachedApplicationEmojis(JArray? rawEmojis); + + /// + /// Updates a cached application emoji. + /// + /// The emoji. + internal abstract DiscordApplicationEmoji UpdateCachedApplicationEmoji(DiscordApplicationEmoji emoji); + /// /// Disposes this client. /// diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index d7ea889fc8..b21a7ae513 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -107,6 +107,16 @@ public DiscordIntents Intents /// internal readonly ConcurrentDictionary GuildsInternal = []; + /// + /// Gets a dictionary of application emojis that this client has. The dictionary's key is the emoji ID. + /// + public override IReadOnlyDictionary Emojis { get; } + + /// + /// Gets the application emojis. + /// + internal readonly ConcurrentDictionary EmojisInternal = []; + /// /// Gets the websocket latency for this client. /// @@ -163,6 +173,7 @@ public DiscordClient(DiscordConfiguration config) this.InternalSetup(); this.Guilds = new ReadOnlyConcurrentDictionary(this.GuildsInternal); + this.Emojis = new ReadOnlyConcurrentDictionary(this.EmojisInternal); } /// @@ -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(this.PresencesInternal)); this._embeddedActivitiesLazy = new(() => new ReadOnlyDictionary(this.EmbeddedActivitiesInternal)); @@ -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; @@ -615,6 +633,60 @@ public async Task RemoveGuildApplicationCommandsAsync(ulong guildId) public async Task RemoveGuildApplicationCommandsAsync(DiscordGuild guild) => await this.RemoveGuildApplicationCommandsAsync(guild.Id).ConfigureAwait(false); + /// + /// Gets the application emojis. + /// Whether to ignore the cache. Defaults to false. + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetApplicationEmojisAsync(bool fetch = false) + => (fetch ? null : this.InternalGetCachedApplicationEmojis()) ?? await this.ApiClient.GetApplicationEmojisAsync(this.CurrentApplication.Id).ConfigureAwait(false); + + /// + /// Gets an application emoji. + /// + /// The emoji id. + /// Whether to ignore the cache. Defaults to false. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetApplicationEmojiAsync(ulong id, bool fetch = false) + => (fetch ? null : this.InternalGetCachedApplicationEmoji(id)) ?? await this.ApiClient.GetApplicationEmojiAsync(this.CurrentApplication.Id, id).ConfigureAwait(false); + + /// + /// Creates an application emoji. + /// + /// The name. + /// The image. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateApplicationEmojiAsync(string name, Stream image) + { + var imageb64 = ImageTool.Base64FromStream(image); + return await this.ApiClient.CreateApplicationEmojiAsync(this.CurrentApplication.Id, name, imageb64); + } + + /// + /// Modifies an application emoji. + /// + /// The emoji id. + /// The name. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task ModifyApplicationEmojiAsync(ulong id, string name) + => this.ApiClient.ModifyApplicationEmojiAsync(this.CurrentApplication.Id, id, name); + + /// + /// Deletes an application emoji. + /// + /// The emoji id. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task DeleteApplicationEmojiAsync(ulong id) + => this.ApiClient.DeleteApplicationEmojiAsync(this.CurrentApplication.Id, id); + /// /// Gets a channel. /// @@ -1286,29 +1358,44 @@ public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) = /// /// The target thread id. /// The requested thread. - 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; } + /// + /// Gets an internal cached emoji. + /// + /// The target emoji id. + /// The requested emoji. + internal DiscordApplicationEmoji? InternalGetCachedApplicationEmoji(ulong emojiId) + => this.EmojisInternal is null || this.EmojisInternal.Count is 0 ? null : this.EmojisInternal.GetValueOrDefault(emojiId); + + /// + /// Gets the internal cached emojis. + /// + /// The requested emoji. + internal IReadOnlyList? InternalGetCachedApplicationEmojis() + => this.EmojisInternal is null || this.EmojisInternal.Count is 0 ? null : this.EmojisInternal.Values.ToList(); + /// /// Gets the internal cached scheduled event. /// /// The target scheduled event id. /// The requested scheduled event. - 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; @@ -1323,10 +1410,10 @@ internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEv /// The requested channel. 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) @@ -1342,9 +1429,9 @@ internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEv /// /// The target guild id. /// The requested guild. - 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; @@ -1402,6 +1489,8 @@ private void UpdateMessage(DiscordMessage message, TransportUser? author, Discor /// The updated scheduled event. private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { + ObjectDisposedException.ThrowIf(this._disposed, this); + if (scheduledEvent != null) _ = guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) => { @@ -1432,6 +1521,8 @@ private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent schedul /// The updated user. 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) @@ -1503,7 +1594,7 @@ private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild? gu /// /// The guild. /// The raw events. - private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray? rawEvents) + private void UpdateCachedScheduledEvents(DiscordGuild guild, JArray? rawEvents) { ObjectDisposedException.ThrowIf(this._disposed, this); @@ -1522,6 +1613,69 @@ private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray? rawEvents) } } + /// + /// Updates the cached application emojis. + /// + /// The raw emojis. + internal override IReadOnlyDictionary 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(); + + 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; + } + + /// + /// Updates a cached application emoji. + /// + /// The emoji. + 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; + } + /// /// Updates the cached guild. /// @@ -1729,6 +1883,7 @@ public override void Dispose() { } this.GuildsInternal.Clear(); + this.EmojisInternal.Clear(); this._heartbeatTask?.Dispose(); if (this.Configuration.EnableSentry) diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index e6f2dd7369..6d2910ae6e 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -130,6 +130,12 @@ public string? Token /// public bool ReconnectIndefinitely { internal get; set; } = false; + /// + /// Defines that the client should attempt to fetch application emojis on startup. + /// Defaults to . + /// + public bool AutoFetchApplicationEmojis { internal get; set; } = false; + /// /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. /// @@ -452,5 +458,6 @@ public DiscordConfiguration(DiscordConfiguration other) this.IncludePrereleaseInUpdateCheck = other.IncludePrereleaseInUpdateCheck; this.UpdateCheckGitHubToken = other.UpdateCheckGitHubToken; this.ShowReleaseNotesInUpdateCheck = other.ShowReleaseNotesInUpdateCheck; + this.AutoFetchApplicationEmojis = other.AutoFetchApplicationEmojis; } } diff --git a/DisCatSharp/Entities/Application/DiscordApplicationEmoji.cs b/DisCatSharp/Entities/Application/DiscordApplicationEmoji.cs new file mode 100644 index 0000000000..fe49d61e2b --- /dev/null +++ b/DisCatSharp/Entities/Application/DiscordApplicationEmoji.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; + +using DisCatSharp.Exceptions; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a application emoji. +/// +public sealed class DiscordApplicationEmoji : DiscordEmoji +{ + /// + /// Gets the user that created the emoji (Either a team member or the bot through api). + /// + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser? User { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + internal DiscordApplicationEmoji() + { } + + /// + /// Modifies this emoji. + /// + /// New name for this emoji. + /// The modified emoji. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task ModifyAsync(string name) + => this.Discord.ApiClient.ModifyApplicationEmojiAsync(this.Discord.CurrentApplication.Id, this.Id, name); + + /// + /// Deletes this emoji. + /// + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task DeleteAsync() + => this.Discord.ApiClient.DeleteApplicationEmojiAsync(this.Discord.CurrentApplication.Id, this.Id); +} diff --git a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs index 4ecf1d39d7..a53f0b7fa6 100644 --- a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs +++ b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs @@ -29,7 +29,7 @@ public partial class DiscordEmoji : SnowflakeObject, IEquatable public IReadOnlyList Roles => this._rolesLazy.Value; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public List RolesInternal; + internal List RolesInternal { get; set; } = []; private readonly Lazy> _rolesLazy; @@ -245,7 +245,7 @@ public static bool TryFromUnicode(string unicodeEntity, out DiscordEmoji emoji) => TryFromUnicode(null, unicodeEntity, out emoji); /// - /// Creates an emoji object from a guild emote. + /// Gets an emoji object from an guild emote. /// /// to attach to the object. /// Id of the emote. @@ -263,7 +263,24 @@ public static DiscordEmoji FromGuildEmote(BaseDiscordClient client, ulong id) } /// - /// Attempts to create an emoji object from a guild emote. + /// Gets an emoji object from an application emote. + /// + /// to attach to the object. + /// Id of the emote. + /// Create object. + public static DiscordEmoji FromApplicationEmote(BaseDiscordClient client, ulong id) + { + if (client == null) + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + + if (client.Emojis.TryGetValue(id, out var found)) + return found; + + throw new KeyNotFoundException("Given emote was not found."); + } + + /// + /// Attempts to get an emoji object from an guild emote. /// /// to attach to the object. /// Id of the emote. @@ -283,15 +300,16 @@ public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out Dis } /// - /// Creates 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 + /// Gets 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), guild emoji, as well as application emoji /// (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Should guild emojis be included in the search. + /// Should application emojis be included in the search. /// Create object. - public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true) + public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true, bool includeApplication = true) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); @@ -317,6 +335,14 @@ public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool return emoji; } + if (includeApplication) + { + var ek = name.AsSpan().Slice(1, name.Length - 2); + foreach (var emoji in client.Emojis.Values) + if (emoji.Name.AsSpan().SequenceEqual(ek)) + return emoji; + } + throw new ArgumentException("Invalid emoji name specified.", nameof(name)); } @@ -330,19 +356,20 @@ public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool /// Resulting object. /// Whether the operation was successful. public static bool TryFromName(BaseDiscordClient client, string name, out DiscordEmoji emoji) - => TryFromName(client, name, true, out emoji); + => TryFromName(client, name, true, true, out emoji); /// - /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also + /// Attempts to get 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:). /// Should guild emojis be included in the search. + /// Should application 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 bool TryFromName(BaseDiscordClient client, string name, bool includeGuilds, bool includeApplication, out DiscordEmoji emoji) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); @@ -374,6 +401,17 @@ public static bool TryFromName(BaseDiscordClient client, string name, bool inclu } } + if (includeApplication) + { + var ek = name.AsSpan().Slice(1, name.Length - 2); + foreach (var xemoji in client.Emojis.Values) + if (xemoji.Name.AsSpan().SequenceEqual(ek)) + { + emoji = xemoji; + return true; + } + } + emoji = null; return false; } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationPayloads.cs index 19432cebc6..3978ae9f55 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationPayloads.cs @@ -69,3 +69,28 @@ internal sealed class RestApplicationModifyPayload : ObservableApiObject [JsonProperty("integration_types_config", NullValueHandling = NullValueHandling.Include)] public Optional IntegrationTypesConfig { get; set; } } + + +/// +/// Represents a application emoji modify payload. +/// +internal class RestApplicationEmojiModifyPayload : ObservableApiObject +{ + /// + /// Gets or sets the name. + /// + [JsonProperty("name")] + public string Name { get; set; } +} + +/// +/// Represents a application emoji create payload. +/// +internal sealed class RestApplicationEmojiCreatePayload : RestApplicationEmojiModifyPayload +{ + /// + /// Gets or sets the image b64. + /// + [JsonProperty("image")] + public string ImageB64 { get; set; } +} diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index d3b44a0e96..a78f8385c0 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -5774,9 +5774,9 @@ internal Task ModifyThreadAsync(ulong threadId, ChannelType parentType, string n #region Emoji /// - /// Gets the guild emojis async. + /// Gets the guild emojis. /// - /// The guild_id. + /// The guild id. internal async Task> GetGuildEmojisAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; @@ -5817,10 +5817,10 @@ internal async Task> GetGuildEmojisAsync(ulong } /// - /// Gets the guild emoji async. + /// Gets the guild emoji. /// - /// The guild_id. - /// The emoji_id. + /// The guild id. + /// The emoji id. internal async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; @@ -5847,9 +5847,9 @@ internal async Task GetGuildEmojiAsync(ulong guildId, ulong e } /// - /// Creates the guild emoji async. + /// Creates the guild emoji. /// - /// The guild_id. + /// The guild id. /// The name. /// The imageb64. /// The roles. @@ -5891,10 +5891,10 @@ internal async Task CreateGuildEmojiAsync(ulong guildId, stri } /// - /// Modifies the guild emoji async. + /// Modifies the guild emoji. /// - /// The guild_id. - /// The emoji_id. + /// The guild id. + /// The emoji id. /// The name. /// The roles. /// The reason. @@ -5934,10 +5934,10 @@ internal async Task ModifyGuildEmojiAsync(ulong guildId, ulon } /// - /// Deletes the guild emoji async. + /// Deletes the guild emoji. /// - /// The guild_id. - /// The emoji_id. + /// The guild id. + /// The emoji id. /// The reason. internal Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string? reason) { @@ -5956,6 +5956,137 @@ internal Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string? reason return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } + /// + /// Gets the application emojis. + /// + /// The application id. + internal async Task> GetApplicationEmojisAsync(ulong applicationId) + { + var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.EMOJIS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new + { + application_id = applicationId, + }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); + + var emojisRaw = JsonConvert.DeserializeObject(res.Response); + + return this.Discord.UpdateCachedApplicationEmojis(emojisRaw?.Value("items")).Select(x => x.Value).ToList(); + } + + /// + /// Gets an application emoji. + /// + /// The application id. + /// The emoji id. + internal async Task GetApplicationEmojiAsync(ulong applicationId, ulong emojiId) + { + var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.EMOJIS}/:emoji_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new + { + application_id = applicationId, + emoji_id = emojiId + }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); + + var emojiRaw = JObject.Parse(res.Response); + var emoji = emojiRaw.ToObject(); + + var xtu = emojiRaw["user"]?.ToObject(); + if (xtu is not null) + emoji.User = new(xtu); + + return this.Discord.UpdateCachedApplicationEmoji(emoji); + } + + /// + /// Creates an application emoji. + /// + /// The application id. + /// The name. + /// The imageb64. + internal async Task CreateApplicationEmojiAsync(ulong applicationId, string name, string imageb64) + { + var pld = new RestApplicationEmojiCreatePayload() + { + Name = name, + ImageB64 = imageb64 + }; + + var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.EMOJIS}"; + var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new + { + application_id = applicationId, + }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); + + var emojiRaw = JObject.Parse(res.Response); + var emoji = emojiRaw.ToObject(); + + var xtu = emojiRaw["user"]?.ToObject(); + if (xtu is not null) + emoji.User = new(xtu); + + return this.Discord.UpdateCachedApplicationEmoji(emoji); + } + + /// + /// Modifies an application emoji. + /// + /// The application id. + /// The emoji id. + /// The name. + internal async Task ModifyApplicationEmojiAsync(ulong applicationId, ulong emojiId, string name) + { + var pld = new RestApplicationEmojiModifyPayload() + { + Name = name + }; + + var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.EMOJIS}/:emoji_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new + { + application_id = applicationId, + emoji_id = emojiId + }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); + + var emojiRaw = JObject.Parse(res.Response); + var emoji = emojiRaw.ToObject(); + + var xtu = emojiRaw["user"]?.ToObject(); + if (xtu is not null) + emoji.User = new(xtu); + + return this.Discord.UpdateCachedApplicationEmoji(emoji); + } + + /// + /// Deletes an application emoji. + /// + /// The application id. + /// The emoji id. + internal Task DeleteApplicationEmojiAsync(ulong applicationId, ulong emojiId) + { + var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.EMOJIS}/:emoji_id"; + var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new + { + application_id = applicationId, + emoji_id = emojiId + }, out var path); + + var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); + return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); + } + #endregion #region Stickers diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fd8bd15770..90e3c2cd4f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ DisCatSharp Release Notes - None + - Implemented application emojis. Read more here: https://discord.com/developers/docs/change-log#application-emoji. Breaking change in 'DiscordEmoji.TryFromName' (additional required parameter). DisCatSharp.Attributes Release Notes