| @@ -177,6 +177,34 @@ namespace Discord | |||
| public static string GetSpotifyDirectUrl(string 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) | |||
| { | |||
| if (format == ImageFormat.Auto) | |||
| @@ -199,6 +199,13 @@ namespace Discord | |||
| /// </returns> | |||
| IReadOnlyCollection<GuildEmote> Emotes { get; } | |||
| /// <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. | |||
| /// </summary> | |||
| /// <returns> | |||
| @@ -942,6 +949,52 @@ namespace Discord | |||
| /// </returns> | |||
| 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> | |||
| /// Gets this guilds slash commands commands | |||
| /// </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.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| @@ -63,5 +65,10 @@ namespace Discord | |||
| /// A <see cref="StickerFormatType"/> with the format type of this sticker. | |||
| /// </returns> | |||
| 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; } | |||
| [JsonProperty("nsfw_level")] | |||
| 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; } | |||
| [JsonProperty("components")] | |||
| 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; } | |||
| [JsonProperty("format_type")] | |||
| 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); | |||
| } | |||
| // 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) | |||
| { | |||
| 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 | |||
| { | |||
| 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) | |||
| => 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.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); | |||
| } | |||
| @@ -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 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<ulong, SocketGuildUser> _members; | |||
| private ConcurrentDictionary<ulong, SocketRole> _roles; | |||
| private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | |||
| private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private ImmutableArray<string> _features; | |||
| private AudioClient _audioClient; | |||
| #pragma warning restore IDISP002, IDISP006 | |||
| @@ -322,6 +325,11 @@ namespace Discord.WebSocket | |||
| } | |||
| /// <inheritdoc /> | |||
| 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 /> | |||
| public IReadOnlyCollection<string> Features => _features; | |||
| /// <summary> | |||
| @@ -440,6 +448,8 @@ namespace Discord.WebSocket | |||
| } | |||
| _voiceStates = voiceStates; | |||
| _syncPromise = new TaskCompletionSource<bool>(); | |||
| _downloaderPromise = new TaskCompletionSource<bool>(); | |||
| var _ = _syncPromise.TrySetResultAsync(true); | |||
| @@ -509,6 +519,23 @@ namespace Discord.WebSocket | |||
| } | |||
| } | |||
| _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 | |||
| { | |||
| @@ -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 | |||
| /// <inheritdoc /> | |||
| 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) | |||
| => 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 | |||
| internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | |||
| { | |||
| @@ -1332,6 +1472,8 @@ namespace Discord.WebSocket | |||
| int? IGuild.ApproximateMemberCount => null; | |||
| /// <inheritdoc /> | |||
| int? IGuild.ApproximatePresenceCount => null; | |||
| /// <inheritdoc /> | |||
| IReadOnlyCollection<ICustomSticker> IGuild.Stickers => Stickers; | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) | |||
| @@ -1481,6 +1623,13 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IApplicationCommand>> IGuild.GetApplicationCommandsAsync (RequestOptions options) | |||
| => 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() | |||
| { | |||
| @@ -1489,6 +1638,6 @@ namespace Discord.WebSocket | |||
| _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; | |||
| } | |||
| } | |||
| } | |||
| } | |||