diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index b1879eebc..1e8fd624d 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -177,6 +177,34 @@ namespace Discord public static string GetSpotifyDirectUrl(string trackId) => $"https://open.spotify.com/track/{trackId}"; + /// + /// Gets a stickers url based off the id and format. + /// + /// The id of the sticker. + /// The format of the sticker + /// + /// A URL to the sticker. + /// + public static string GetStickerUrl(ulong stickerId, StickerFormatType format = StickerFormatType.Png) + => $"{DiscordConfig.CDNUrl}stickers/{stickerId}.{FormatToExtension(format)}"; + + private static string FormatToExtension(StickerFormatType format) + { + switch (format) + { + case StickerFormatType.None: + case StickerFormatType.Png: + return "png"; + case StickerFormatType.Lottie: + return "lottie"; + case StickerFormatType.Apng: + return "apng"; + default: + throw new ArgumentException(nameof(format)); + + } + } + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 414b6fe73..d9daf80cd 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -199,6 +199,13 @@ namespace Discord /// IReadOnlyCollection Emotes { get; } /// + /// Gets a collection of all custom stickers for this guild. + /// + /// + /// A read-only collection of all custom stickers for this guild. + /// + IReadOnlyCollection Stickers { get; } + /// /// Gets a collection of all extra features added to this guild. /// /// @@ -942,6 +949,52 @@ namespace Discord /// Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image of the new emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + Task CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options = null); + + /// + /// Gets a specific sticker within this guild. + /// + /// The id of the sticker to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the sticker found with the + /// specified ; if none is found. + /// + Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Gets a collection of all stickers within this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of stickers found within the guild. + /// + Task> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Deletes a sticker within this guild. + /// + /// The sticker to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); + /// /// Gets this guilds slash commands commands /// diff --git a/src/Discord.Net.Core/Entities/Stickers/ICustomSticker.cs b/src/Discord.Net.Core/Entities/Stickers/ICustomSticker.cs new file mode 100644 index 000000000..03f4ac2eb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/ICustomSticker.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a custom sticker within a guild. + /// + public interface ICustomSticker : ISticker + { + /// + /// Gets the users id who uploaded the sticker. + /// + /// + /// In order to get the author id, the bot needs the MANAGE_EMOJIS_AND_STICKERS permission. + /// + ulong? AuthorId { get; } + + /// + /// Gets the guild that this custom sticker is in. + /// + IGuild Guild { get; } + + /// + /// Modifies this sticker. + /// + /// + /// This method modifies this sticker with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + ///
+ ///
+ /// The bot needs the MANAGE_EMOJIS_AND_STICKERS permission within the guild in order to modify stickers. + ///
+ /// + /// The following example replaces the name of the sticker with kekw. + /// + /// await sticker.ModifyAsync(x => x.Name = "kekw"); + /// + /// + /// A delegate containing the properties to modify the sticker with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Deletes the current sticker. + /// + /// + /// The bot neeeds the MANAGE_EMOJIS_AND_STICKERS permission inside the guild in order to delete stickers. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous deletion operation. + /// + Task DeleteAsync(RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/ISticker.cs b/src/Discord.Net.Core/Entities/Stickers/ISticker.cs similarity index 92% rename from src/Discord.Net.Core/Entities/Messages/ISticker.cs rename to src/Discord.Net.Core/Entities/Stickers/ISticker.cs index e7e4405b6..eca613051 100644 --- a/src/Discord.Net.Core/Entities/Messages/ISticker.cs +++ b/src/Discord.Net.Core/Entities/Stickers/ISticker.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Discord { @@ -63,5 +65,10 @@ namespace Discord /// A with the format type of this sticker. /// StickerFormatType FormatType { get; } + + /// + /// Gets the image url for this sticker. + /// + string GetStickerUrl(); } } diff --git a/src/Discord.Net.Core/Entities/Stickers/StickerProperties.cs b/src/Discord.Net.Core/Entities/Stickers/StickerProperties.cs new file mode 100644 index 000000000..21267cdda --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/StickerProperties.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a class used to modify stickers. + /// + public class StickerProperties + { + /// + /// Gets or sets the name of the sticker. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets the description of the sticker. + /// + public Optional Description { get; set; } + + /// + /// Gets or sets the tags of the sticker. + /// + public Optional> Tags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index a22df9a30..c5ec00117 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -80,5 +80,7 @@ namespace Discord.API public Optional Threads { get; set; } [JsonProperty("nsfw_level")] public NsfwLevel NsfwLevel { get; set; } + [JsonProperty("stickers")] + public Sticker[] Stickers { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index 0474fec5b..e88a94af8 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -60,7 +60,7 @@ namespace Discord.API public Optional ReferencedMessage { get; set; } [JsonProperty("components")] public Optional Components { get; set; } - [JsonProperty("stickers")] - public Optional Stickers { get; set; } + [JsonProperty("sticker_items")] + public Optional StickerItems { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/NitroStickerPacks.cs b/src/Discord.Net.Rest/API/Common/NitroStickerPacks.cs new file mode 100644 index 000000000..ddb9b0bc5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/NitroStickerPacks.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class NitroStickerPacks + { + [JsonProperty("sticker_packs")] + public List StickerPacks { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Sticker.cs b/src/Discord.Net.Rest/API/Common/Sticker.cs index 0d1cac974..31bd97370 100644 --- a/src/Discord.Net.Rest/API/Common/Sticker.cs +++ b/src/Discord.Net.Rest/API/Common/Sticker.cs @@ -21,5 +21,7 @@ namespace Discord.API public string PreviewAsset { get; set; } [JsonProperty("format_type")] public StickerFormatType FormatType { get; set; } + [JsonProperty("user")] + public Optional User { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/StickerItem.cs b/src/Discord.Net.Rest/API/Common/StickerItem.cs new file mode 100644 index 000000000..9ec0fb503 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/StickerItem.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class StickerItem + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("format_type")] + public StickerFormatType FormatType { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/StickerPack.cs b/src/Discord.Net.Rest/API/Common/StickerPack.cs new file mode 100644 index 000000000..aa3314d7c --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/StickerPack.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class StickerPack + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("stickers")] + public Sticker[] Stickers { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("sku_id")] + public ulong SkuId { get; set; } + [JsonProperty("cover_sticker_id")] + public Optional CoverStickerId { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("banner_asset_id")] + public ulong BannerAssetId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs new file mode 100644 index 000000000..291052f3a --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs @@ -0,0 +1,32 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreateStickerParams + { + public Stream File { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Tags { get; set; } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + + d["file"] = new MultipartFile(File, Name); + + d["name"] = Name; + d["description"] = Description; + d["tags"] = Tags; + + return d; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyStickerParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyStickerParams.cs new file mode 100644 index 000000000..47331b5a0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyStickerParams.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyStickerParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("tags")] + public Optional Tags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 5535f4e5c..14aba69ea 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -886,6 +886,67 @@ namespace Discord.API return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + // Stickers + public async Task GetStickerAsync(ulong id, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + + options = RequestOptions.CreateOrClone(options); + + return await NullifyNotFound(SendAsync("GET", () => $"stickers/{id}", new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task GetGuildStickerAsync(ulong guildId, ulong id, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await NullifyNotFound(SendAsync("GET", () => $"guilds/{guildId}/stickers/{id}", new BucketIds(guildId), options: options)).ConfigureAwait(false); + } + public async Task ListGuildStickersAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"guilds/{guildId}/stickers", new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task ListNitroStickerPacksAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"sticker-packs", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task CreateGuildStickerAsync(CreateStickerParams args, ulong guildId, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendMultipartAsync("POST", () => $"guilds/{guildId}/stickers", args.ToDictionary(), new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task ModifyStickerAsync(ModifyStickerParams args, ulong guildId, ulong stickerId, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(stickerId, 0, nameof(stickerId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/stickers/{stickerId}", args, new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task DeleteStickerAsync(ulong guildId, ulong stickerId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(stickerId, 0, nameof(stickerId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", () => $"guilds/{guildId}/stickers/{stickerId}", new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -2002,6 +2063,32 @@ namespace Discord.API } } + protected async Task NullifyNotFound(Task sendTask) where T : class + { + try + { + var result = await sendTask.ConfigureAwait(false); + + if (sendTask.Exception != null) + { + if (sendTask.Exception.InnerException is HttpException x) + { + if (x.HttpCode == HttpStatusCode.NotFound) + { + return null; + } + } + + throw sendTask.Exception; + } + else + return result; + } + catch (HttpException x) when (x.HttpCode == HttpStatusCode.NotFound) + { + return null; + } + } internal class BucketIds { public ulong GuildId { get; internal set; } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index f0eaaf7df..820461e9a 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -535,5 +535,52 @@ namespace Discord.Rest } public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) => client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); + + public static async Task CreateStickerAsync(BaseDiscordClient client, IGuild guild, string name, string description, IEnumerable tags, + Image image, RequestOptions options = null) + { + Preconditions.NotNull(name, nameof(name)); + Preconditions.NotNull(description, nameof(description)); + + Preconditions.AtLeast(name.Length, 2, nameof(name)); + Preconditions.AtLeast(description.Length, 2, nameof(description)); + + Preconditions.AtMost(name.Length, 30, nameof(name)); + Preconditions.AtMost(description.Length, 100, nameof(name)); + + var apiArgs = new CreateStickerParams() + { + Name = name, + Description = description, + File = image.Stream, + Tags = string.Join(", ", tags) + }; + + return await client.ApiClient.CreateGuildStickerAsync(apiArgs, guild.Id, options).ConfigureAwait(false); + } + + public static async Task ModifyStickerAsync(BaseDiscordClient client, IGuild guild, ISticker sticker, Action func, + RequestOptions options = null) + { + if (func == null) + throw new ArgumentNullException(paramName: nameof(func)); + + var props = new StickerProperties(); + func(props); + + var apiArgs = new ModifyStickerParams() + { + Description = props.Description, + Name = props.Name, + Tags = props.Tags.IsSpecified ? + string.Join(", ", props.Tags.Value) : + Optional.Unspecified + }; + + return await client.ApiClient.ModifyStickerAsync(apiArgs, guild.Id, sticker.Id, options).ConfigureAwait(false); + } + + public static async Task DeleteStickerAsync(BaseDiscordClient client, IGuild guild, ISticker sticker, RequestOptions options = null) + => await client.ApiClient.DeleteStickerAsync(guild.Id, sticker.Id, options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Sticker.cs b/src/Discord.Net.Rest/Entities/Messages/Sticker.cs index 5482bed74..aa4960f5b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Sticker.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Sticker.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Model = Discord.API.Sticker; namespace Discord @@ -39,7 +40,7 @@ namespace Discord internal static Sticker Create(Model model) { return new Sticker(model.Id, model.PackId, model.Name, model.Desription, - model.Tags.IsSpecified ? model.Tags.Value.Split(',') : new string[0], + model.Tags.IsSpecified ? model.Tags.Value.Split(',').Select(x => x.Trim()).ToArray() : new string[0], model.Asset, model.PreviewAsset, model.FormatType); } diff --git a/src/Discord.Net.Rest/Entities/Messages/StickerItem.cs b/src/Discord.Net.Rest/Entities/Messages/StickerItem.cs new file mode 100644 index 000000000..61284e604 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/StickerItem.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.StickerItem; + +namespace Discord.Rest +{ + /// + /// Represents a partial sticker received in a message. + /// + public class StickerItem : RestEntity + { + /// + /// The name of this sticker. + /// + public readonly string Name; + + /// + /// The format of this sticker. + /// + public readonly StickerFormatType Format; + + internal StickerItem(BaseDiscordClient client, Model model) + : base(client, model.Id) + { + this.Name = model.Name; + this.Format = model.FormatType; + } + + /// + /// Resolves this sticker item by fetching the from the API. + /// + /// + /// A task representing the download operation, the result of the task is a sticker object. + /// + + public async Task ResolveStickerAsync() + { + var model = await Discord.ApiClient.GetStickerAsync(this.Id); + + return Sticker.Create(model); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 5c385fe01..2521d6b2b 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -19,6 +19,7 @@ using PresenceModel = Discord.API.Presence; using RoleModel = Discord.API.Role; using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; +using StickerModel = Discord.API.Sticker; namespace Discord.WebSocket { @@ -36,7 +37,9 @@ namespace Discord.WebSocket private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; + private ConcurrentDictionary _stickers; private ImmutableArray _emotes; + private ImmutableArray _features; private AudioClient _audioClient; #pragma warning restore IDISP002, IDISP006 @@ -322,6 +325,11 @@ namespace Discord.WebSocket } /// public IReadOnlyCollection Emotes => _emotes; + /// + /// Gets a collection of all custom stickers for this guild. + /// + public IReadOnlyCollection Stickers + => _stickers.Select(x => x.Value).ToImmutableArray(); /// public IReadOnlyCollection Features => _features; /// @@ -440,6 +448,8 @@ namespace Discord.WebSocket } _voiceStates = voiceStates; + + _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); var _ = _syncPromise.TrySetResultAsync(true); @@ -509,6 +519,23 @@ namespace Discord.WebSocket } } _roles = roles; + + if (model.Stickers != null) + { + var stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Stickers.Length * 1.05)); + for (int i = 0; i < model.Stickers.Length; i++) + { + var sticker = model.Stickers[i]; + if (sticker.User.IsSpecified) + AddOrUpdateUser(sticker.User.Value); + + var entity = SocketCustomSticker.Create(Discord, sticker, this, sticker.User.IsSpecified ? sticker.User.Value.Id : null); + + stickers.TryAdd(sticker.Id, entity); + } + } + else + _stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 7); } /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related { @@ -898,6 +925,33 @@ namespace Discord.WebSocket return role; } + internal SocketCustomSticker AddSticker(StickerModel model) + { + if (model.User.IsSpecified) + AddOrUpdateUser(model.User.Value); + + var sticker = SocketCustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); + _stickers[model.Id] = sticker; + return sticker; + } + + internal SocketCustomSticker AddOrUpdateSticker(StickerModel model) + { + if (_stickers.TryGetValue(model.Id, out SocketCustomSticker sticker)) + _stickers[model.Id].Update(model); + else + sticker = AddSticker(model); + + return sticker; + } + + internal SocketCustomSticker RemoveSticker(ulong id) + { + if (_stickers.TryRemove(id, out SocketCustomSticker sticker)) + return sticker; + return null; + } + //Users /// public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) @@ -1109,6 +1163,92 @@ namespace Discord.WebSocket public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + //Stickers + /// + /// Gets a specific sticker within this guild. + /// + /// The id of the sticker to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the sticker found with the + /// specified ; if none is found. + /// + public async ValueTask GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var sticker = _stickers[id]; + + if (sticker != null) + return sticker; + + if (mode == CacheMode.CacheOnly) + return null; + + var model = await Discord.ApiClient.GetGuildStickerAsync(this.Id, id, options).ConfigureAwait(false); + + if (model == null) + return null; + + return AddOrUpdateSticker(model); + } + /// + /// Gets a collection of all stickers within this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of stickers found within the guild. + /// + public async ValueTask> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, + RequestOptions options = null) + { + if (this.Stickers.Count > 0) + return this.Stickers; + + if (mode == CacheMode.CacheOnly) + return ImmutableArray.Create(); + + var models = await Discord.ApiClient.ListGuildStickersAsync(this.Id, options).ConfigureAwait(false); + + List stickers = new(); + + foreach (var model in models) + { + stickers.Add(AddOrUpdateSticker(model)); + } + + return stickers; + } + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image of the new emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Image image, + RequestOptions options = null) + { + var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, image, options).ConfigureAwait(false); + + return AddOrUpdateSticker(model); + } + /// + /// Deletes a sticker within this guild. + /// + /// The sticker to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + public Task DeleteStickerAsync(SocketCustomSticker sticker, RequestOptions options = null) + => sticker.DeleteAsync(options); + //Voice States internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { @@ -1332,6 +1472,8 @@ namespace Discord.WebSocket int? IGuild.ApproximateMemberCount => null; /// int? IGuild.ApproximatePresenceCount => null; + /// + IReadOnlyCollection IGuild.Stickers => Stickers; /// async Task> IGuild.GetBansAsync(RequestOptions options) @@ -1481,6 +1623,13 @@ namespace Discord.WebSocket /// async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) + => await CreateStickerAsync(name, description, tags, image, options); + async Task IGuild.GetStickerAsync(ulong id, CacheMode mode, RequestOptions options) + => await GetStickerAsync(id, mode, options); + async Task> IGuild.GetStickersAsync(CacheMode mode, RequestOptions options) + => await GetStickersAsync(mode, options); + Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) => throw new NotImplementedException(); void IDisposable.Dispose() { @@ -1489,6 +1638,6 @@ namespace Discord.WebSocket _audioClient?.Dispose(); } - + } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketCustomSticker.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketCustomSticker.cs new file mode 100644 index 000000000..4e00873c7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketCustomSticker.cs @@ -0,0 +1,73 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Sticker; + + +namespace Discord.WebSocket +{ + public class SocketCustomSticker : SocketSticker, ICustomSticker + { + /// + /// Gets the user that uploaded the guild sticker. + /// + /// + /// + /// This may return in the WebSocket implementation due to incomplete user collection in + /// large guilds, or the bot doesnt have the MANAGE_EMOJIS_AND_STICKERS permission. + /// + /// + public SocketGuildUser Author + => this.AuthorId.HasValue ? Guild.GetUser(this.AuthorId.Value) : null; + + /// + /// Gets the guild the sticker lives in. + /// + public SocketGuild Guild { get; } + + /// + public ulong? AuthorId { get; set; } + + internal SocketCustomSticker(DiscordSocketClient client, ulong id, SocketGuild guild, ulong? authorId = null) + : base(client, id) + { + this.Guild = guild; + this.AuthorId = authorId; + } + + internal static SocketCustomSticker Create(DiscordSocketClient client, Model model, SocketGuild guild, ulong? authorId = null) + { + var entity = new SocketCustomSticker(client, model.Id, guild, authorId); + entity.Update(model); + return entity; + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + if(!Guild.CurrentUser.GuildPermissions.Has(GuildPermission.ManageEmojisAndStickers)) + throw new InvalidOperationException($"Missing permission {nameof(GuildPermission.ManageEmojisAndStickers)}"); + + var model = await GuildHelper.ModifyStickerAsync(this.Discord, this.Guild, this, func, options); + + this.Update(model); + } + + /// + public async Task DeleteAsync(RequestOptions options = null) + { + await GuildHelper.DeleteStickerAsync(Discord, Guild, this, options); + Guild.RemoveSticker(this.Id); + } + + // ICustomSticker + ulong? ICustomSticker.AuthorId + => this.AuthorId; + + IGuild ICustomSticker.Guild + => this.Guild; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSticker.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSticker.cs new file mode 100644 index 000000000..4b4f6a605 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSticker.cs @@ -0,0 +1,68 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Sticker; + +namespace Discord.WebSocket +{ + public class SocketSticker : SocketEntity, ISticker + { + /// + public ulong PackId { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + public string Asset { get; private set; } + + /// + public string PreviewAsset { get; private set; } + + /// + public StickerFormatType FormatType { get; private set; } + + /// + public string GetStickerUrl() + => CDN.GetStickerUrl(this.Id, this.FormatType); + + internal SocketSticker(DiscordSocketClient client, ulong id) + : base(client, id) { } + + internal static SocketSticker Create(DiscordSocketClient client, Model model) + { + var entity = new SocketSticker(client, model.Id); + entity.Update(model); + return entity; + } + + internal virtual void Update(Model model) + { + this.Name = model.Name; + this.Description = model.Desription; + this.PackId = model.PackId; + this.Asset = model.Asset; + this.PreviewAsset = model.PreviewAsset; + this.FormatType = model.FormatType; + + if (model.Tags.IsSpecified) + { + this.Tags = model.Tags.Value.Split(',').Select(x => x.Trim()).ToImmutableArray(); + } + else + { + this.Tags = ImmutableArray.Empty; + } + } + } +}