diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs
index 6957c02f..8686c845 100644
--- a/src/Discord.Net.Core/DiscordConfig.cs
+++ b/src/Discord.Net.Core/DiscordConfig.cs
@@ -237,5 +237,10 @@ namespace Discord
/// Returns the max amount of tags applied to an application.
///
public const int MaxApplicationTagCount = 5;
+
+ ///
+ /// Returns the maximum number of entitlements that can be gotten per-batch.
+ ///
+ public const int MaxEntitlementsPerBatch = 100;
}
}
diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/EntitlementType.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/EntitlementType.cs
new file mode 100644
index 00000000..6dac3676
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AppSubscriptions/EntitlementType.cs
@@ -0,0 +1,9 @@
+namespace Discord;
+
+public enum EntitlementType
+{
+ ///
+ /// The entitlement was purchased as an app subscription.
+ ///
+ ApplicationSubscription = 8
+}
diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/IEntitlement.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/IEntitlement.cs
new file mode 100644
index 00000000..19b2276c
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AppSubscriptions/IEntitlement.cs
@@ -0,0 +1,61 @@
+using System;
+
+namespace Discord;
+
+public interface IEntitlement : ISnowflakeEntity
+{
+ ///
+ /// Gets the ID of the SKU this entitlement is for.
+ ///
+ ulong SkuId { get; }
+
+ ///
+ /// Gets the ID of the user that is granted access to the entitlement's SKU.
+ ///
+ ///
+ /// if the entitlement is for a guild.
+ ///
+ ulong? UserId { get; }
+
+ ///
+ /// Gets the ID of the guild that is granted access to the entitlement's SKU.
+ ///
+ ///
+ /// if the entitlement is for a user.
+ ///
+ ulong? GuildId { get; }
+
+ ///
+ /// Gets the ID of the parent application.
+ ///
+ ulong ApplicationId { get; }
+
+ ///
+ /// Gets the type of the entitlement.
+ ///
+ EntitlementType Type { get; }
+
+ ///
+ /// Gets whether this entitlement has been consumed.
+ ///
+ ///
+ /// Not applicable for App Subscriptions.
+ ///
+ bool IsConsumed { get; }
+
+ ///
+ /// Gets the start date at which the entitlement is valid.
+ ///
+ ///
+ /// when using test entitlements.
+ ///
+ DateTimeOffset? StartsAt { get; }
+
+ ///
+ /// Gets the end date at which the entitlement is no longer valid.
+ ///
+ ///
+ /// when using test entitlements.
+ ///
+ DateTimeOffset? EndsAt { get; }
+}
diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs
new file mode 100644
index 00000000..338b14ea
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SKU.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Discord;
+
+public struct SKU : ISnowflakeEntity
+{
+ ///
+ public ulong Id { get; }
+
+ ///
+ public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
+
+ ///
+ /// Gets the type of the SKU.
+ ///
+ public SKUType Type { get; }
+
+ ///
+ /// Gets the ID of the parent application.
+ ///
+ public ulong ApplicationId { get; }
+
+ ///
+ /// Gets the customer-facing name of your premium offering.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the system-generated URL slug based on the SKU's name.
+ ///
+ public string Slug { get; }
+
+ internal SKU(ulong id, SKUType type, ulong applicationId, string name, string slug)
+ {
+ Id = id;
+ Type = type;
+ ApplicationId = applicationId;
+ Name = name;
+ Slug = slug;
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs
new file mode 100644
index 00000000..d748bf7b
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SKUFlags.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Discord;
+
+[Flags]
+public enum SKUFlags
+{
+ GuildSubscription = 1 << 7,
+
+ UserSubscription = 1 << 8
+}
diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SKUType.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SKUType.cs
new file mode 100644
index 00000000..a15b6c59
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SKUType.cs
@@ -0,0 +1,14 @@
+namespace Discord;
+
+public enum SKUType
+{
+ ///
+ /// Represents a recurring subscription.
+ ///
+ Subscription = 5,
+
+ ///
+ /// System-generated group for each SKU created.
+ ///
+ SubscriptionGroup = 6,
+}
diff --git a/src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionOwnerType.cs b/src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionOwnerType.cs
new file mode 100644
index 00000000..eb042649
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AppSubscriptions/SubscriptionOwnerType.cs
@@ -0,0 +1,14 @@
+namespace Discord;
+
+public enum SubscriptionOwnerType
+{
+ ///
+ /// The owner of the application subscription is a guild.
+ ///
+ Guild = 1,
+
+ ///
+ /// The owner of the application subscription is a user.
+ ///
+ User = 2,
+}
diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
index fca9b769..9519c30d 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
@@ -91,6 +91,11 @@ namespace Discord
///
ulong ApplicationId { get; }
+ ///
+ /// Gets entitlements for the invoking user.
+ ///
+ IReadOnlyCollection Entitlements { get; }
+
///
/// Responds to an Interaction with type .
///
@@ -368,5 +373,12 @@ namespace Discord
/// The request options for this request.
/// A task that represents the asynchronous operation of responding to the interaction.
Task RespondWithModalAsync(Modal modal, RequestOptions options = null);
+
+ ///
+ /// Responds to the interaction with an ephemeral message the invoking user,
+ /// instructing them that whatever they tried to do requires the premium benefits of your app.
+ ///
+ /// A task that represents the asynchronous operation of responding to the interaction.
+ Task RespondWithPremiumRequiredAsync(RequestOptions options = null);
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
index b0c2384e..b52d6b04 100644
--- a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
@@ -47,5 +47,10 @@ namespace Discord
/// Respond by showing the user a modal.
///
Modal = 9,
+
+ ///
+ /// Respond to an interaction with an upgrade button, only available for apps with monetization enabled.
+ ///
+ PremiumRequired = 10
}
}
diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs
index ddf8c32d..a87a25c7 100644
--- a/src/Discord.Net.Core/IDiscordClient.cs
+++ b/src/Discord.Net.Core/IDiscordClient.cs
@@ -327,5 +327,27 @@ namespace Discord
/// that represents the gateway information related to the bot.
///
Task GetBotGatewayAsync(RequestOptions options = null);
+
+ ///
+ /// Creates a test entitlement to a given SKU for a given guild or user.
+ ///
+ Task CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options = null);
+
+ ///
+ /// Deletes a currently-active test entitlement.
+ ///
+ Task DeleteTestEntitlementAsync(ulong entitlementId, RequestOptions options = null);
+
+ ///
+ /// Returns all entitlements for a given app, active and expired.
+ ///
+ IAsyncEnumerable> GetEntitlementsAsync(int? limit = 100,
+ ulong? afterId = null, ulong? beforeId = null, bool excludeEnded = false, ulong? guildId = null, ulong? userId = null,
+ ulong[] skuIds = null, RequestOptions options = null);
+
+ ///
+ /// Returns all SKUs for a given application.
+ ///
+ Task> GetSKUsAsync(RequestOptions options = null);
}
}
diff --git a/src/Discord.Net.Interactions/InteractionModuleBase.cs b/src/Discord.Net.Interactions/InteractionModuleBase.cs
index 41857582..76b2da25 100644
--- a/src/Discord.Net.Interactions/InteractionModuleBase.cs
+++ b/src/Discord.Net.Interactions/InteractionModuleBase.cs
@@ -123,6 +123,10 @@ namespace Discord.Interactions
protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where TModal : class, IModal
=> await Context.Interaction.RespondWithModalAsync(customId, options);
+ ///
+ protected virtual Task RespondWithPremiumRequiredAsync(RequestOptions options = null)
+ => Context.Interaction.RespondWithPremiumRequiredAsync(options);
+
//IInteractionModuleBase
///
diff --git a/src/Discord.Net.Rest/API/Common/Entitlement.cs b/src/Discord.Net.Rest/API/Common/Entitlement.cs
new file mode 100644
index 00000000..f12945e4
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/Entitlement.cs
@@ -0,0 +1,34 @@
+using Newtonsoft.Json;
+using System;
+
+namespace Discord.API;
+
+internal class Entitlement
+{
+ [JsonProperty("id")]
+ public ulong Id { get; set; }
+
+ [JsonProperty("sku_id")]
+ public ulong SkuId { get; set; }
+
+ [JsonProperty("user_id")]
+ public Optional UserId { get; set; }
+
+ [JsonProperty("guild_id")]
+ public Optional GuildId { get; set; }
+
+ [JsonProperty("application_id")]
+ public ulong ApplicationId { get; set; }
+
+ [JsonProperty("type")]
+ public EntitlementType Type { get; set; }
+
+ [JsonProperty("consumed")]
+ public bool IsConsumed { get; set; }
+
+ [JsonProperty("starts_at")]
+ public Optional StartsAt { get; set; }
+
+ [JsonProperty("ends_at")]
+ public Optional EndsAt { get; set; }
+}
diff --git a/src/Discord.Net.Rest/API/Common/Interaction.cs b/src/Discord.Net.Rest/API/Common/Interaction.cs
index 7ac81432..14148ee7 100644
--- a/src/Discord.Net.Rest/API/Common/Interaction.cs
+++ b/src/Discord.Net.Rest/API/Common/Interaction.cs
@@ -46,5 +46,8 @@ namespace Discord.API
[JsonProperty("guild_locale")]
public Optional GuildLocale { get; set; }
+
+ [JsonProperty("entitlements")]
+ public Entitlement[] Entitlements { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Common/SKU.cs b/src/Discord.Net.Rest/API/Common/SKU.cs
new file mode 100644
index 00000000..dd3bb483
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/SKU.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+
+namespace Discord.API;
+
+internal class SKU
+{
+ [JsonProperty("id")]
+ public ulong Id { get; set; }
+
+ [JsonProperty("type")]
+ public SKUType Type { get; set; }
+
+ [JsonProperty("application_id")]
+ public ulong ApplicationId { get; set; }
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonProperty("slug")]
+ public string Slug { get; set; }
+}
diff --git a/src/Discord.Net.Rest/API/Rest/CreateEntitlementParams.cs b/src/Discord.Net.Rest/API/Rest/CreateEntitlementParams.cs
new file mode 100644
index 00000000..e6443d0e
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Rest/CreateEntitlementParams.cs
@@ -0,0 +1,15 @@
+using Newtonsoft.Json;
+
+namespace Discord.API.Rest;
+
+internal class CreateEntitlementParams
+{
+ [JsonProperty("sku_id")]
+ public ulong SkuId { get; set; }
+
+ [JsonProperty("owner_id")]
+ public ulong OwnerId { get; set; }
+
+ [JsonProperty("owner_type")]
+ public SubscriptionOwnerType Type { get; set; }
+}
diff --git a/src/Discord.Net.Rest/API/Rest/ListEntitlementsParams.cs b/src/Discord.Net.Rest/API/Rest/ListEntitlementsParams.cs
new file mode 100644
index 00000000..6e569efc
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Rest/ListEntitlementsParams.cs
@@ -0,0 +1,18 @@
+namespace Discord.API.Rest;
+
+internal class ListEntitlementsParams
+{
+ public Optional UserId { get; set; }
+
+ public Optional SkuIds { get; set; }
+
+ public Optional BeforeId { get; set; }
+
+ public Optional AfterId { get; set; }
+
+ public Optional Limit { get; set; }
+
+ public Optional GuildId { get; set; }
+
+ public Optional ExcludeEnded { get; set; }
+}
diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs
index 2af84a9c..67a5baa7 100644
--- a/src/Discord.Net.Rest/BaseDiscordClient.cs
+++ b/src/Discord.Net.Rest/BaseDiscordClient.cs
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -258,6 +259,30 @@ namespace Discord.Rest
///
Task IDiscordClient.StopAsync()
=> Task.Delay(0);
+
+ ///
+ /// Creates a test entitlement to a given SKU for a given guild or user.
+ ///
+ Task IDiscordClient.CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options)
+ => Task.FromResult(null);
+
+ ///
+ /// Deletes a currently-active test entitlement.
+ ///
+ Task IDiscordClient.DeleteTestEntitlementAsync(ulong entitlementId, RequestOptions options)
+ => Task.CompletedTask;
+
+ ///
+ /// Returns all entitlements for a given app.
+ ///
+ IAsyncEnumerable> IDiscordClient.GetEntitlementsAsync(int? limit, ulong? afterId, ulong? beforeId,
+ bool excludeEnded, ulong? guildId, ulong? userId, ulong[] skuIds, RequestOptions options) => AsyncEnumerable.Empty>();
+
+ ///
+ /// Gets all SKUs for a given application.
+ ///
+ Task> IDiscordClient.GetSKUsAsync(RequestOptions options) => Task.FromResult>(Array.Empty());
+
#endregion
}
}
diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs
index 1fb7b38e..5c4ee019 100644
--- a/src/Discord.Net.Rest/ClientHelper.cs
+++ b/src/Discord.Net.Rest/ClientHelper.cs
@@ -379,6 +379,66 @@ namespace Discord.Rest
}
+ #endregion
+
+ #region App Subscriptions
+
+ public static async Task CreateTestEntitlementAsync(BaseDiscordClient client, ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType,
+ RequestOptions options = null)
+ {
+ var model = await client.ApiClient.CreateEntitlementAsync(new CreateEntitlementParams
+ {
+ Type = ownerType,
+ OwnerId = ownerId,
+ SkuId = skuId
+ }, options);
+
+ return RestEntitlement.Create(client, model);
+ }
+
+ public static IAsyncEnumerable> ListEntitlementsAsync(BaseDiscordClient client, int? limit = 100,
+ ulong? afterId = null, ulong? beforeId = null, bool excludeEnded = false, ulong? guildId = null, ulong? userId = null,
+ ulong[] skuIds = null, RequestOptions options = null)
+ {
+ return new PagedAsyncEnumerable(
+ DiscordConfig.MaxEntitlementsPerBatch,
+ async (info, ct) =>
+ {
+ var args = new ListEntitlementsParams()
+ {
+ Limit = info.PageSize,
+ BeforeId = beforeId ?? Optional.Unspecified,
+ ExcludeEnded = excludeEnded,
+ GuildId = guildId ?? Optional.Unspecified,
+ UserId = userId ?? Optional.Unspecified,
+ SkuIds = skuIds ?? Optional.Unspecified,
+ };
+ if (info.Position != null)
+ args.AfterId = info.Position.Value;
+ var models = await client.ApiClient.ListEntitlementAsync(args, options).ConfigureAwait(false);
+ return models
+ .Select(x => RestEntitlement.Create(client, x))
+ .ToImmutableArray();
+ },
+ nextPage: (info, lastPage) =>
+ {
+ if (lastPage.Count != DiscordConfig.MaxEntitlementsPerBatch)
+ return false;
+ info.Position = lastPage.Max(x => x.Id);
+ return true;
+ },
+ start: afterId,
+ count: limit
+ );
+ }
+
+ public static async Task> ListSKUsAsync(BaseDiscordClient client, RequestOptions options = null)
+ {
+ var models = await client.ApiClient.ListSKUsAsync(options).ConfigureAwait(false);
+
+ return models.Select(x => new SKU(x.Id, x.Type, x.ApplicationId, x.Name, x.Slug)).ToImmutableArray();
+ }
+
#endregion
}
}
diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs
index e9b0c157..4a6d6274 100644
--- a/src/Discord.Net.Rest/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs
@@ -7,6 +7,7 @@ using Discord.Net.Rest;
using Newtonsoft.Json;
using System;
+using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.Design;
@@ -2254,7 +2255,7 @@ namespace Discord.API
return await SendAsync("GET", () => $"guilds/{guildId}/onboarding", new BucketIds(guildId: guildId), options: options);
}
- public async Task ModifyGuildOnboardingAsync(ulong guildId, ModifyGuildOnboardingParams args, RequestOptions options)
+ public async Task ModifyGuildOnboardingAsync(ulong guildId, ModifyGuildOnboardingParams args, RequestOptions options)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
@@ -2689,5 +2690,55 @@ namespace Discord.API
=> await SendJsonAsync("PUT", () => $"users/@me/applications/{applicationId}/role-connection", connection, new BucketIds(), options: options);
#endregion
+
+ #region App Monetization
+
+ public async Task CreateEntitlementAsync(CreateEntitlementParams args, RequestOptions options = null)
+ => await SendJsonAsync("POST", () => $"applications/{CurrentApplicationId}/entitlements", args, new BucketIds(), options: options).ConfigureAwait(false);
+
+ public async Task DeleteEntitlementAsync(ulong entitlementId, RequestOptions options = null)
+ => await SendAsync("DELETE", () => $"applications/{CurrentApplicationId}/entitlements/{entitlementId}", new BucketIds(), options: options).ConfigureAwait(false);
+
+ public async Task ListEntitlementAsync(ListEntitlementsParams args, RequestOptions options = null)
+ {
+ var query = $"?limit={args.Limit.GetValueOrDefault(100)}";
+
+ if (args.UserId.IsSpecified)
+ {
+ query += $"&user_id={args.UserId.Value}";
+ }
+
+ if (args.SkuIds.IsSpecified)
+ {
+ query += $"&sku_ids={WebUtility.UrlEncode(string.Join(",", args.SkuIds.Value))}";
+ }
+
+ if (args.BeforeId.IsSpecified)
+ {
+ query += $"&before={args.BeforeId.Value}";
+ }
+
+ if (args.AfterId.IsSpecified)
+ {
+ query += $"&after={args.AfterId.Value}";
+ }
+
+ if (args.GuildId.IsSpecified)
+ {
+ query += $"&guild_id={args.GuildId.Value}";
+ }
+
+ if (args.ExcludeEnded.IsSpecified)
+ {
+ query += $"&exclude_ended={args.ExcludeEnded.Value}";
+ }
+
+ return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/entitlements{query}", new BucketIds(), options: options).ConfigureAwait(false);
+ }
+
+ public async Task ListSKUsAsync(RequestOptions options = null)
+ => await SendAsync("GET", () => $"applications/{CurrentApplicationId}/skus", new BucketIds(), options: options).ConfigureAwait(false);
+
+ #endregion
}
}
diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs
index de9f2313..4d78d678 100644
--- a/src/Discord.Net.Rest/DiscordRestClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestClient.cs
@@ -278,9 +278,30 @@ namespace Discord.Rest
public Task ModifyUserApplicationRoleConnectionAsync(ulong applicationId, RoleConnectionProperties roleConnection, RequestOptions options = null)
=> ClientHelper.ModifyUserRoleConnectionAsync(applicationId, roleConnection, this, options);
+ ///
+ public Task CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options = null)
+ => ClientHelper.CreateTestEntitlementAsync(this, skuId, ownerId, ownerType, options);
+
+ ///
+ public Task DeleteTestEntitlementAsync(ulong entitlementId, RequestOptions options = null)
+ => ApiClient.DeleteEntitlementAsync(entitlementId, options);
+
+ ///
+ public IAsyncEnumerable> GetEntitlementsAsync(int? limit = 100,
+ ulong? afterId = null, ulong? beforeId = null, bool excludeEnded = false, ulong? guildId = null, ulong? userId = null,
+ ulong[] skuIds = null, RequestOptions options = null)
+ => ClientHelper.ListEntitlementsAsync(this, limit, afterId, beforeId, excludeEnded, guildId, userId, skuIds, options);
+
+ ///
+ public Task> GetSKUsAsync(RequestOptions options = null)
+ => ClientHelper.ListSKUsAsync(this, options);
+
#endregion
#region IDiscordClient
+ async Task IDiscordClient.CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options)
+ => await CreateTestEntitlementAsync(skuId, ownerId, ownerType, options).ConfigureAwait(false);
+
///
async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> await GetApplicationInfoAsync(options).ConfigureAwait(false);
diff --git a/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs b/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs
new file mode 100644
index 00000000..e1d2b61e
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AppSubscriptions/RestEntitlement.cs
@@ -0,0 +1,66 @@
+using System;
+
+using Model = Discord.API.Entitlement;
+
+namespace Discord.Rest;
+
+public class RestEntitlement : RestEntity, IEntitlement
+{
+ ///
+ public DateTimeOffset CreatedAt { get; private set; }
+
+ ///
+ public ulong SkuId { get; private set; }
+
+ ///
+ public ulong? UserId { get; private set; }
+
+ ///
+ public ulong? GuildId { get; private set; }
+
+ ///
+ public ulong ApplicationId { get; private set; }
+
+ ///
+ public EntitlementType Type { get; private set; }
+
+ ///
+ public bool IsConsumed { get; private set; }
+
+ ///
+ public DateTimeOffset? StartsAt { get; private set; }
+
+ ///
+ public DateTimeOffset? EndsAt { get; private set; }
+
+ internal RestEntitlement(BaseDiscordClient discord, ulong id) : base(discord, id)
+ {
+ }
+
+ internal static RestEntitlement Create(BaseDiscordClient discord, Model model)
+ {
+ var entity = new RestEntitlement(discord, model.Id);
+ entity.Update(model);
+ return entity;
+ }
+
+ internal void Update(Model model)
+ {
+ SkuId = model.SkuId;
+ UserId = model.UserId.IsSpecified
+ ? model.UserId.Value
+ : null;
+ GuildId = model.GuildId.IsSpecified
+ ? model.GuildId.Value
+ : null;
+ ApplicationId = model.ApplicationId;
+ Type = model.Type;
+ IsConsumed = model.IsConsumed;
+ StartsAt = model.StartsAt.IsSpecified
+ ? model.StartsAt.Value
+ : null;
+ EndsAt = model.EndsAt.IsSpecified
+ ? model.EndsAt.Value
+ : null;
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
index d412fc41..a889aeac 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
@@ -522,6 +522,17 @@ namespace Discord.Rest
return client.ApiClient.CreateInteractionResponseAsync(apiArgs, interactionId, interactionToken, options);
}
+
+ public static async Task RespondWithPremiumRequiredAsync(BaseDiscordClient client, ulong interactionId,
+ string interactionToken, RequestOptions options = null)
+ {
+ await client.ApiClient.CreateInteractionResponseAsync(new InteractionResponse
+ {
+ Type = InteractionResponseType.PremiumRequired,
+ Data = Optional.Unspecified
+ }, interactionId, interactionToken, options);
+ }
+
#endregion
#region Guild permissions
diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs
index 70445f80..4860f60e 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs
@@ -2,7 +2,9 @@ using Discord.Net;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DataModel = Discord.API.ApplicationCommandInteractionData;
@@ -88,6 +90,9 @@ namespace Discord.Rest
///
public ulong ApplicationId { get; private set; }
+ ///
+ public IReadOnlyCollection Entitlements { get; private set; }
+
internal RestInteraction(BaseDiscordClient discord, ulong id)
: base(discord, id)
{
@@ -223,6 +228,8 @@ namespace Discord.Rest
GuildLocale = model.GuildLocale.IsSpecified
? model.GuildLocale.Value
: null;
+
+ Entitlements = model.Entitlements.Select(x => RestEntitlement.Create(discord, x)).ToImmutableArray();
}
internal string SerializePayload(object payload)
@@ -413,7 +420,14 @@ namespace Discord.Rest
public Task DeleteOriginalResponseAsync(RequestOptions options = null)
=> InteractionHelper.DeleteInteractionResponseAsync(Discord, this, options);
+ ///
+ public Task RespondWithPremiumRequiredAsync(RequestOptions options = null)
+ => InteractionHelper.RespondWithPremiumRequiredAsync(Discord, Id, Token, options);
+
#region IDiscordInteraction
+ ///
+ IReadOnlyCollection IDiscordInteraction.Entitlements => Entitlements;
+
///
IUser IDiscordInteraction.User => User;
diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
index 7ae8d6af..1109f4f7 100644
--- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
+++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
@@ -1,4 +1,5 @@
using Discord.Rest;
+
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -907,7 +908,7 @@ namespace Discord.WebSocket
internal readonly AsyncEvent> _auditLogCreated = new();
#endregion
-
+
#region AutoModeration
///
@@ -918,7 +919,7 @@ namespace Discord.WebSocket
add => _autoModRuleCreated.Add(value);
remove => _autoModRuleCreated.Remove(value);
}
- internal readonly AsyncEvent> _autoModRuleCreated = new ();
+ internal readonly AsyncEvent> _autoModRuleCreated = new();
///
/// Fired when an auto moderation rule is modified.
@@ -928,7 +929,7 @@ namespace Discord.WebSocket
add => _autoModRuleUpdated.Add(value);
remove => _autoModRuleUpdated.Remove(value);
}
- internal readonly AsyncEvent, SocketAutoModRule, Task>> _autoModRuleUpdated = new ();
+ internal readonly AsyncEvent, SocketAutoModRule, Task>> _autoModRuleUpdated = new();
///
/// Fired when an auto moderation rule is deleted.
@@ -938,7 +939,7 @@ namespace Discord.WebSocket
add => _autoModRuleDeleted.Add(value);
remove => _autoModRuleDeleted.Remove(value);
}
- internal readonly AsyncEvent> _autoModRuleDeleted = new ();
+ internal readonly AsyncEvent> _autoModRuleDeleted = new();
///
/// Fired when an auto moderation rule is triggered by a user.
@@ -948,8 +949,48 @@ namespace Discord.WebSocket
add => _autoModActionExecuted.Add(value);
remove => _autoModActionExecuted.Remove(value);
}
- internal readonly AsyncEvent> _autoModActionExecuted = new ();
-
+ internal readonly AsyncEvent> _autoModActionExecuted = new();
+
+ #endregion
+
+ #region App Subscriptions
+
+ ///
+ /// Fired when a user subscribes to a SKU.
+ ///
+ public event Func EntitlementCreated
+ {
+ add { _entitlementCreated.Add(value); }
+ remove { _entitlementCreated.Remove(value); }
+ }
+
+ internal readonly AsyncEvent> _entitlementCreated = new();
+
+
+ ///
+ /// Fired when a subscription to a SKU is updated.
+ ///
+ public event Func, SocketEntitlement, Task> EntitlementUpdated
+ {
+ add { _entitlementUpdated.Add(value); }
+ remove { _entitlementUpdated.Remove(value); }
+ }
+
+ internal readonly AsyncEvent, SocketEntitlement, Task>> _entitlementUpdated = new();
+
+
+ ///
+ /// Fired when a user's entitlement is deleted.
+ ///
+ public event Func, Task> EntitlementDeleted
+ {
+ add { _entitlementDeleted.Add(value); }
+ remove { _entitlementDeleted.Remove(value); }
+ }
+
+ internal readonly AsyncEvent, Task>> _entitlementDeleted = new();
+
+
#endregion
}
}
diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs
index c40ae3f9..745d5581 100644
--- a/src/Discord.Net.WebSocket/ClientState.cs
+++ b/src/Discord.Net.WebSocket/ClientState.cs
@@ -17,6 +17,7 @@ namespace Discord.WebSocket
private readonly ConcurrentDictionary _users;
private readonly ConcurrentHashSet _groupChannels;
private readonly ConcurrentDictionary _commands;
+ private readonly ConcurrentDictionary _entitlements;
internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection();
internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection();
@@ -24,6 +25,7 @@ namespace Discord.WebSocket
internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection();
internal IReadOnlyCollection Users => _users.ToReadOnlyCollection();
internal IReadOnlyCollection Commands => _commands.ToReadOnlyCollection();
+ internal IReadOnlyCollection Entitlements => _entitlements.ToReadOnlyCollection();
internal IReadOnlyCollection PrivateChannels =>
_dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat(
@@ -40,6 +42,7 @@ namespace Discord.WebSocket
_users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier));
_groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier));
_commands = new ConcurrentDictionary();
+ _entitlements = new();
}
internal SocketChannel GetChannel(ulong id)
@@ -170,5 +173,29 @@ namespace Discord.WebSocket
foreach (var id in ids)
_commands.TryRemove(id, out var _);
}
+
+ internal void AddEntitlement(ulong id, SocketEntitlement entitlement)
+ {
+ _entitlements.TryAdd(id, entitlement);
+ }
+
+ internal SocketEntitlement GetEntitlement(ulong id)
+ {
+ if (_entitlements.TryGetValue(id, out var entitlement))
+ return entitlement;
+ return null;
+ }
+
+ internal SocketEntitlement GetOrAddEntitlement(ulong id, Func entitlementFactory)
+ {
+ return _entitlements.GetOrAdd(id, entitlementFactory);
+ }
+
+ internal SocketEntitlement RemoveEntitlement(ulong id)
+ {
+ if(_entitlements.TryRemove(id, out var entitlement))
+ return entitlement;
+ return null;
+ }
}
}
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
index 2356f226..d11d86a6 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
@@ -428,6 +428,35 @@ namespace Discord.WebSocket
public override SocketUser GetUser(string username, string discriminator = null)
=> State.Users.FirstOrDefault(x => (discriminator is null || x.Discriminator == discriminator) && x.Username == username);
+ ///
+ public Task CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options = null)
+ => ClientHelper.CreateTestEntitlementAsync(this, skuId, ownerId, ownerType, options);
+
+ ///
+ public Task DeleteTestEntitlementAsync(ulong entitlementId, RequestOptions options = null)
+ => ApiClient.DeleteEntitlementAsync(entitlementId, options);
+
+ ///
+ public IAsyncEnumerable> GetEntitlementsAsync(BaseDiscordClient client, int? limit = 100,
+ ulong? afterId = null, ulong? beforeId = null, bool excludeEnded = false, ulong? guildId = null, ulong? userId = null,
+ ulong[] skuIds = null, RequestOptions options = null)
+ => ClientHelper.ListEntitlementsAsync(this, limit, afterId, beforeId, excludeEnded, guildId, userId, skuIds, options);
+
+ ///
+ public Task> GetSKUsAsync(RequestOptions options = null)
+ => ClientHelper.ListSKUsAsync(this, options);
+
+ ///
+ /// Gets entitlements from cache.
+ ///
+ public IReadOnlyCollection Entitlements => State.Entitlements;
+
+ ///
+ /// Gets an entitlement from cache. if not found.
+ ///
+ public SocketEntitlement GetEntitlement(ulong id)
+ => State.GetEntitlement(id);
+
///
/// Gets a global application command.
///
@@ -2903,20 +2932,20 @@ namespace Discord.WebSocket
#region Audit Logs
case "GUILD_AUDIT_LOG_ENTRY_CREATE":
- {
- var data = (payload as JToken).ToObject(_serializer);
- type = "GUILD_AUDIT_LOG_ENTRY_CREATE";
- await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AUDIT_LOG_ENTRY_CREATE)").ConfigureAwait(false);
+ {
+ var data = (payload as JToken).ToObject(_serializer);
+ type = "GUILD_AUDIT_LOG_ENTRY_CREATE";
+ await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AUDIT_LOG_ENTRY_CREATE)").ConfigureAwait(false);
- var guild = State.GetGuild(data.GuildId);
- var auditLog = SocketAuditLogEntry.Create(this, data);
- guild.AddAuditLog(auditLog);
+ var guild = State.GetGuild(data.GuildId);
+ var auditLog = SocketAuditLogEntry.Create(this, data);
+ guild.AddAuditLog(auditLog);
- await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild);
- }
- break;
+ await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild);
+ }
+ break;
#endregion
-
+
#region Auto Moderation
case "AUTO_MODERATION_RULE_CREATE":
@@ -3051,6 +3080,65 @@ namespace Discord.WebSocket
#endregion
+ #region App Subscriptions
+
+ case "ENTITLEMENT_CREATE":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_CREATE)").ConfigureAwait(false);
+ var data = (payload as JToken).ToObject(_serializer);
+
+ var entitlement = SocketEntitlement.Create(this, data);
+ State.AddEntitlement(data.Id, entitlement);
+
+ await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement);
+ }
+ break;
+
+ case "ENTITLEMENT_UPDATE":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_UPDATE)").ConfigureAwait(false);
+ var data = (payload as JToken).ToObject(_serializer);
+
+ var entitlement = State.GetEntitlement(data.Id);
+
+ var cacheableBefore = new Cacheable(entitlement?.Clone(), data.Id,
+ entitlement is not null, () => null);
+
+ if (entitlement is null)
+ {
+ entitlement = SocketEntitlement.Create(this, data);
+ State.AddEntitlement(data.Id, entitlement);
+ }
+ else
+ {
+ entitlement.Update(data);
+ }
+
+ await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement);
+ }
+ break;
+
+ case "ENTITLEMENT_DELETE":
+ {
+ await _gatewayLogger.DebugAsync("Received Dispatch (ENTITLEMENT_DELETE)").ConfigureAwait(false);
+ var data = (payload as JToken).ToObject(_serializer);
+
+ var entitlement = State.RemoveEntitlement(data.Id);
+
+ if (entitlement is null)
+ entitlement = SocketEntitlement.Create(this, data);
+ else
+ entitlement.Update(data);
+
+ var cacheableEntitlement = new Cacheable(entitlement, data.Id,
+ entitlement is not null, () => null);
+
+ await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement);
+ }
+ break;
+
+ #endregion
+
#region Ignored (User only)
case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);
@@ -3380,10 +3468,9 @@ namespace Discord.WebSocket
internal int GetAudioId() => _nextAudioId++;
#region IDiscordClient
- ///
- async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
- => await GetApplicationInfoAsync().ConfigureAwait(false);
+ async Task IDiscordClient.CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options)
+ => await CreateTestEntitlementAsync(skuId, ownerId, ownerType, options).ConfigureAwait(false);
///
async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options)
=> mode == CacheMode.AllowDownload ? await GetChannelAsync(id, options).ConfigureAwait(false) : GetChannel(id);
diff --git a/src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketEntitlement.cs b/src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketEntitlement.cs
new file mode 100644
index 00000000..0bfafe87
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Entities/AppSubscriptions/SocketEntitlement.cs
@@ -0,0 +1,91 @@
+using Discord.Rest;
+using System;
+
+using Model = Discord.API.Entitlement;
+
+namespace Discord.WebSocket;
+
+public class SocketEntitlement : SocketEntity, IEntitlement
+{
+ ///
+ public DateTimeOffset CreatedAt { get; private set; }
+
+ ///
+ public ulong SkuId { get; private set; }
+
+ ///
+ public Cacheable? User { get; private set; }
+
+ ///
+ public Cacheable? Guild { get; private set; }
+
+ ///
+ public ulong ApplicationId { get; private set; }
+
+ ///
+ public EntitlementType Type { get; private set; }
+
+ ///
+ public bool IsConsumed { get; private set; }
+
+ ///
+ public DateTimeOffset? StartsAt { get; private set; }
+
+ ///
+ public DateTimeOffset? EndsAt { get; private set; }
+
+ internal SocketEntitlement(DiscordSocketClient discord, ulong id) : base(discord, id)
+ {
+ }
+
+ internal static SocketEntitlement Create(DiscordSocketClient discord, Model model)
+ {
+ var entity = new SocketEntitlement(discord, model.Id);
+ entity.Update(model);
+ return entity;
+ }
+
+ internal void Update(Model model)
+ {
+ SkuId = model.SkuId;
+
+ if (model.UserId.IsSpecified)
+ {
+ var user = Discord.GetUser(model.UserId.Value);
+
+ User = new Cacheable(user, model.UserId.Value, user is not null, async ()
+ => await Discord.Rest.GetUserAsync(model.UserId.Value));
+ }
+
+ if (model.GuildId.IsSpecified)
+ {
+ var guild = Discord.GetGuild(model.GuildId.Value);
+
+ Guild = new Cacheable(guild, model.GuildId.Value, guild is not null, async ()
+ => await Discord.Rest.GetGuildAsync(model.GuildId.Value));
+ }
+
+ ApplicationId = model.ApplicationId;
+ Type = model.Type;
+ IsConsumed = model.IsConsumed;
+ StartsAt = model.StartsAt.IsSpecified
+ ? model.StartsAt.Value
+ : null;
+ EndsAt = model.EndsAt.IsSpecified
+ ? model.EndsAt.Value
+ : null;
+ }
+
+ internal SocketEntitlement Clone() => MemberwiseClone() as SocketEntitlement;
+
+ #region IEntitlement
+
+ ///
+ ulong? IEntitlement.GuildId => Guild?.Id;
+
+ ///
+ ulong? IEntitlement.UserId => User?.Id;
+
+ #endregion
+
+}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
index c7c1e268..d4ee3661 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
@@ -2,7 +2,9 @@ using Discord.Net;
using Discord.Rest;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using DataModel = Discord.API.ApplicationCommandInteractionData;
using Model = Discord.API.Interaction;
@@ -71,6 +73,9 @@ namespace Discord.WebSocket
///
public ulong ApplicationId { get; private set; }
+ ///
+ public IReadOnlyCollection Entitlements { get; private set; }
+
internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel, SocketUser user)
: base(client, id)
{
@@ -142,6 +147,8 @@ namespace Discord.WebSocket
GuildLocale = model.GuildLocale.IsSpecified
? model.GuildLocale.Value
: null;
+
+ Entitlements = model.Entitlements.Select(x => RestEntitlement.Create(Discord, x)).ToImmutableArray();
}
///
@@ -398,6 +405,11 @@ namespace Discord.WebSocket
/// The request options for this request.
/// A task that represents the asynchronous operation of responding to the interaction.
public abstract Task RespondWithModalAsync(Modal modal, RequestOptions options = null);
+
+ ///
+ public Task RespondWithPremiumRequiredAsync(RequestOptions options = null)
+ => InteractionHelper.RespondWithPremiumRequiredAsync(Discord, Id, Token, options);
+
#endregion
///
@@ -423,6 +435,9 @@ namespace Discord.WebSocket
}
#region IDiscordInteraction
+ ///
+ IReadOnlyCollection IDiscordInteraction.Entitlements => Entitlements;
+
///
IUser IDiscordInteraction.User => User;