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