[Feature] Premium subscriptions (#2781)
* what a big commit lel, add app sub enums * work * ah yup lol * `?` * events 1 * typo * `list` => `get` | remaining events * add `RespondWithPremiumRequiredAsync` to interaction module base
This commit is contained in:
@@ -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<Func<SocketAuditLogEntry, SocketGuild, Task>> _auditLogCreated = new();
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region AutoModeration
|
||||
|
||||
/// <summary>
|
||||
@@ -918,7 +919,7 @@ namespace Discord.WebSocket
|
||||
add => _autoModRuleCreated.Add(value);
|
||||
remove => _autoModRuleCreated.Remove(value);
|
||||
}
|
||||
internal readonly AsyncEvent<Func<SocketAutoModRule, Task>> _autoModRuleCreated = new ();
|
||||
internal readonly AsyncEvent<Func<SocketAutoModRule, Task>> _autoModRuleCreated = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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<Func<Cacheable<SocketAutoModRule, ulong>, SocketAutoModRule, Task>> _autoModRuleUpdated = new ();
|
||||
internal readonly AsyncEvent<Func<Cacheable<SocketAutoModRule, ulong>, SocketAutoModRule, Task>> _autoModRuleUpdated = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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<Func<SocketAutoModRule, Task>> _autoModRuleDeleted = new ();
|
||||
internal readonly AsyncEvent<Func<SocketAutoModRule, Task>> _autoModRuleDeleted = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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<Func<SocketGuild, AutoModRuleAction, AutoModActionExecutedData, Task>> _autoModActionExecuted = new ();
|
||||
|
||||
internal readonly AsyncEvent<Func<SocketGuild, AutoModRuleAction, AutoModActionExecutedData, Task>> _autoModActionExecuted = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region App Subscriptions
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a user subscribes to a SKU.
|
||||
/// </summary>
|
||||
public event Func<SocketEntitlement, Task> EntitlementCreated
|
||||
{
|
||||
add { _entitlementCreated.Add(value); }
|
||||
remove { _entitlementCreated.Remove(value); }
|
||||
}
|
||||
|
||||
internal readonly AsyncEvent<Func<SocketEntitlement, Task>> _entitlementCreated = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a subscription to a SKU is updated.
|
||||
/// </summary>
|
||||
public event Func<Cacheable<SocketEntitlement, ulong>, SocketEntitlement, Task> EntitlementUpdated
|
||||
{
|
||||
add { _entitlementUpdated.Add(value); }
|
||||
remove { _entitlementUpdated.Remove(value); }
|
||||
}
|
||||
|
||||
internal readonly AsyncEvent<Func<Cacheable<SocketEntitlement, ulong>, SocketEntitlement, Task>> _entitlementUpdated = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a user's entitlement is deleted.
|
||||
/// </summary>
|
||||
public event Func<Cacheable<SocketEntitlement, ulong>, Task> EntitlementDeleted
|
||||
{
|
||||
add { _entitlementDeleted.Add(value); }
|
||||
remove { _entitlementDeleted.Remove(value); }
|
||||
}
|
||||
|
||||
internal readonly AsyncEvent<Func<Cacheable<SocketEntitlement, ulong>, Task>> _entitlementDeleted = new();
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Discord.WebSocket
|
||||
private readonly ConcurrentDictionary<ulong, SocketGlobalUser> _users;
|
||||
private readonly ConcurrentHashSet<ulong> _groupChannels;
|
||||
private readonly ConcurrentDictionary<ulong, SocketApplicationCommand> _commands;
|
||||
private readonly ConcurrentDictionary<ulong, SocketEntitlement> _entitlements;
|
||||
|
||||
internal IReadOnlyCollection<SocketChannel> Channels => _channels.ToReadOnlyCollection();
|
||||
internal IReadOnlyCollection<SocketDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection();
|
||||
@@ -24,6 +25,7 @@ namespace Discord.WebSocket
|
||||
internal IReadOnlyCollection<SocketGuild> Guilds => _guilds.ToReadOnlyCollection();
|
||||
internal IReadOnlyCollection<SocketGlobalUser> Users => _users.ToReadOnlyCollection();
|
||||
internal IReadOnlyCollection<SocketApplicationCommand> Commands => _commands.ToReadOnlyCollection();
|
||||
internal IReadOnlyCollection<SocketEntitlement> Entitlements => _entitlements.ToReadOnlyCollection();
|
||||
|
||||
internal IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels =>
|
||||
_dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat(
|
||||
@@ -40,6 +42,7 @@ namespace Discord.WebSocket
|
||||
_users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier));
|
||||
_groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier));
|
||||
_commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>();
|
||||
_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<ulong, SocketEntitlement> entitlementFactory)
|
||||
{
|
||||
return _entitlements.GetOrAdd(id, entitlementFactory);
|
||||
}
|
||||
|
||||
internal SocketEntitlement RemoveEntitlement(ulong id)
|
||||
{
|
||||
if(_entitlements.TryRemove(id, out var entitlement))
|
||||
return entitlement;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/// <inheritdoc cref="IDiscordClient.CreateTestEntitlementAsync"/>
|
||||
public Task<RestEntitlement> CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options = null)
|
||||
=> ClientHelper.CreateTestEntitlementAsync(this, skuId, ownerId, ownerType, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteTestEntitlementAsync(ulong entitlementId, RequestOptions options = null)
|
||||
=> ApiClient.DeleteEntitlementAsync(entitlementId, options);
|
||||
|
||||
/// <inheritdoc cref="IDiscordClient.GetEntitlementsAsync"/>
|
||||
public IAsyncEnumerable<IReadOnlyCollection<IEntitlement>> 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);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyCollection<SKU>> GetSKUsAsync(RequestOptions options = null)
|
||||
=> ClientHelper.ListSKUsAsync(this, options);
|
||||
|
||||
/// <summary>
|
||||
/// Gets entitlements from cache.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<SocketEntitlement> Entitlements => State.Entitlements;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an entitlement from cache. <see langword="null"/> if not found.
|
||||
/// </summary>
|
||||
public SocketEntitlement GetEntitlement(ulong id)
|
||||
=> State.GetEntitlement(id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a global application command.
|
||||
/// </summary>
|
||||
@@ -2903,20 +2932,20 @@ namespace Discord.WebSocket
|
||||
#region Audit Logs
|
||||
|
||||
case "GUILD_AUDIT_LOG_ENTRY_CREATE":
|
||||
{
|
||||
var data = (payload as JToken).ToObject<AuditLogCreatedEvent>(_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<AuditLogCreatedEvent>(_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<Entitlement>(_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<Entitlement>(_serializer);
|
||||
|
||||
var entitlement = State.GetEntitlement(data.Id);
|
||||
|
||||
var cacheableBefore = new Cacheable<SocketEntitlement, ulong>(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<Entitlement>(_serializer);
|
||||
|
||||
var entitlement = State.RemoveEntitlement(data.Id);
|
||||
|
||||
if (entitlement is null)
|
||||
entitlement = SocketEntitlement.Create(this, data);
|
||||
else
|
||||
entitlement.Update(data);
|
||||
|
||||
var cacheableEntitlement = new Cacheable<SocketEntitlement, ulong>(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
|
||||
/// <inheritdoc />
|
||||
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
|
||||
=> await GetApplicationInfoAsync().ConfigureAwait(false);
|
||||
|
||||
async Task<IEntitlement> IDiscordClient.CreateTestEntitlementAsync(ulong skuId, ulong ownerId, SubscriptionOwnerType ownerType, RequestOptions options)
|
||||
=> await CreateTestEntitlementAsync(skuId, ownerId, ownerType, options).ConfigureAwait(false);
|
||||
/// <inheritdoc />
|
||||
async Task<IChannel> IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options)
|
||||
=> mode == CacheMode.AllowDownload ? await GetChannelAsync(id, options).ConfigureAwait(false) : GetChannel(id);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using Discord.Rest;
|
||||
using System;
|
||||
|
||||
using Model = Discord.API.Entitlement;
|
||||
|
||||
namespace Discord.WebSocket;
|
||||
|
||||
public class SocketEntitlement : SocketEntity<ulong>, IEntitlement
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ulong SkuId { get; private set; }
|
||||
|
||||
/// <inheritdoc cref="IEntitlement.UserId"/>
|
||||
public Cacheable<SocketUser, RestUser, IUser, ulong>? User { get; private set; }
|
||||
|
||||
/// <inheritdoc cref="IEntitlement.GuildId"/>
|
||||
public Cacheable<SocketGuild, RestGuild, IGuild, ulong>? Guild { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ulong ApplicationId { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public EntitlementType Type { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsConsumed { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTimeOffset? StartsAt { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<SocketUser, RestUser, IUser, ulong>(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<SocketGuild, RestGuild, IGuild, ulong>(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
|
||||
|
||||
/// <inheritdoc/>
|
||||
ulong? IEntitlement.GuildId => Guild?.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
ulong? IEntitlement.UserId => User?.Id;
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
@@ -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
|
||||
/// <inheritdoc/>
|
||||
public ulong ApplicationId { get; private set; }
|
||||
|
||||
/// <inheritdoc cref="IDiscordInteraction.Entitlements" />
|
||||
public IReadOnlyCollection<RestEntitlement> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -398,6 +405,11 @@ namespace Discord.WebSocket
|
||||
/// <param name="options">The request options for this <see langword="async"/> request.</param>
|
||||
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
|
||||
public abstract Task RespondWithModalAsync(Modal modal, RequestOptions options = null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RespondWithPremiumRequiredAsync(RequestOptions options = null)
|
||||
=> InteractionHelper.RespondWithPremiumRequiredAsync(Discord, Id, Token, options);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -423,6 +435,9 @@ namespace Discord.WebSocket
|
||||
}
|
||||
|
||||
#region IDiscordInteraction
|
||||
/// <inheritdoc/>
|
||||
IReadOnlyCollection<IEntitlement> IDiscordInteraction.Entitlements => Entitlements;
|
||||
|
||||
/// <inheritdoc/>
|
||||
IUser IDiscordInteraction.User => User;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user