@@ -177,6 +177,34 @@ namespace Discord | |||||
public static string GetSpotifyDirectUrl(string trackId) | public static string GetSpotifyDirectUrl(string trackId) | ||||
=> $"https://open.spotify.com/track/{trackId}"; | => $"https://open.spotify.com/track/{trackId}"; | ||||
/// <summary> | |||||
/// Gets a stickers url based off the id and format. | |||||
/// </summary> | |||||
/// <param name="stickerId">The id of the sticker.</param> | |||||
/// <param name="format">The format of the sticker</param> | |||||
/// <returns> | |||||
/// A URL to the sticker. | |||||
/// </returns> | |||||
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) | private static string FormatToExtension(ImageFormat format, string imageId) | ||||
{ | { | ||||
if (format == ImageFormat.Auto) | if (format == ImageFormat.Auto) | ||||
@@ -199,6 +199,13 @@ namespace Discord | |||||
/// </returns> | /// </returns> | ||||
IReadOnlyCollection<GuildEmote> Emotes { get; } | IReadOnlyCollection<GuildEmote> Emotes { get; } | ||||
/// <summary> | /// <summary> | ||||
/// Gets a collection of all custom stickers for this guild. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// A read-only collection of all custom stickers for this guild. | |||||
/// </returns> | |||||
IReadOnlyCollection<ICustomSticker> Stickers { get; } | |||||
/// <summary> | |||||
/// Gets a collection of all extra features added to this guild. | /// Gets a collection of all extra features added to this guild. | ||||
/// </summary> | /// </summary> | ||||
/// <returns> | /// <returns> | ||||
@@ -942,6 +949,52 @@ namespace Discord | |||||
/// </returns> | /// </returns> | ||||
Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); | Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); | ||||
/// <summary> | |||||
/// Creates a new sticker in this guild. | |||||
/// </summary> | |||||
/// <param name="name">The name of the sticker.</param> | |||||
/// <param name="description">The description of the sticker.</param> | |||||
/// <param name="tags">The tags of the sticker.</param> | |||||
/// <param name="image">The image of the new emote.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous creation operation. The task result contains the created sticker. | |||||
/// </returns> | |||||
Task<ICustomSticker> CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Gets a specific sticker within this guild. | |||||
/// </summary> | |||||
/// <param name="id">The id of the sticker to get.</param> | |||||
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous get operation. The task result contains the sticker found with the | |||||
/// specified <paramref name="id"/>; <see langword="null" /> if none is found. | |||||
/// </returns> | |||||
Task<ICustomSticker> GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Gets a collection of all stickers within this guild. | |||||
/// </summary> | |||||
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection | |||||
/// of stickers found within the guild. | |||||
/// </returns> | |||||
Task<IReadOnlyCollection<ICustomSticker>> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Deletes a sticker within this guild. | |||||
/// </summary> | |||||
/// <param name="sticker">The sticker to delete.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous removal operation. | |||||
/// </returns> | |||||
Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); | |||||
/// <summary> | /// <summary> | ||||
/// Gets this guilds slash commands commands | /// Gets this guilds slash commands commands | ||||
/// </summary> | /// </summary> | ||||
@@ -0,0 +1,62 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord | |||||
{ | |||||
/// <summary> | |||||
/// Represents a custom sticker within a guild. | |||||
/// </summary> | |||||
public interface ICustomSticker : ISticker | |||||
{ | |||||
/// <summary> | |||||
/// Gets the users id who uploaded the sticker. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// In order to get the author id, the bot needs the MANAGE_EMOJIS_AND_STICKERS permission. | |||||
/// </remarks> | |||||
ulong? AuthorId { get; } | |||||
/// <summary> | |||||
/// Gets the guild that this custom sticker is in. | |||||
/// </summary> | |||||
IGuild Guild { get; } | |||||
/// <summary> | |||||
/// Modifies this sticker. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// This method modifies this sticker with the specified properties. To see an example of this | |||||
/// method and what properties are available, please refer to <see cref="StickerProperties"/>. | |||||
/// <br/> | |||||
/// <br/> | |||||
/// The bot needs the MANAGE_EMOJIS_AND_STICKERS permission within the guild in order to modify stickers. | |||||
/// </remarks> | |||||
/// <example> | |||||
/// <para>The following example replaces the name of the sticker with <c>kekw</c>.</para> | |||||
/// <code language="cs"> | |||||
/// await sticker.ModifyAsync(x => x.Name = "kekw"); | |||||
/// </code> | |||||
/// </example> | |||||
/// <param name="func">A delegate containing the properties to modify the sticker with.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous modification operation. | |||||
/// </returns> | |||||
Task ModifyAsync(Action<StickerProperties> func, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Deletes the current sticker. | |||||
/// </summary> | |||||
/// <remakrs> | |||||
/// The bot neeeds the MANAGE_EMOJIS_AND_STICKERS permission inside the guild in order to delete stickers. | |||||
/// </remakrs> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous deletion operation. | |||||
/// </returns> | |||||
Task DeleteAsync(RequestOptions options = null); | |||||
} | |||||
} |
@@ -1,4 +1,6 @@ | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Threading.Tasks; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -63,5 +65,10 @@ namespace Discord | |||||
/// A <see cref="StickerFormatType"/> with the format type of this sticker. | /// A <see cref="StickerFormatType"/> with the format type of this sticker. | ||||
/// </returns> | /// </returns> | ||||
StickerFormatType FormatType { get; } | StickerFormatType FormatType { get; } | ||||
/// <summary> | |||||
/// Gets the image url for this sticker. | |||||
/// </summary> | |||||
string GetStickerUrl(); | |||||
} | } | ||||
} | } |
@@ -0,0 +1,29 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord | |||||
{ | |||||
/// <summary> | |||||
/// Represents a class used to modify stickers. | |||||
/// </summary> | |||||
public class StickerProperties | |||||
{ | |||||
/// <summary> | |||||
/// Gets or sets the name of the sticker. | |||||
/// </summary> | |||||
public Optional<string> Name { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the description of the sticker. | |||||
/// </summary> | |||||
public Optional<string> Description { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the tags of the sticker. | |||||
/// </summary> | |||||
public Optional<IEnumerable<string>> Tags { get; set; } | |||||
} | |||||
} |
@@ -80,5 +80,7 @@ namespace Discord.API | |||||
public Optional<Channel[]> Threads { get; set; } | public Optional<Channel[]> Threads { get; set; } | ||||
[JsonProperty("nsfw_level")] | [JsonProperty("nsfw_level")] | ||||
public NsfwLevel NsfwLevel { get; set; } | public NsfwLevel NsfwLevel { get; set; } | ||||
[JsonProperty("stickers")] | |||||
public Sticker[] Stickers { get; set; } | |||||
} | } | ||||
} | } |
@@ -60,7 +60,7 @@ namespace Discord.API | |||||
public Optional<Message> ReferencedMessage { get; set; } | public Optional<Message> ReferencedMessage { get; set; } | ||||
[JsonProperty("components")] | [JsonProperty("components")] | ||||
public Optional<API.ActionRowComponent[]> Components { get; set; } | public Optional<API.ActionRowComponent[]> Components { get; set; } | ||||
[JsonProperty("stickers")] | |||||
public Optional<Sticker[]> Stickers { get; set; } | |||||
[JsonProperty("sticker_items")] | |||||
public Optional<StickerItem[]> StickerItems { get; set; } | |||||
} | } | ||||
} | } |
@@ -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<StickerPack> StickerPacks { get; set; } | |||||
} | |||||
} |
@@ -21,5 +21,7 @@ namespace Discord.API | |||||
public string PreviewAsset { get; set; } | public string PreviewAsset { get; set; } | ||||
[JsonProperty("format_type")] | [JsonProperty("format_type")] | ||||
public StickerFormatType FormatType { get; set; } | public StickerFormatType FormatType { get; set; } | ||||
[JsonProperty("user")] | |||||
public Optional<User> User { get; set; } | |||||
} | } | ||||
} | } |
@@ -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; } | |||||
} | |||||
} |
@@ -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<ulong> CoverStickerId { get; set; } | |||||
[JsonProperty("description")] | |||||
public string Description { get; set; } | |||||
[JsonProperty("banner_asset_id")] | |||||
public ulong BannerAssetId { get; set; } | |||||
} | |||||
} |
@@ -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<string, object> ToDictionary() | |||||
{ | |||||
var d = new Dictionary<string, object>(); | |||||
d["file"] = new MultipartFile(File, Name); | |||||
d["name"] = Name; | |||||
d["description"] = Description; | |||||
d["tags"] = Tags; | |||||
return d; | |||||
} | |||||
} | |||||
} |
@@ -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<string> Name { get; set; } | |||||
[JsonProperty("description")] | |||||
public Optional<string> Description { get; set; } | |||||
[JsonProperty("tags")] | |||||
public Optional<string> Tags { get; set; } | |||||
} | |||||
} |
@@ -886,6 +886,67 @@ namespace Discord.API | |||||
return await SendJsonAsync<Message>("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | return await SendJsonAsync<Message>("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | ||||
} | } | ||||
// Stickers | |||||
public async Task<Sticker> GetStickerAsync(ulong id, RequestOptions options = null) | |||||
{ | |||||
Preconditions.NotEqual(id, 0, nameof(id)); | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await NullifyNotFound(SendAsync<Sticker>("GET", () => $"stickers/{id}", new BucketIds(), options: options)).ConfigureAwait(false); | |||||
} | |||||
public async Task<Sticker> 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<Sticker>("GET", () => $"guilds/{guildId}/stickers/{id}", new BucketIds(guildId), options: options)).ConfigureAwait(false); | |||||
} | |||||
public async Task<Sticker[]> ListGuildStickersAsync(ulong guildId, RequestOptions options = null) | |||||
{ | |||||
Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await SendAsync<Sticker[]>("GET", () => $"guilds/{guildId}/stickers", new BucketIds(guildId), options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<NitroStickerPacks> ListNitroStickerPacksAsync(RequestOptions options = null) | |||||
{ | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await SendAsync<NitroStickerPacks>("GET", () => $"sticker-packs", new BucketIds(), options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<Sticker> 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<Sticker>("POST", () => $"guilds/{guildId}/stickers", args.ToDictionary(), new BucketIds(guildId), options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<Sticker> 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<Sticker>("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) | public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) | ||||
{ | { | ||||
Preconditions.NotEqual(channelId, 0, nameof(channelId)); | Preconditions.NotEqual(channelId, 0, nameof(channelId)); | ||||
@@ -2002,6 +2063,32 @@ namespace Discord.API | |||||
} | } | ||||
} | } | ||||
protected async Task<T> NullifyNotFound<T>(Task<T> 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 | internal class BucketIds | ||||
{ | { | ||||
public ulong GuildId { get; internal set; } | public ulong GuildId { get; internal set; } | ||||
@@ -535,5 +535,52 @@ namespace Discord.Rest | |||||
} | } | ||||
public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) | public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) | ||||
=> client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); | => client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); | ||||
public static async Task<API.Sticker> CreateStickerAsync(BaseDiscordClient client, IGuild guild, string name, string description, IEnumerable<string> 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<API.Sticker> ModifyStickerAsync(BaseDiscordClient client, IGuild guild, ISticker sticker, Action<StickerProperties> 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<string>.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); | |||||
} | } | ||||
} | } |
@@ -1,5 +1,6 @@ | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Linq; | |||||
using Model = Discord.API.Sticker; | using Model = Discord.API.Sticker; | ||||
namespace Discord | namespace Discord | ||||
@@ -39,7 +40,7 @@ namespace Discord | |||||
internal static Sticker Create(Model model) | internal static Sticker Create(Model model) | ||||
{ | { | ||||
return new Sticker(model.Id, model.PackId, model.Name, model.Desription, | 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); | model.Asset, model.PreviewAsset, model.FormatType); | ||||
} | } | ||||
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// Represents a partial sticker received in a message. | |||||
/// </summary> | |||||
public class StickerItem : RestEntity<ulong> | |||||
{ | |||||
/// <summary> | |||||
/// The name of this sticker. | |||||
/// </summary> | |||||
public readonly string Name; | |||||
/// <summary> | |||||
/// The format of this sticker. | |||||
/// </summary> | |||||
public readonly StickerFormatType Format; | |||||
internal StickerItem(BaseDiscordClient client, Model model) | |||||
: base(client, model.Id) | |||||
{ | |||||
this.Name = model.Name; | |||||
this.Format = model.FormatType; | |||||
} | |||||
/// <summary> | |||||
/// Resolves this sticker item by fetching the <see cref="Sticker"/> from the API. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// A task representing the download operation, the result of the task is a sticker object. | |||||
/// </returns> | |||||
public async Task<Sticker> ResolveStickerAsync() | |||||
{ | |||||
var model = await Discord.ApiClient.GetStickerAsync(this.Id); | |||||
return Sticker.Create(model); | |||||
} | |||||
} | |||||
} |
@@ -19,6 +19,7 @@ using PresenceModel = Discord.API.Presence; | |||||
using RoleModel = Discord.API.Role; | using RoleModel = Discord.API.Role; | ||||
using UserModel = Discord.API.User; | using UserModel = Discord.API.User; | ||||
using VoiceStateModel = Discord.API.VoiceState; | using VoiceStateModel = Discord.API.VoiceState; | ||||
using StickerModel = Discord.API.Sticker; | |||||
namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
{ | { | ||||
@@ -36,7 +37,9 @@ namespace Discord.WebSocket | |||||
private ConcurrentDictionary<ulong, SocketGuildUser> _members; | private ConcurrentDictionary<ulong, SocketGuildUser> _members; | ||||
private ConcurrentDictionary<ulong, SocketRole> _roles; | private ConcurrentDictionary<ulong, SocketRole> _roles; | ||||
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | ||||
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | |||||
private ImmutableArray<GuildEmote> _emotes; | private ImmutableArray<GuildEmote> _emotes; | ||||
private ImmutableArray<string> _features; | private ImmutableArray<string> _features; | ||||
private AudioClient _audioClient; | private AudioClient _audioClient; | ||||
#pragma warning restore IDISP002, IDISP006 | #pragma warning restore IDISP002, IDISP006 | ||||
@@ -322,6 +325,11 @@ namespace Discord.WebSocket | |||||
} | } | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public IReadOnlyCollection<GuildEmote> Emotes => _emotes; | public IReadOnlyCollection<GuildEmote> Emotes => _emotes; | ||||
/// <summary> | |||||
/// Gets a collection of all custom stickers for this guild. | |||||
/// </summary> | |||||
public IReadOnlyCollection<SocketCustomSticker> Stickers | |||||
=> _stickers.Select(x => x.Value).ToImmutableArray(); | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public IReadOnlyCollection<string> Features => _features; | public IReadOnlyCollection<string> Features => _features; | ||||
/// <summary> | /// <summary> | ||||
@@ -440,6 +448,8 @@ namespace Discord.WebSocket | |||||
} | } | ||||
_voiceStates = voiceStates; | _voiceStates = voiceStates; | ||||
_syncPromise = new TaskCompletionSource<bool>(); | _syncPromise = new TaskCompletionSource<bool>(); | ||||
_downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
var _ = _syncPromise.TrySetResultAsync(true); | var _ = _syncPromise.TrySetResultAsync(true); | ||||
@@ -509,6 +519,23 @@ namespace Discord.WebSocket | |||||
} | } | ||||
} | } | ||||
_roles = roles; | _roles = roles; | ||||
if (model.Stickers != null) | |||||
{ | |||||
var stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(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<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | |||||
} | } | ||||
/*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related | /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related | ||||
{ | { | ||||
@@ -898,6 +925,33 @@ namespace Discord.WebSocket | |||||
return role; | 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 | //Users | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null) | public Task<RestGuildUser> AddGuildUserAsync(ulong id, string accessToken, Action<AddGuildUserProperties> func = null, RequestOptions options = null) | ||||
@@ -1109,6 +1163,92 @@ namespace Discord.WebSocket | |||||
public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) | public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) | ||||
=> GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); | => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); | ||||
//Stickers | |||||
/// <summary> | |||||
/// Gets a specific sticker within this guild. | |||||
/// </summary> | |||||
/// <param name="id">The id of the sticker to get.</param> | |||||
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous get operation. The task result contains the sticker found with the | |||||
/// specified <paramref name="id"/>; <see langword="null" /> if none is found. | |||||
/// </returns> | |||||
public async ValueTask<SocketCustomSticker> 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); | |||||
} | |||||
/// <summary> | |||||
/// Gets a collection of all stickers within this guild. | |||||
/// </summary> | |||||
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous get operation. The task result contains a read-only collection | |||||
/// of stickers found within the guild. | |||||
/// </returns> | |||||
public async ValueTask<IReadOnlyCollection<SocketCustomSticker>> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, | |||||
RequestOptions options = null) | |||||
{ | |||||
if (this.Stickers.Count > 0) | |||||
return this.Stickers; | |||||
if (mode == CacheMode.CacheOnly) | |||||
return ImmutableArray.Create<SocketCustomSticker>(); | |||||
var models = await Discord.ApiClient.ListGuildStickersAsync(this.Id, options).ConfigureAwait(false); | |||||
List<SocketCustomSticker> stickers = new(); | |||||
foreach (var model in models) | |||||
{ | |||||
stickers.Add(AddOrUpdateSticker(model)); | |||||
} | |||||
return stickers; | |||||
} | |||||
/// <summary> | |||||
/// Creates a new sticker in this guild. | |||||
/// </summary> | |||||
/// <param name="name">The name of the sticker.</param> | |||||
/// <param name="description">The description of the sticker.</param> | |||||
/// <param name="tags">The tags of the sticker.</param> | |||||
/// <param name="image">The image of the new emote.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous creation operation. The task result contains the created sticker. | |||||
/// </returns> | |||||
public async Task<SocketCustomSticker> CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, | |||||
RequestOptions options = null) | |||||
{ | |||||
var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, image, options).ConfigureAwait(false); | |||||
return AddOrUpdateSticker(model); | |||||
} | |||||
/// <summary> | |||||
/// Deletes a sticker within this guild. | |||||
/// </summary> | |||||
/// <param name="sticker">The sticker to delete.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous removal operation. | |||||
/// </returns> | |||||
public Task DeleteStickerAsync(SocketCustomSticker sticker, RequestOptions options = null) | |||||
=> sticker.DeleteAsync(options); | |||||
//Voice States | //Voice States | ||||
internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | ||||
{ | { | ||||
@@ -1332,6 +1472,8 @@ namespace Discord.WebSocket | |||||
int? IGuild.ApproximateMemberCount => null; | int? IGuild.ApproximateMemberCount => null; | ||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
int? IGuild.ApproximatePresenceCount => null; | int? IGuild.ApproximatePresenceCount => null; | ||||
/// <inheritdoc /> | |||||
IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers; | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) | async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) | ||||
@@ -1481,6 +1623,13 @@ namespace Discord.WebSocket | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | ||||
=> await GetApplicationCommandsAsync(options).ConfigureAwait(false); | => await GetApplicationCommandsAsync(options).ConfigureAwait(false); | ||||
async Task<ICustomSticker> IGuild.CreateStickerAsync(string name, string description, IEnumerable<string> tags, Image image, RequestOptions options) | |||||
=> await CreateStickerAsync(name, description, tags, image, options); | |||||
async Task<ICustomSticker> IGuild.GetStickerAsync(ulong id, CacheMode mode, RequestOptions options) | |||||
=> await GetStickerAsync(id, mode, options); | |||||
async Task<IReadOnlyCollection<ICustomSticker>> IGuild.GetStickersAsync(CacheMode mode, RequestOptions options) | |||||
=> await GetStickersAsync(mode, options); | |||||
Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) => throw new NotImplementedException(); | |||||
void IDisposable.Dispose() | void IDisposable.Dispose() | ||||
{ | { | ||||
@@ -1489,6 +1638,6 @@ namespace Discord.WebSocket | |||||
_audioClient?.Dispose(); | _audioClient?.Dispose(); | ||||
} | } | ||||
} | } | ||||
} | } |
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// Gets the user that uploaded the guild sticker. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// <note> | |||||
/// This may return <see langword="null"/> in the WebSocket implementation due to incomplete user collection in | |||||
/// large guilds, or the bot doesnt have the MANAGE_EMOJIS_AND_STICKERS permission. | |||||
/// </note> | |||||
/// </remarks> | |||||
public SocketGuildUser Author | |||||
=> this.AuthorId.HasValue ? Guild.GetUser(this.AuthorId.Value) : null; | |||||
/// <summary> | |||||
/// Gets the guild the sticker lives in. | |||||
/// </summary> | |||||
public SocketGuild Guild { get; } | |||||
/// <inheritdoc/> | |||||
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; | |||||
} | |||||
/// <inheritdoc/> | |||||
public async Task ModifyAsync(Action<StickerProperties> 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); | |||||
} | |||||
/// <inheritdoc/> | |||||
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; | |||||
} | |||||
} |
@@ -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<ulong>, ISticker | |||||
{ | |||||
/// <inheritdoc/> | |||||
public ulong PackId { get; private set; } | |||||
/// <inheritdoc/> | |||||
public string Name { get; private set; } | |||||
/// <inheritdoc/> | |||||
public string Description { get; private set; } | |||||
/// <inheritdoc/> | |||||
public IReadOnlyCollection<string> Tags { get; private set; } | |||||
/// <inheritdoc/> | |||||
public string Asset { get; private set; } | |||||
/// <inheritdoc/> | |||||
public string PreviewAsset { get; private set; } | |||||
/// <inheritdoc/> | |||||
public StickerFormatType FormatType { get; private set; } | |||||
/// <inheritdoc/> | |||||
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<string>.Empty; | |||||
} | |||||
} | |||||
} | |||||
} |