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;
+ }
+ }
+ }
+}