diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 3d7746c6d..3b9a90cda 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -9,6 +9,7 @@ export 'src/errors.dart' ShardDisconnectedError, RoleNotFoundException, AuditLogEntryNotFoundException, + EntitlementNotFoundException, OutOfRemainingSessionsError, IntegrationNotFoundException, AlreadyAcknowledgedError, @@ -64,6 +65,8 @@ export 'src/builders/sticker.dart' show StickerBuilder, StickerUpdateBuilder; export 'src/builders/application_command.dart' show ApplicationCommandBuilder, ApplicationCommandUpdateBuilder, CommandOptionBuilder, CommandOptionChoiceBuilder; export 'src/builders/interaction_response.dart' show InteractionResponseBuilder, ModalBuilder, InteractionCallbackType; +export 'src/builders/entitlement.dart' show TestEntitlementBuilder, TestEntitlementType; +export 'src/builders/application.dart' show ApplicationUpdateBuilder; export 'src/cache/cache.dart' show Cache, CacheConfig; @@ -94,6 +97,7 @@ export 'src/http/managers/audit_log_manager.dart' show AuditLogManager; export 'src/http/managers/sticker_manager.dart' show GuildStickerManager, GlobalStickerManager; export 'src/http/managers/application_command_manager.dart' show ApplicationCommandManager, GlobalApplicationCommandManager, GuildApplicationCommandManager; export 'src/http/managers/interaction_manager.dart' show InteractionManager; +export 'src/http/managers/entitlement_manager.dart' show EntitlementManager; export 'src/gateway/gateway.dart' show Gateway; export 'src/gateway/message.dart' show Disconnecting, Dispose, ErrorReceived, EventReceived, GatewayMessage, Send, ShardData, ShardMessage; @@ -112,6 +116,7 @@ export 'src/models/channel/channel.dart' show Channel, ChannelFlags, PartialChan export 'src/models/channel/followed_channel.dart' show FollowedChannel; export 'src/models/channel/guild_channel.dart' show GuildChannel; export 'src/models/channel/has_threads_channel.dart' show HasThreadsChannel; +export 'src/models/channel/thread_aggregate.dart' show ThreadsOnlyChannel; export 'src/models/channel/text_channel.dart' show PartialTextChannel, TextChannel; export 'src/models/channel/thread_list.dart' show ThreadList; export 'src/models/channel/thread.dart' show PartialThreadMember, Thread, ThreadMember; @@ -129,13 +134,14 @@ export 'src/models/channel/types/guild_text.dart' show GuildTextChannel; export 'src/models/channel/types/guild_voice.dart' show GuildVoiceChannel; export 'src/models/channel/types/private_thread.dart' show PrivateThread; export 'src/models/channel/types/public_thread.dart' show PublicThread; +export 'src/models/channel/types/guild_media.dart' show GuildMediaChannel; export 'src/models/message/activity.dart' show MessageActivity, MessageActivityType; -export 'src/models/message/attachment.dart' show Attachment; +export 'src/models/message/attachment.dart' show Attachment, AttachmentFlags; export 'src/models/message/author.dart' show MessageAuthor; export 'src/models/message/channel_mention.dart' show ChannelMention; export 'src/models/message/embed.dart' show Embed, EmbedAuthor, EmbedField, EmbedFooter, EmbedImage, EmbedProvider, EmbedThumbnail, EmbedVideo; export 'src/models/message/message.dart' show Message, MessageFlags, PartialMessage, MessageType, MessageInteraction; -export 'src/models/message/reaction.dart' show Reaction; +export 'src/models/message/reaction.dart' show Reaction, ReactionCountDetails; export 'src/models/message/reference.dart' show MessageReference; export 'src/models/message/role_subscription_data.dart' show RoleSubscriptionData; export 'src/models/message/component.dart' @@ -189,7 +195,7 @@ export 'src/models/guild/auto_moderation.dart' TriggerType; export 'src/models/voice/voice_state.dart' show VoiceState; export 'src/models/voice/voice_region.dart' show VoiceRegion; -export 'src/models/role.dart' show PartialRole, Role, RoleTags; +export 'src/models/role.dart' show PartialRole, Role, RoleTags, RoleFlags; export 'src/models/gateway/gateway.dart' show GatewayBot, GatewayConfiguration, SessionStartLimit; export 'src/models/gateway/event.dart' show @@ -260,6 +266,7 @@ export 'src/models/gateway/events/ready.dart' show ReadyEvent, ResumedEvent; export 'src/models/gateway/events/stage_instance.dart' show StageInstanceCreateEvent, StageInstanceDeleteEvent, StageInstanceUpdateEvent; export 'src/models/gateway/events/voice.dart' show VoiceServerUpdateEvent, VoiceStateUpdateEvent; export 'src/models/gateway/events/webhook.dart' show WebhooksUpdateEvent; +export 'src/models/gateway/events/entitlement.dart' show EntitlementCreateEvent, EntitlementDeleteEvent, EntitlementUpdateEvent; export 'src/models/presence.dart' show Activity, ActivityAssets, ActivityButton, ActivityFlags, ActivityParty, ActivitySecrets, ActivityTimestamps, ClientStatus, ActivityType, UserStatus; export 'src/models/emoji.dart' show Emoji, GuildEmoji, PartialEmoji, TextEmoji; @@ -270,7 +277,7 @@ export 'src/models/sticker/sticker_pack.dart' show StickerPack; export 'src/models/commands/application_command.dart' show ApplicationCommand, PartialApplicationCommand, ApplicationCommandType; export 'src/models/commands/application_command_option.dart' show CommandOption, CommandOptionChoice, CommandOptionType, CommandOptionMentionable; export 'src/models/commands/application_command_permissions.dart' show CommandPermission, CommandPermissions, CommandPermissionType; -export 'src/models/team.dart' show Team, TeamMember, TeamMembershipState; +export 'src/models/team.dart' show Team, TeamMember, TeamMembershipState, TeamMemberRole; export 'src/models/interaction.dart' show ApplicationCommandInteractionData, @@ -287,6 +294,8 @@ export 'src/models/interaction.dart' MessageComponentInteraction, ModalSubmitInteraction, PingInteraction; +export 'src/models/entitlement.dart' show Entitlement, PartialEntitlement, EntitlementType; +export 'src/models/sku.dart' show Sku, SkuType; export 'src/utils/flags.dart' show Flag, Flags; export 'src/intents.dart' show GatewayIntents; diff --git a/lib/src/builders/application.dart b/lib/src/builders/application.dart new file mode 100644 index 000000000..03fc247d4 --- /dev/null +++ b/lib/src/builders/application.dart @@ -0,0 +1,53 @@ +import 'package:nyxx/src/builders/builder.dart'; +import 'package:nyxx/src/builders/image.dart'; +import 'package:nyxx/src/builders/sentinels.dart'; +import 'package:nyxx/src/models/application.dart'; +import 'package:nyxx/src/utils/flags.dart'; + +class ApplicationUpdateBuilder extends UpdateBuilder { + Uri? customInstallUrl; + + String? description; + + Uri? roleConnectionsVerificationUrl; + + InstallationParameters? installationParameters; + + Flags? flags; + + ImageBuilder? icon; + + ImageBuilder? coverImage; + + Uri? interactionsEndpointUrl; + + List? tags; + + ApplicationUpdateBuilder({ + this.customInstallUrl, + this.description, + this.roleConnectionsVerificationUrl, + this.installationParameters, + this.flags, + this.icon = sentinelImageBuilder, + this.coverImage = sentinelImageBuilder, + this.interactionsEndpointUrl, + this.tags, + }); + + @override + Map build() => { + if (customInstallUrl != null) 'custom_install_url': customInstallUrl!.toString(), + if (description != null) 'description': description, + if (roleConnectionsVerificationUrl != null) 'role_connections_verification_url': roleConnectionsVerificationUrl!.toString(), + if (installationParameters != null) + 'install_params': { + 'scopes': installationParameters!.scopes, + 'permissions': installationParameters!.permissions.toString(), + }, + if (flags != null) 'flags': flags!.value, + if (!identical(icon, sentinelImageBuilder)) 'icon': icon?.buildDataString(), + if (!identical(coverImage, sentinelImageBuilder)) 'cover_image': coverImage?.buildDataString(), + if (tags != null) 'tags': tags, + }; +} diff --git a/lib/src/builders/channel/stage_instance.dart b/lib/src/builders/channel/stage_instance.dart index 6fa0d3107..f398f0ca7 100644 --- a/lib/src/builders/channel/stage_instance.dart +++ b/lib/src/builders/channel/stage_instance.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/models/channel/stage_instance.dart'; +import 'package:nyxx/src/models/snowflake.dart'; class StageInstanceBuilder extends CreateBuilder { String topic; @@ -8,10 +9,13 @@ class StageInstanceBuilder extends CreateBuilder { bool? sendStartNotification; + Snowflake? guildScheduledEventId; + StageInstanceBuilder({ required this.topic, this.privacyLevel, this.sendStartNotification, + this.guildScheduledEventId, }); @override @@ -19,6 +23,7 @@ class StageInstanceBuilder extends CreateBuilder { 'topic': topic, if (privacyLevel != null) 'privacy_level': privacyLevel!.value, if (sendStartNotification != null) 'send_start_notification': sendStartNotification, + if (guildScheduledEventId != null) 'guild_scheduled_event_id': guildScheduledEventId!.toString(), }; } diff --git a/lib/src/builders/entitlement.dart b/lib/src/builders/entitlement.dart new file mode 100644 index 000000000..29d93be78 --- /dev/null +++ b/lib/src/builders/entitlement.dart @@ -0,0 +1,29 @@ +import 'package:nyxx/src/builders/builder.dart'; +import 'package:nyxx/src/models/entitlement.dart'; +import 'package:nyxx/src/models/snowflake.dart'; + +class TestEntitlementBuilder extends CreateBuilder { + Snowflake skuId; + + Snowflake ownerId; + + TestEntitlementType ownerType; + + TestEntitlementBuilder({required this.skuId, required this.ownerId, required this.ownerType}); + + @override + Map build() => { + 'sku_id': skuId.toString(), + 'owner_id': ownerId.toString(), + 'owner_type': ownerType.value, + }; +} + +enum TestEntitlementType { + guildSubscription._(1), + userSubscription._(2); + + final int value; + + const TestEntitlementType._(this.value); +} diff --git a/lib/src/builders/interaction_response.dart b/lib/src/builders/interaction_response.dart index 7cf4605b2..1b9efdc7e 100644 --- a/lib/src/builders/interaction_response.dart +++ b/lib/src/builders/interaction_response.dart @@ -54,6 +54,8 @@ class InteractionResponseBuilder extends CreateBuilder InteractionResponseBuilder(type: InteractionCallbackType.modal, data: modal); + factory InteractionResponseBuilder.premiumRequired() => InteractionResponseBuilder(type: InteractionCallbackType.premiumRequired, data: null); + @override Map build() { final builtData = switch (data) { @@ -108,7 +110,8 @@ enum InteractionCallbackType { deferredUpdateMessage._(6), updateMessage._(7), applicationCommandAutocompleteResult._(8), - modal._(9); + modal._(9), + premiumRequired._(10); final int value; diff --git a/lib/src/builders/message/component.dart b/lib/src/builders/message/component.dart index c47640535..656d10fbf 100644 --- a/lib/src/builders/message/component.dart +++ b/lib/src/builders/message/component.dart @@ -1,8 +1,12 @@ import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/models/channel/channel.dart'; +import 'package:nyxx/src/models/commands/application_command_option.dart'; import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/message/component.dart'; +import 'package:nyxx/src/models/role.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/models/user/user.dart'; abstract class MessageComponentBuilder extends CreateBuilder { MessageComponentType type; @@ -111,6 +115,8 @@ class SelectMenuBuilder extends MessageComponentBuilder { List? channelTypes; + List? defaultValues; + String? placeholder; int? minValues; @@ -125,6 +131,7 @@ class SelectMenuBuilder extends MessageComponentBuilder { this.options, this.channelTypes, this.placeholder, + this.defaultValues, this.minValues, this.maxValues, this.isDisabled, @@ -142,6 +149,7 @@ class SelectMenuBuilder extends MessageComponentBuilder { SelectMenuBuilder.userSelect({ required this.customId, this.placeholder, + List>? this.defaultValues, this.minValues, this.maxValues, this.isDisabled, @@ -150,6 +158,7 @@ class SelectMenuBuilder extends MessageComponentBuilder { SelectMenuBuilder.roleSelect({ required this.customId, this.placeholder, + List>? this.defaultValues, this.minValues, this.maxValues, this.isDisabled, @@ -159,6 +168,7 @@ class SelectMenuBuilder extends MessageComponentBuilder { required this.customId, this.channelTypes, this.placeholder, + List>? this.defaultValues, this.minValues, this.maxValues, this.isDisabled, @@ -167,6 +177,7 @@ class SelectMenuBuilder extends MessageComponentBuilder { SelectMenuBuilder.channelSelect({ required this.customId, this.placeholder, + List>? this.defaultValues, this.minValues, this.maxValues, this.isDisabled, @@ -179,6 +190,7 @@ class SelectMenuBuilder extends MessageComponentBuilder { if (options != null) 'options': options?.map((e) => e.build()).toList(), if (channelTypes != null) 'channel_types': channelTypes?.map((e) => e.value).toList(), if (placeholder != null) 'placeholder': placeholder, + if (defaultValues != null) 'default_values': defaultValues!.map((e) => e.build()).toList(), if (minValues != null) 'min_values': minValues, if (maxValues != null) 'max_values': maxValues, if (isDisabled != null) 'disabled': isDisabled, @@ -219,6 +231,29 @@ class SelectMenuOptionBuilder extends CreateBuilder { }; } +class DefaultValue> extends CreateBuilder> { + Snowflake id; + + String type; + + DefaultValue({ + required this.id, + required this.type, + }); + + static DefaultValue user({required Snowflake id}) => DefaultValue(id: id, type: 'user'); + + static DefaultValue role({required Snowflake id}) => DefaultValue(id: id, type: 'role'); + + static DefaultValue channel({required Snowflake id}) => DefaultValue(id: id, type: 'channel'); + + @override + Map build() => { + 'id': id.toString(), + 'type': type, + }; +} + class TextInputBuilder extends MessageComponentBuilder { String customId; diff --git a/lib/src/client_options.dart b/lib/src/client_options.dart index f2b0a0a3e..173a4bfa0 100644 --- a/lib/src/client_options.dart +++ b/lib/src/client_options.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/models/commands/application_command.dart'; import 'package:nyxx/src/models/commands/application_command_permissions.dart'; import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/channel/stage_instance.dart'; +import 'package:nyxx/src/models/entitlement.dart'; import 'package:nyxx/src/models/guild/audit_log.dart'; import 'package:nyxx/src/models/guild/auto_moderation.dart'; import 'package:nyxx/src/models/guild/guild.dart'; @@ -92,6 +93,9 @@ class RestClientOptions extends ClientOptions { /// The [CacheConfig] to use for the [GuildApplicationCommandManager.permissionsCache] cache. final CacheConfig commandPermissionsConfig; + /// The [CacheConfig] to use for the [Application.entitlements] manager. + final CacheConfig entitlementConfig; + /// Create a new [RestClientOptions]. const RestClientOptions({ super.plugins, @@ -114,6 +118,7 @@ class RestClientOptions extends ClientOptions { this.globalStickerCacheConfig = const CacheConfig(), this.applicationCommandConfig = const CacheConfig(), this.commandPermissionsConfig = const CacheConfig(), + this.entitlementConfig = const CacheConfig(), }); } @@ -126,6 +131,7 @@ class GatewayClientOptions extends RestClientOptions { /// If the remaining number of session starts is below this number, an error will be thrown when connecting. final int minimumSessionStarts; + /// Create a new [GatewayClientOptions]. const GatewayClientOptions({ this.minimumSessionStarts = 10, super.plugins, @@ -145,5 +151,6 @@ class GatewayClientOptions extends RestClientOptions { super.voiceStateConfig, super.applicationCommandConfig, super.commandPermissionsConfig, + super.entitlementConfig, }); } diff --git a/lib/src/errors.dart b/lib/src/errors.dart index f70e65399..89c366ae9 100644 --- a/lib/src/errors.dart +++ b/lib/src/errors.dart @@ -69,6 +69,18 @@ class AuditLogEntryNotFoundException extends NyxxException { AuditLogEntryNotFoundException(this.guildId, this.auditLogEntryId) : super('Audit log entry $auditLogEntryId not found in guild $guildId'); } +/// An exception thrown when an entitlement is not found for an application. +class EntitlementNotFoundException extends NyxxException { + /// The ID of the application. + final Snowflake applicationId; + + /// The ID of the entitlement. + final Snowflake entitlementId; + + /// Create a new [EntitlementNotFoundException]. + EntitlementNotFoundException(this.applicationId, this.entitlementId) : super('Entitlement $entitlementId not found for application $applicationId'); +} + /// An error thrown when a shard disconnects unexpectedly. class ShardDisconnectedError extends Error { /// The shard that was disconnected. diff --git a/lib/src/event_mixin.dart b/lib/src/event_mixin.dart index 74ded31a7..5d9c3e3dd 100644 --- a/lib/src/event_mixin.dart +++ b/lib/src/event_mixin.dart @@ -5,6 +5,7 @@ import 'package:nyxx/src/models/gateway/event.dart'; import 'package:nyxx/src/models/gateway/events/application_command.dart'; import 'package:nyxx/src/models/gateway/events/auto_moderation.dart'; import 'package:nyxx/src/models/gateway/events/channel.dart'; +import 'package:nyxx/src/models/gateway/events/entitlement.dart'; import 'package:nyxx/src/models/gateway/events/guild.dart'; import 'package:nyxx/src/models/gateway/events/integration.dart'; import 'package:nyxx/src/models/gateway/events/interaction.dart'; @@ -225,6 +226,15 @@ mixin EventMixin implements Nyxx { /// A [Stream] of [StageInstanceDeleteEvent]s received by this client. Stream get onStageInstanceDelete => onEvent.whereType(); + /// A [Stream] of [EntitlementCreateEvent]s received by this client. + Stream get onEntitlementCreate => onEvent.whereType(); + + /// A [Stream] of [EntitlementUpdateEvent]s received by this client. + Stream get onEntitlementUpdate => onEvent.whereType(); + + /// A [Stream] of [EntitlementDeleteEvent]s received by this client. + Stream get onEntitlementDelete => onEvent.whereType(); + // Specializations of [onInteractionCreate] for convenience. /// A [Stream] of [PingInteraction]s received by this client. diff --git a/lib/src/gateway/gateway.dart b/lib/src/gateway/gateway.dart index d4c3d9ad2..015a796c7 100644 --- a/lib/src/gateway/gateway.dart +++ b/lib/src/gateway/gateway.dart @@ -18,6 +18,7 @@ import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/channel/guild_channel.dart'; import 'package:nyxx/src/models/channel/text_channel.dart'; import 'package:nyxx/src/models/channel/thread.dart'; +import 'package:nyxx/src/models/gateway/events/entitlement.dart'; import 'package:nyxx/src/models/gateway/gateway.dart'; import 'package:nyxx/src/models/gateway/event.dart'; import 'package:nyxx/src/models/gateway/events/application_command.dart'; @@ -183,6 +184,10 @@ class Gateway extends GatewayManager with EventParser { client.guilds[guildId].roles.cache.addAll(data.roles!); } }(), + EntitlementCreateEvent(:final entitlement) || + EntitlementUpdateEvent(:final entitlement) => + client.applications[entitlement.applicationId].entitlements.cache[entitlement.id] = entitlement, + EntitlementDeleteEvent() => null, // TODO _ => null, }); } @@ -259,6 +264,7 @@ class Gateway extends GatewayManager with EventParser { /// Throws an error if the shard handling events for [guildId] is not in this [Gateway] instance. Shard shardFor(Snowflake guildId) => shards.singleWhere((shard) => shard.id == shardIdFor(guildId)); + /// Parse a [DispatchEvent] from [raw]. DispatchEvent parseDispatchEvent(RawDispatchEvent raw) { final mapping = { 'READY': parseReady, @@ -322,11 +328,15 @@ class Gateway extends GatewayManager with EventParser { 'STAGE_INSTANCE_CREATE': parseStageInstanceCreate, 'STAGE_INSTANCE_UPDATE': parseStageInstanceUpdate, 'STAGE_INSTANCE_DELETE': parseStageInstanceDelete, + 'ENTITLEMENT_CREATE': parseEntitlementCreate, + 'ENTITLEMENT_UPDATE': parseEntitlementUpdate, + 'ENTITLEMENT_DELETE': parseEntitlementDelete, }; return mapping[raw.name]?.call(raw.payload) ?? UnknownDispatchEvent(gateway: this, raw: raw); } + /// Parse a [ReadyEvent] from [raw]. ReadyEvent parseReady(Map raw) { return ReadyEvent( gateway: this, @@ -347,12 +357,14 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ResumedEvent] from [raw]. ResumedEvent parseResumed(Map raw) { return ResumedEvent( gateway: this, ); } + /// Parse an [ApplicationCommandPermissionsUpdateEvent] from [raw]. ApplicationCommandPermissionsUpdateEvent parseApplicationCommandPermissionsUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); final permissions = client.guilds[guildId].commands.parseCommandPermissions(raw); @@ -364,6 +376,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [AutoModerationRuleCreateEvent] from [raw]. AutoModerationRuleCreateEvent parseAutoModerationRuleCreate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -373,6 +386,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [AutoModerationRuleUpdateEvent] from [raw]. AutoModerationRuleUpdateEvent parseAutoModerationRuleUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); final rule = client.guilds[guildId].autoModerationRules.parse(raw); @@ -384,6 +398,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [AutoModerationRuleDeleteEvent] from [raw]. AutoModerationRuleDeleteEvent parseAutoModerationRuleDelete(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -393,6 +408,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [AutoModerationActionExecutionEvent] from [raw]. AutoModerationActionExecutionEvent parseAutoModerationActionExecution(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -412,6 +428,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ChannelCreateEvent] from [raw]. ChannelCreateEvent parseChannelCreate(Map raw) { return ChannelCreateEvent( gateway: this, @@ -419,6 +436,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ChannelUpdateEvent] from [raw]. ChannelUpdateEvent parseChannelUpdate(Map raw) { final channel = client.channels.parse(raw); @@ -429,6 +447,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ChannelDeleteEvent] from [raw]. ChannelDeleteEvent parseChannelDelete(Map raw) { return ChannelDeleteEvent( gateway: this, @@ -436,6 +455,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ThreadCreateEvent] from [raw]. ThreadCreateEvent parseThreadCreate(Map raw) { return ThreadCreateEvent( gateway: this, @@ -443,6 +463,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ThreadUpdateEvent] from [raw]. ThreadUpdateEvent parseThreadUpdate(Map raw) { final thread = client.channels.parse(raw) as Thread; @@ -453,6 +474,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ThreadDeleteEvent] from [raw]. ThreadDeleteEvent parseThreadDelete(Map raw) { return ThreadDeleteEvent( gateway: this, @@ -463,6 +485,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ThreadListSyncEvent] from [raw]. ThreadListSyncEvent parseThreadListSync(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -478,6 +501,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ThreadMemberUpdateEvent] from [raw]. ThreadMemberUpdateEvent parseThreadMemberUpdate(Map raw) { return ThreadMemberUpdateEvent( gateway: this, @@ -485,6 +509,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ThreadMembersUpdateEvent] from [raw]. ThreadMembersUpdateEvent parseThreadMembersUpdate(Map raw) { return ThreadMembersUpdateEvent( gateway: this, @@ -496,6 +521,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [ChannelPinsUpdateEvent] from [raw]. ChannelPinsUpdateEvent parseChannelPinsUpdate(Map raw) { return ChannelPinsUpdateEvent( gateway: this, @@ -505,6 +531,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [UnavailableGuildCreateEvent] from [raw]. UnavailableGuildCreateEvent parseGuildCreate(Map raw) { if (raw['unavailable'] == true) { return UnavailableGuildCreateEvent(gateway: this, guild: PartialGuild(id: Snowflake.parse(raw['id']!), manager: client.guilds)); @@ -528,6 +555,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildUpdateEvent] from [raw]. GuildUpdateEvent parseGuildUpdate(Map raw) { final guild = client.guilds.parse(raw); @@ -538,6 +566,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildDeleteEvent] from [raw]. GuildDeleteEvent parseGuildDelete(Map raw) { return GuildDeleteEvent( gateway: this, @@ -546,6 +575,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildAuditLogCreateEvent] from [raw]. GuildAuditLogCreateEvent parseGuildAuditLogCreate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -556,6 +586,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildBanAddEvent] from [raw]. GuildBanAddEvent parseGuildBanAdd(Map raw) { return GuildBanAddEvent( gateway: this, @@ -564,6 +595,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildBanRemoveEvent] from [raw]. GuildBanRemoveEvent parseGuildBanRemove(Map raw) { return GuildBanRemoveEvent( gateway: this, @@ -572,6 +604,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildEmojisUpdateEvent] from [raw]. GuildEmojisUpdateEvent parseGuildEmojisUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -582,6 +615,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildStickersUpdateEvent] from [raw]. GuildStickersUpdateEvent parseGuildStickersUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -592,6 +626,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildIntegrationsUpdateEvent] from [raw]. GuildIntegrationsUpdateEvent parseGuildIntegrationsUpdate(Map raw) { return GuildIntegrationsUpdateEvent( gateway: this, @@ -599,6 +634,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildMemberAddEvent] from [raw]. GuildMemberAddEvent parseGuildMemberAdd(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -609,6 +645,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildMemberRemoveEvent] from [raw]. GuildMemberRemoveEvent parseGuildMemberRemove(Map raw) { return GuildMemberRemoveEvent( gateway: this, @@ -617,6 +654,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildMemberUpdateEvent] from [raw]. GuildMemberUpdateEvent parseGuildMemberUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); final member = client.guilds[guildId].members.parse(raw); @@ -629,6 +667,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildMembersChunkEvent] from [raw]. GuildMembersChunkEvent parseGuildMembersChunk(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -644,6 +683,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildRoleCreateEvent] from [raw]. GuildRoleCreateEvent parseGuildRoleCreate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -654,6 +694,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildRoleUpdateEvent] from [raw]. GuildRoleUpdateEvent parseGuildRoleUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); final role = client.guilds[guildId].roles.parse(raw['role'] as Map); @@ -666,6 +707,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildRoleDeleteEvent] from [raw]. GuildRoleDeleteEvent parseGuildRoleDelete(Map raw) { return GuildRoleDeleteEvent( gateway: this, @@ -674,6 +716,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildScheduledEventCreateEvent] from [raw]. GuildScheduledEventCreateEvent parseGuildScheduledEventCreate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -683,6 +726,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildScheduledEventUpdateEvent] from [raw]. GuildScheduledEventUpdateEvent parseGuildScheduledEventUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); final event = client.guilds[guildId].scheduledEvents.parse(raw); @@ -694,6 +738,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildScheduledEventDeleteEvent] from [raw]. GuildScheduledEventDeleteEvent parseGuildScheduledEventDelete(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -703,6 +748,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildScheduledEventUserAddEvent] from [raw]. GuildScheduledEventUserAddEvent parseGuildScheduledEventUserAdd(Map raw) { return GuildScheduledEventUserAddEvent( gateway: this, @@ -712,6 +758,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [GuildScheduledEventUserRemoveEvent] from [raw]. GuildScheduledEventUserRemoveEvent parseGuildScheduledEventUserRemove(Map raw) { return GuildScheduledEventUserRemoveEvent( gateway: this, @@ -721,6 +768,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [IntegrationCreateEvent] from [raw]. IntegrationCreateEvent parseIntegrationCreate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); @@ -731,6 +779,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [IntegrationUpdateEvent] from [raw]. IntegrationUpdateEvent parseIntegrationUpdate(Map raw) { final guildId = Snowflake.parse(raw['guild_id']!); final integration = client.guilds[guildId].integrations.parse(raw); @@ -743,6 +792,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [IntegrationDeleteEvent] from [raw]. IntegrationDeleteEvent parseIntegrationDelete(Map raw) { return IntegrationDeleteEvent( gateway: this, @@ -752,6 +802,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [InviteCreateEvent] from [raw]. InviteCreateEvent parseInviteCreate(Map raw) { return InviteCreateEvent( gateway: this, @@ -763,6 +814,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [InviteDeleteEvent] from [raw]. InviteDeleteEvent parseInviteDelete(Map raw) { return InviteDeleteEvent( gateway: this, @@ -772,6 +824,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageCreateEvent] from [raw]. MessageCreateEvent parseMessageCreate(Map raw) { final guildId = maybeParse(raw['guild_id'], Snowflake.parse); final message = MessageManager( @@ -795,6 +848,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageUpdateEvent] from [raw]. MessageUpdateEvent parseMessageUpdate(Map raw) { final guildId = maybeParse(raw['guild_id'], Snowflake.parse); final channelId = Snowflake.parse(raw['channel_id']!); @@ -816,6 +870,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageDeleteEvent] from [raw]. MessageDeleteEvent parseMessageDelete(Map raw) { return MessageDeleteEvent( gateway: this, @@ -825,6 +880,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageBulkDeleteEvent] from [raw]. MessageBulkDeleteEvent parseMessageBulkDelete(Map raw) { return MessageBulkDeleteEvent( gateway: this, @@ -834,20 +890,22 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageReactionAddEvent] from [raw]. MessageReactionAddEvent parseMessageReactionAdd(Map raw) { final guildId = maybeParse(raw['guild_id'], Snowflake.parse); return MessageReactionAddEvent( - gateway: this, - userId: Snowflake.parse(raw['user_id']!), - channelId: Snowflake.parse(raw['channel_id']!), - messageId: Snowflake.parse(raw['message_id']!), - guildId: guildId, - member: maybeParse(raw['member'], client.guilds[guildId ?? Snowflake.zero].members.parse), - emoji: client.guilds[Snowflake.zero].emojis.parse(raw['emoji'] as Map), - ); + gateway: this, + userId: Snowflake.parse(raw['user_id']!), + channelId: Snowflake.parse(raw['channel_id']!), + messageId: Snowflake.parse(raw['message_id']!), + guildId: guildId, + member: maybeParse(raw['member'], client.guilds[guildId ?? Snowflake.zero].members.parse), + emoji: client.guilds[Snowflake.zero].emojis.parse(raw['emoji'] as Map), + messageAuthorId: maybeParse(raw['message_author_id'], Snowflake.parse)); } + /// Parse a [MessageReactionRemoveEvent] from [raw]. MessageReactionRemoveEvent parseMessageReactionRemove(Map raw) { final guildId = maybeParse(raw['guild_id'], Snowflake.parse); @@ -861,6 +919,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageReactionRemoveAllEvent] from [raw]. MessageReactionRemoveAllEvent parseMessageReactionRemoveAll(Map raw) { return MessageReactionRemoveAllEvent( gateway: this, @@ -870,6 +929,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [MessageReactionRemoveEmojiEvent] from [raw]. MessageReactionRemoveEmojiEvent parseMessageReactionRemoveEmoji(Map raw) { return MessageReactionRemoveEmojiEvent( gateway: this, @@ -880,6 +940,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [PresenceUpdateEvent] from [raw]. PresenceUpdateEvent parsePresenceUpdate(Map raw) { return PresenceUpdateEvent( gateway: this, @@ -894,6 +955,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [TypingStartEvent] from [raw]. TypingStartEvent parseTypingStart(Map raw) { var guildId = maybeParse(raw['guild_id'], Snowflake.parse); @@ -907,6 +969,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [UserUpdateEvent] from [raw]. UserUpdateEvent parseUserUpdate(Map raw) { final user = client.users.parse(raw); @@ -917,6 +980,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [VoiceStateUpdateEvent] from [raw]. VoiceStateUpdateEvent parseVoiceStateUpdate(Map raw) { final voiceState = client.voice.parseVoiceState(raw); @@ -927,6 +991,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [VoiceServerUpdateEvent] from [raw]. VoiceServerUpdateEvent parseVoiceServerUpdate(Map raw) { return VoiceServerUpdateEvent( gateway: this, @@ -936,6 +1001,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [WebhooksUpdateEvent] from [raw]. WebhooksUpdateEvent parseWebhooksUpdate(Map raw) { return WebhooksUpdateEvent( gateway: this, @@ -944,6 +1010,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [InteractionCreateEvent] from [raw]. InteractionCreateEvent> parseInteractionCreate(Map raw) { final interaction = client.interactions.parse(raw); @@ -960,6 +1027,7 @@ class Gateway extends GatewayManager with EventParser { } as InteractionCreateEvent>; } + /// Parse a [StageInstanceCreateEvent] from [raw]. StageInstanceCreateEvent parseStageInstanceCreate(Map raw) { return StageInstanceCreateEvent( gateway: this, @@ -967,6 +1035,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [StageInstanceUpdateEvent] from [raw]. StageInstanceUpdateEvent parseStageInstanceUpdate(Map raw) { final instance = client.channels.parseStageInstance(raw); @@ -977,6 +1046,7 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse a [StageInstanceDeleteEvent] from [raw]. StageInstanceDeleteEvent parseStageInstanceDelete(Map raw) { return StageInstanceDeleteEvent( gateway: this, @@ -984,6 +1054,33 @@ class Gateway extends GatewayManager with EventParser { ); } + /// Parse an [EntitlementCreateEvent] from [raw]. + EntitlementCreateEvent parseEntitlementCreate(Map raw) { + final applicationId = Snowflake.parse(raw['application_id']!); + + return EntitlementCreateEvent( + gateway: this, + entitlement: client.applications[applicationId].entitlements.parse(raw), + ); + } + + /// Parse an [EntitlementUpdateEvent] from [raw]. + EntitlementUpdateEvent parseEntitlementUpdate(Map raw) { + final applicationId = Snowflake.parse(raw['application_id']!); + final entitlement = client.applications[applicationId].entitlements.parse(raw); + + return EntitlementUpdateEvent( + gateway: this, + entitlement: entitlement, + oldEntitlement: client.applications[applicationId].entitlements.cache[entitlement.id], + ); + } + + /// Parse an [EntitlementDeleteEvent] from [raw]. + EntitlementDeleteEvent parseEntitlementDelete(Map raw) { + return EntitlementDeleteEvent(gateway: this); + } + /// Stream all members in a guild that match [query] or [userIds]. /// /// If neither is provided, all members in the guild are returned. diff --git a/lib/src/http/managers/application_manager.dart b/lib/src/http/managers/application_manager.dart index 1ea6402b3..b66eef8ec 100644 --- a/lib/src/http/managers/application_manager.dart +++ b/lib/src/http/managers/application_manager.dart @@ -1,9 +1,14 @@ +import 'dart:convert'; + +import 'package:nyxx/src/builders/application.dart'; import 'package:nyxx/src/client.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/application.dart'; +import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; +import 'package:nyxx/src/models/sku.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/team.dart'; import 'package:nyxx/src/models/user/user.dart'; @@ -32,6 +37,7 @@ class ApplicationManager { rpcOrigins: maybeParseMany(raw['rpc_origins']), isBotPublic: raw['bot_public'] as bool, botRequiresCodeGrant: raw['bot_require_code_grant'] as bool, + bot: maybeParse(raw['bot'], (Map raw) => PartialUser(id: Snowflake.parse(raw['id']!), manager: client.users)), termsOfServiceUrl: maybeParse(raw['terms_of_service_url'], Uri.parse), privacyPolicyUrl: maybeParse(raw['privacy_policy_url'], Uri.parse), owner: maybeParse( @@ -44,10 +50,14 @@ class ApplicationManager { verifyKey: raw['verify_key'] as String, team: maybeParse(raw['team'], parseTeam), guildId: maybeParse(raw['guild_id'], Snowflake.parse), + guild: maybeParse(raw['guild'], (Map raw) => PartialGuild(id: Snowflake.parse(raw['id']!), manager: client.guilds)), primarySkuId: maybeParse(raw['primary_sku_id'], Snowflake.parse), slug: raw['slug'] as String?, coverImageHash: raw['cover_image'] as String?, flags: ApplicationFlags(raw['flags'] as int? ?? 0), + approximateGuildCount: raw['approximate_guild_count'] as int?, + redirectUris: maybeParseMany(raw['redirect_uris'], Uri.parse), + interactionsEndpointUrl: maybeParse(raw['interactions_endpoint_url'], Uri.parse), tags: maybeParseMany(raw['tags']), installationParameters: maybeParse(raw['install_params'], parseInstallationParameters), customInstallUrl: maybeParse(raw['custom_install_url'], Uri.parse), @@ -71,6 +81,7 @@ class ApplicationManager { membershipState: TeamMembershipState.parse(raw['membership_state'] as int), teamId: Snowflake.parse(raw['team_id']!), user: PartialUser(id: Snowflake.parse((raw['user'] as Map)['id']!), manager: client.users), + role: TeamMemberRole.parse(raw['role'] as String), ); } @@ -98,6 +109,17 @@ class ApplicationManager { ); } + Sku parseSku(Map raw) { + return Sku( + manager: this, + id: Snowflake.parse(raw['id']!), + type: SkuType.parse(raw['type'] as int), + applicationId: Snowflake.parse(raw['application_id']!), + name: raw['name'] as String, + slug: raw['slug'] as String, + ); + } + /// Fetch an application's role connection metadata. Future> fetchApplicationRoleConnectionMetadata(Snowflake id) async { final route = HttpRoute() @@ -123,12 +145,28 @@ class ApplicationManager { } Future fetchCurrentApplication() async { - final route = HttpRoute() - ..oauth2() - ..applications(id: '@me'); + final route = HttpRoute()..applications(id: '@me'); final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); return parse(response.jsonBody as Map); } + + Future updateCurrentApplication(ApplicationUpdateBuilder builder) async { + final route = HttpRoute()..applications(id: '@me'); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); + + final response = await client.httpHandler.executeSafe(request); + return parse(response.jsonBody as Map); + } + + Future> listSkus(Snowflake id) async { + final route = HttpRoute() + ..applications(id: id.toString()) + ..skus(); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + return parseMany(response.jsonBody as List, parseSku); + } } diff --git a/lib/src/http/managers/audit_log_manager.dart b/lib/src/http/managers/audit_log_manager.dart index fd7cc3b5e..47703e5c5 100644 --- a/lib/src/http/managers/audit_log_manager.dart +++ b/lib/src/http/managers/audit_log_manager.dart @@ -52,6 +52,7 @@ class AuditLogManager extends ReadOnlyManager { messageId: maybeParse(raw['message_id'], Snowflake.parse), roleName: raw['role_name'] as String?, overwriteType: maybeParse(raw['type'], (String raw) => PermissionOverwriteType.parse(int.parse(raw))), + integrationType: raw['integration_type'] as String?, ); } diff --git a/lib/src/http/managers/channel_manager.dart b/lib/src/http/managers/channel_manager.dart index 9d273328d..01a9dc993 100644 --- a/lib/src/http/managers/channel_manager.dart +++ b/lib/src/http/managers/channel_manager.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' show MultipartFile; +import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/builders/channel/stage_instance.dart'; import 'package:nyxx/src/builders/channel/thread.dart'; @@ -76,6 +77,7 @@ class ChannelManager extends ReadOnlyManager { ChannelType.guildStageVoice: parseGuildStageChannel, ChannelType.guildDirectory: parseDirectoryChannel, ChannelType.guildForum: parseForumChannel, + ChannelType.guildMedia: parseGuildMediaChannel, }; return parsers[type]!(raw, guildId: guildId); @@ -320,12 +322,42 @@ class ChannelManager extends ReadOnlyManager { return ForumChannel( id: Snowflake.parse(raw['id']!), manager: this, + defaultLayout: maybeParse(raw['default_forum_layout'], ForumLayout.parse), topic: raw['topic'] as String?, + rateLimitPerUser: maybeParse(raw['rate_limit_per_user'], (value) => value == 0 ? null : Duration(seconds: value)), + lastThreadId: maybeParse(raw['last_message_id'], Snowflake.parse), + lastPinTimestamp: maybeParse(raw['last_pin_timestamp'], DateTime.parse), + flags: ChannelFlags(raw['flags'] as int), + availableTags: parseMany(raw['available_tags'] as List, parseForumTag), + defaultReaction: maybeParse(raw['default_reaction_emoji'], parseDefaultReaction), + defaultSortOrder: maybeParse(raw['default_sort_order'], ForumSort.parse), + // Discord doesn't seem to include this field if the default 3 day expiration is used (3 days = 4320 minutes) + defaultAutoArchiveDuration: Duration(minutes: raw['default_auto_archive_duration'] as int? ?? 4320), + defaultThreadRateLimitPerUser: + maybeParse(raw['default_thread_rate_limit_per_user'], (value) => value == 0 ? null : Duration(seconds: value)), + guildId: guildId ?? Snowflake.parse(raw['guild_id']!), + isNsfw: raw['nsfw'] as bool? ?? false, + name: raw['name'] as String, + parentId: maybeParse(raw['parent_id'], Snowflake.parse), + permissionOverwrites: maybeParseMany(raw['permission_overwrites'], parsePermissionOverwrite) ?? [], + position: raw['position'] as int, + ); + } + + GuildMediaChannel parseGuildMediaChannel(Map raw, {Snowflake? guildId}) { + assert(raw['type'] == ChannelType.guildMedia.value, 'Invalid type for GuildMediaChannel'); + + return GuildMediaChannel( + id: Snowflake.parse(raw['id']!), + manager: this, + topic: raw['topic'] as String?, + rateLimitPerUser: maybeParse(raw['rate_limit_per_user'], (value) => value == 0 ? null : Duration(seconds: value)), lastThreadId: maybeParse(raw['last_message_id'], Snowflake.parse), lastPinTimestamp: maybeParse(raw['last_pin_timestamp'], DateTime.parse), flags: ChannelFlags(raw['flags'] as int), availableTags: parseMany(raw['available_tags'] as List, parseForumTag), defaultReaction: maybeParse(raw['default_reaction_emoji'], parseDefaultReaction), + defaultSortOrder: maybeParse(raw['default_sort_order'], ForumSort.parse), // Discord doesn't seem to include this field if the default 3 day expiration is used (3 days = 4320 minutes) defaultAutoArchiveDuration: Duration(minutes: raw['default_auto_archive_duration'] as int? ?? 4320), defaultThreadRateLimitPerUser: diff --git a/lib/src/http/managers/entitlement_manager.dart b/lib/src/http/managers/entitlement_manager.dart new file mode 100644 index 000000000..f383df7b2 --- /dev/null +++ b/lib/src/http/managers/entitlement_manager.dart @@ -0,0 +1,104 @@ +import 'dart:convert'; + +import 'package:nyxx/src/builders/entitlement.dart'; +import 'package:nyxx/src/cache/cache.dart'; +import 'package:nyxx/src/errors.dart'; +import 'package:nyxx/src/http/managers/manager.dart'; +import 'package:nyxx/src/http/request.dart'; +import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/entitlement.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/parsing_helpers.dart'; + +/// A [Manager] for [Entitlement]s. +class EntitlementManager extends ReadOnlyManager { + /// The ID of the application this manager is for. + final Snowflake applicationId; + + /// Create a new [EntitlementManager]. + EntitlementManager(super.config, super.client, {required this.applicationId}) : super(identifier: '$applicationId.entitlements'); + + @override + PartialEntitlement operator [](Snowflake id) => PartialEntitlement(manager: this, id: id); + + @override + Entitlement parse(Map raw) { + return Entitlement( + manager: this, + id: Snowflake.parse(raw['id']!), + skuId: Snowflake.parse(raw['sku_id']!), + userId: maybeParse(raw['user_id'], Snowflake.parse), + guildId: maybeParse(raw['guild_id'], Snowflake.parse), + applicationId: Snowflake.parse(raw['application_id']!), + type: EntitlementType.parse(raw['type'] as int), + isConsumed: raw['consumed'] as bool, + startsAt: maybeParse(raw['starts_at'], DateTime.parse), + endsAt: maybeParse(raw['ends_at'], DateTime.parse), + ); + } + + /// List all the entitlements for this application. + Future> list({ + Snowflake? userId, + List? skuIds, + Snowflake? before, + Snowflake? after, + int? limit, + Snowflake? guildId, + bool? excludeEnded, + }) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..entitlements(); + final request = BasicRequest(route, queryParameters: { + if (userId != null) 'user_id': userId.toString(), + if (skuIds != null) 'sku_ids': skuIds.join(','), + if (before != null) 'before': before.toString(), + if (after != null) 'after': after.toString(), + if (limit != null) 'limit': limit.toString(), + if (guildId != null) 'guild_id': guildId.toString(), + if (excludeEnded != null) 'exclude_ended': excludeEnded.toString(), + }); + + final response = await client.httpHandler.executeSafe(request); + final entitlements = parseMany(response.jsonBody as List, parse); + + cache.addEntities(entitlements); + return entitlements; + } + + @override + Future fetch(Snowflake id) async { + final entitlements = await list(before: Snowflake(id.value + 1)); + + return entitlements.firstWhere( + (entitlement) => entitlement.id == id, + orElse: () => throw EntitlementNotFoundException(applicationId, id), + ); + } + + /// Create a test entitlement that never expires. + Future createTestEntitlement(TestEntitlementBuilder builder) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..entitlements(); + final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build())); + + final response = await client.httpHandler.executeSafe(request); + final entitlement = parse(response.jsonBody as Map); + + cache[entitlement.id] = entitlement; + return entitlement; + } + + /// Delete a test entitlement. + Future deleteTestEntitlement(Snowflake id) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..entitlements(id: id.toString()); + final request = BasicRequest(route, method: 'DELETE'); + + await client.httpHandler.executeSafe(request); + cache.remove(id); + } +} diff --git a/lib/src/http/managers/guild_manager.dart b/lib/src/http/managers/guild_manager.dart index d09835384..f53373de4 100644 --- a/lib/src/http/managers/guild_manager.dart +++ b/lib/src/http/managers/guild_manager.dart @@ -87,6 +87,7 @@ class GuildManager extends Manager { hasPremiumProgressBarEnabled: raw['premium_progress_bar_enabled'] as bool, emojiList: parseMany(raw['emojis'] as List, this[id].emojis.parse), stickerList: parseMany(raw['stickers'] as List? ?? [], this[id].stickers.parse), + safetyAlertsChannelId: maybeParse(raw['safety_alerts_channel_id'], Snowflake.parse), ); } diff --git a/lib/src/http/managers/interaction_manager.dart b/lib/src/http/managers/interaction_manager.dart index 18d4fb2e0..88b46637f 100644 --- a/lib/src/http/managers/interaction_manager.dart +++ b/lib/src/http/managers/interaction_manager.dart @@ -45,6 +45,7 @@ class InteractionManager { final appPermissions = maybeParse(raw['app_permissions'], (String raw) => Permissions(int.parse(raw))); final locale = maybeParse(raw['locale'], Locale.parse); final guildLocale = maybeParse(raw['guild_locale'], Locale.parse); + final entitlements = parseMany(raw['entitlements'] as List, client.applications[applicationId].entitlements.parse); return switch (type) { InteractionType.ping => PingInteraction( @@ -63,6 +64,7 @@ class InteractionManager { appPermissions: appPermissions, locale: locale, guildLocale: guildLocale, + entitlements: entitlements, ), InteractionType.applicationCommand => ApplicationCommandInteraction( manager: this, @@ -81,6 +83,7 @@ class InteractionManager { appPermissions: appPermissions, locale: locale, guildLocale: guildLocale, + entitlements: entitlements, ), InteractionType.messageComponent => MessageComponentInteraction( manager: this, @@ -99,6 +102,7 @@ class InteractionManager { appPermissions: appPermissions, locale: locale, guildLocale: guildLocale, + entitlements: entitlements, ), InteractionType.modalSubmit => ModalSubmitInteraction( manager: this, @@ -117,6 +121,7 @@ class InteractionManager { appPermissions: appPermissions, locale: locale, guildLocale: guildLocale, + entitlements: entitlements, ), InteractionType.applicationCommandAutocomplete => ApplicationCommandAutocompleteInteraction( manager: this, @@ -135,6 +140,7 @@ class InteractionManager { appPermissions: appPermissions, locale: locale, guildLocale: guildLocale, + entitlements: entitlements, ), } as Interaction; } @@ -218,6 +224,7 @@ class InteractionManager { customId: raw['custom_id'] as String, type: MessageComponentType.parse(raw['component_type'] as int), values: maybeParseMany(raw['values']), + resolved: maybeParse(raw['resolved'], parseResolvedData), ); } diff --git a/lib/src/http/managers/message_manager.dart b/lib/src/http/managers/message_manager.dart index d093cb911..1a3eb54bd 100644 --- a/lib/src/http/managers/message_manager.dart +++ b/lib/src/http/managers/message_manager.dart @@ -81,6 +81,7 @@ class MessageManager extends Manager { position: raw['position'] as int?, roleSubscriptionData: maybeParse(raw['role_subscription_data'], parseRoleSubscriptionData), stickers: parseMany(raw['sticker_items'] as List? ?? [], client.stickers.parseStickerItem), + resolved: maybeParse(raw['resolved'], client.interactions.parseResolvedData), ); } @@ -107,6 +108,9 @@ class MessageManager extends Manager { height: raw['height'] as int?, width: raw['width'] as int?, isEphemeral: raw['ephemeral'] as bool? ?? false, + duration: maybeParse(raw['duration_secs'], (double value) => Duration(microseconds: (value * Duration.microsecondsPerSecond).floor())), + waveform: maybeParse(raw['waveform'], base64.decode), + flags: maybeParse(raw['flags'], AttachmentFlags.new), ); } @@ -189,8 +193,18 @@ class MessageManager extends Manager { Reaction parseReaction(Map raw) { return Reaction( count: raw['count'] as int, + countDetails: parseReactionCountDetails(raw['count_details'] as Map), me: raw['me'] as bool, + meBurst: raw['me_burst'] as bool, emoji: client.guilds[Snowflake.zero].emojis.parse(raw['emoji'] as Map), + burstColors: parseMany(raw['burst_colors'] as List, DiscordColor.parseHexString), + ); + } + + ReactionCountDetails parseReactionCountDetails(Map raw) { + return ReactionCountDetails( + burst: raw['burst'] as int, + normal: raw['normal'] as int, ); } diff --git a/lib/src/http/managers/role_manager.dart b/lib/src/http/managers/role_manager.dart index 67fed3611..564d8de95 100644 --- a/lib/src/http/managers/role_manager.dart +++ b/lib/src/http/managers/role_manager.dart @@ -37,6 +37,7 @@ class RoleManager extends Manager { permissions: Permissions(int.parse(raw['permissions'] as String)), isMentionable: raw['mentionable'] as bool, tags: maybeParse(raw['tags'], parseRoleTags), + flags: RoleFlags(raw['flags'] as int), ); } diff --git a/lib/src/http/managers/user_manager.dart b/lib/src/http/managers/user_manager.dart index 25be9109f..62470bb67 100644 --- a/lib/src/http/managers/user_manager.dart +++ b/lib/src/http/managers/user_manager.dart @@ -51,6 +51,7 @@ class UserManager extends ReadOnlyManager { flags: hasFlags ? UserFlags(raw['flags'] as int) : null, nitroType: hasPremiumType ? NitroType.parse(raw['premium_type'] as int) : NitroType.none, publicFlags: hasPublicFlags ? UserFlags(raw['public_flags'] as int) : null, + avatarDecorationHash: raw['avatar_decoration'] as String?, ); } @@ -125,7 +126,7 @@ class UserManager extends ReadOnlyManager { } /// List the guilds the current user is a member of. - Future> listCurrentUserGuilds({Snowflake? after, Snowflake? before, int? limit}) async { + Future> listCurrentUserGuilds({Snowflake? after, Snowflake? before, int? limit, bool? withCounts}) async { final route = HttpRoute() ..users(id: '@me') ..guilds(); @@ -133,6 +134,7 @@ class UserManager extends ReadOnlyManager { if (before != null) 'before': before.toString(), if (after != null) 'after': after.toString(), if (limit != null) 'limit': limit.toString(), + if (withCounts != null) 'with_counts': withCounts.toString(), }); final response = await client.httpHandler.executeSafe(request); diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 65df5b3d2..16d91b545 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -302,4 +302,13 @@ extension RouteHelpers on HttpRoute { /// Adds the [`callback`](https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response) part to this [HttpRoute]. void callback() => add(HttpRoutePart('callback')); + + /// Adds the [`entitlements`](https://discord.com/developers/docs/monetization/entitlements#list-entitlements) part to this [HttpRoute]. + void entitlements({String? id}) => add(HttpRoutePart('entitlements', [if (id != null) HttpRouteParam(id)])); + + /// Adds the [`skus`](https://discord.com/developers/docs/monetization/skus#list-skus) part to this [HttpRoute]. + void skus() => add(HttpRoutePart('skus')); + + /// Adds the [`avatar-decorations`](https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints) part to this [HttpRoute]. + void avatarDecorations({String? id}) => add(HttpRoutePart('avatar-decorations', [if (id != null) HttpRouteParam(id)])); } diff --git a/lib/src/models/application.dart b/lib/src/models/application.dart index c10f2142d..4cbe2c51f 100644 --- a/lib/src/models/application.dart +++ b/lib/src/models/application.dart @@ -1,9 +1,11 @@ import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/application_manager.dart'; +import 'package:nyxx/src/http/managers/entitlement_manager.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; +import 'package:nyxx/src/models/sku.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/team.dart'; import 'package:nyxx/src/models/user/user.dart'; @@ -20,6 +22,9 @@ class PartialApplication with ToStringHelper { /// The manager for this application. final ApplicationManager manager; + /// An [EntitlementManager] for this application's [Entitlement]s. + EntitlementManager get entitlements => EntitlementManager(manager.client.options.entitlementConfig, manager.client, applicationId: id); + /// Create a new [PartialApplication]. PartialApplication({required this.id, required this.manager}); @@ -28,6 +33,9 @@ class PartialApplication with ToStringHelper { /// Update and fetch this application's role connection metadata. Future> updateRoleConnectionMetadata() => manager.updateApplicationRoleConnectionMetadata(id); + + /// List this application's SKUs. + Future> listSkus() => manager.listSkus(id); } /// {@template application} @@ -52,6 +60,9 @@ class Application extends PartialApplication { /// Whether the bot account associated with this application requires the OAuth2 code grant to be completed before joining a guild. final bool botRequiresCodeGrant; + /// The bot user associated with the application. + final PartialUser? bot; + /// The URL of this application's Terms of Service. final Uri? termsOfServiceUrl; @@ -67,9 +78,12 @@ class Application extends PartialApplication { /// If this application belongs to a team, the team which owns this app. final Team? team; - /// If this application is a game sold on Discord, the ID of the guild it was linked to. + /// The ID of the guild associated with this application. final Snowflake? guildId; + /// The guild associated with this application. + final PartialGuild? guild; + /// If this application is a game sold on Discord, the ID of the "Game SKU" that is created, if it exists. final Snowflake? primarySkuId; @@ -82,6 +96,15 @@ class Application extends PartialApplication { /// The public flags for this application. final ApplicationFlags flags; + /// The approximate number of guilds this bot has been added to. + final int? approximateGuildCount; + + /// The list of redirect URIs for this application. + final List? redirectUris; + + /// The interactions endpoint URL for this application. + final Uri? interactionsEndpointUrl; + /// Up to 5 tags describing this application. final List? tags; @@ -106,25 +129,27 @@ class Application extends PartialApplication { required this.rpcOrigins, required this.isBotPublic, required this.botRequiresCodeGrant, + required this.bot, required this.termsOfServiceUrl, required this.privacyPolicyUrl, required this.owner, required this.verifyKey, required this.team, required this.guildId, + required this.guild, required this.primarySkuId, required this.slug, required this.coverImageHash, required this.flags, + required this.approximateGuildCount, + required this.redirectUris, + required this.interactionsEndpointUrl, required this.tags, required this.installationParameters, required this.customInstallUrl, required this.roleConnectionsVerificationUrl, }); - /// If this application is a game sold on Discord, the guild it was linked to. - PartialGuild? get guild => guildId == null ? null : manager.client.guilds[guildId!]; - /// This application's icon. CdnAsset? get icon => iconHash == null ? null diff --git a/lib/src/models/channel/channel.dart b/lib/src/models/channel/channel.dart index 142de5f73..8321c4606 100644 --- a/lib/src/models/channel/channel.dart +++ b/lib/src/models/channel/channel.dart @@ -81,7 +81,10 @@ enum ChannelType { guildDirectory._(14), /// A forum channel in a [Guild]. - guildForum._(15); + guildForum._(15), + + /// A media channel in a [Guild]. + guildMedia._(16); /// The value of this [ChannelType]. final int value; @@ -109,12 +112,18 @@ class ChannelFlags extends Flags { /// The forum channel requires threads to have tags. static const requireTag = Flag.fromOffset(4); + /// The media channel hides embedded media download options. + static const hideMediaDownloadOptions = Flag.fromOffset(15); + /// Whether this channel has the [pinned] flag set. bool get isPinned => has(pinned); /// Whether this channel has the [requireTag] flag set. bool get requiresTag => has(requireTag); + /// Whether this channel has the [hideMediaDownloadOptions] flag set. + bool get hidesMediaDownloadOptions => has(hideMediaDownloadOptions); + /// Create a new [ChannelFlags]. const ChannelFlags(super.value); } diff --git a/lib/src/models/channel/thread_aggregate.dart b/lib/src/models/channel/thread_aggregate.dart new file mode 100644 index 000000000..2d823a916 --- /dev/null +++ b/lib/src/models/channel/thread_aggregate.dart @@ -0,0 +1,37 @@ +import 'package:nyxx/nyxx.dart'; + +abstract class ThreadsOnlyChannel implements HasThreadsChannel { + /// The topic of this channel. + String? get topic; + + /// The rate limit duration of this channel per user. + /// + /// Does not apply to threads created in this channel. + /// See [HasThreadsChannel.defaultThreadRateLimitPerUser] for that. + Duration? get rateLimitPerUser; + + /// The ID of the last [Thread] created. + Snowflake? get lastThreadId; + + /// The time at which the last message was pinned. + DateTime? get lastPinTimestamp; + + /// Any flags applied to this channel. + ChannelFlags get flags; + + /// A list of tags available in this channel. + List get availableTags; + + /// The default reaction for this channel. + DefaultReaction? get defaultReaction; + + /// The default sort order in this channel + ForumSort? get defaultSortOrder; + + /// Create a thread in this thread aggregate channel. + /// + /// External references: + /// * [ChannelManager.createForumThread] + /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#start-thread-in-forum-channel + Future createForumThread(ForumThreadBuilder builder); +} diff --git a/lib/src/models/channel/types/forum.dart b/lib/src/models/channel/types/forum.dart index daf87de97..2f65bf1d4 100644 --- a/lib/src/models/channel/types/forum.dart +++ b/lib/src/models/channel/types/forum.dart @@ -3,8 +3,8 @@ import 'package:nyxx/src/builders/invite.dart'; import 'package:nyxx/src/builders/permission_overwrite.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/channel/guild_channel.dart'; -import 'package:nyxx/src/models/channel/has_threads_channel.dart'; import 'package:nyxx/src/models/channel/thread.dart'; +import 'package:nyxx/src/models/channel/thread_aggregate.dart'; import 'package:nyxx/src/models/channel/thread_list.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/invite/invite.dart'; @@ -17,25 +17,34 @@ import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template forum_channel} /// A forum channel. /// {@endtemplate} -class ForumChannel extends Channel implements GuildChannel, HasThreadsChannel { - /// The topic of this channel. +class ForumChannel extends Channel implements GuildChannel, ThreadsOnlyChannel { + /// The default layout in this channel + final ForumLayout? defaultLayout; + + @override final String? topic; - /// The ID of the last [Thread] created. + @override + final Duration? rateLimitPerUser; + + @override final Snowflake? lastThreadId; - /// The time at which the last message was pinned. + @override final DateTime? lastPinTimestamp; - /// Any flags applied to this channel. + @override final ChannelFlags flags; - /// A list of tags available in this channel. + @override final List availableTags; - /// The default reaction for this channel. + @override final DefaultReaction? defaultReaction; + @override + final ForumSort? defaultSortOrder; + @override final Duration defaultAutoArchiveDuration; @@ -67,12 +76,15 @@ class ForumChannel extends Channel implements GuildChannel, HasThreadsChannel { ForumChannel({ required super.id, required super.manager, + required this.defaultLayout, required this.topic, + required this.rateLimitPerUser, required this.lastThreadId, required this.lastPinTimestamp, required this.flags, required this.availableTags, required this.defaultReaction, + required this.defaultSortOrder, required this.defaultAutoArchiveDuration, required this.defaultThreadRateLimitPerUser, required this.guildId, @@ -89,11 +101,7 @@ class ForumChannel extends Channel implements GuildChannel, HasThreadsChannel { @override PartialChannel? get parent => parentId == null ? null : manager.client.channels[parentId!]; - /// Create a thread in this forum channel. - /// - /// External references: - /// * [ChannelManager.createForumThread] - /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#start-thread-in-forum-channel + @override Future createForumThread(ForumThreadBuilder builder) => manager.createForumThread(id, builder); @override @@ -182,6 +190,14 @@ enum ForumSort { const ForumSort._(this.value); + /// Parse a [ForumSort] from an [int]. + /// + /// The [value] must be a valid forum sort. + factory ForumSort.parse(int value) => ForumSort.values.firstWhere( + (sort) => sort.value == value, + orElse: () => throw FormatException('Unknown forum sort', value), + ); + @override String toString() => 'ForumSort($value)'; } @@ -197,6 +213,14 @@ enum ForumLayout { const ForumLayout._(this.value); + /// Parse a [ForumLayout] from an [int]. + /// + /// The [value] must be a valid forum layout. + factory ForumLayout.parse(int value) => ForumLayout.values.firstWhere( + (layout) => layout.value == value, + orElse: () => throw FormatException('Unknown forum layout', value), + ); + @override String toString() => 'ForumLayout($value)'; } diff --git a/lib/src/models/channel/types/guild_media.dart b/lib/src/models/channel/types/guild_media.dart new file mode 100644 index 000000000..0a885cece --- /dev/null +++ b/lib/src/models/channel/types/guild_media.dart @@ -0,0 +1,134 @@ +import 'package:nyxx/src/builders/channel/thread.dart'; +import 'package:nyxx/src/builders/invite.dart'; +import 'package:nyxx/src/builders/permission_overwrite.dart'; +import 'package:nyxx/src/models/channel/channel.dart'; +import 'package:nyxx/src/models/channel/guild_channel.dart'; +import 'package:nyxx/src/models/channel/thread.dart'; +import 'package:nyxx/src/models/channel/thread_aggregate.dart'; +import 'package:nyxx/src/models/channel/thread_list.dart'; +import 'package:nyxx/src/models/channel/types/forum.dart'; +import 'package:nyxx/src/models/guild/guild.dart'; +import 'package:nyxx/src/models/invite/invite.dart'; +import 'package:nyxx/src/models/invite/invite_metadata.dart'; +import 'package:nyxx/src/models/permission_overwrite.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/models/webhook.dart'; + +/// {@template guild_media_channel} +/// A channel in a guild in which threads can be posted, similarly to a [ForumChannel]. +/// {@endtemplate} +class GuildMediaChannel extends Channel implements GuildChannel, ThreadsOnlyChannel { + @override + final String? topic; + + @override + final Duration? rateLimitPerUser; + + @override + final Snowflake? lastThreadId; + + @override + final DateTime? lastPinTimestamp; + + @override + final ChannelFlags flags; + + @override + final List availableTags; + + @override + final DefaultReaction? defaultReaction; + + @override + final ForumSort? defaultSortOrder; + + @override + final Duration defaultAutoArchiveDuration; + + @override + final Duration? defaultThreadRateLimitPerUser; + + @override + final Snowflake guildId; + + @override + final bool isNsfw; + + @override + final String name; + + @override + final Snowflake? parentId; + + @override + final List permissionOverwrites; + + @override + final int position; + + @override + ChannelType get type => ChannelType.guildForum; + + /// {@macro guild_media_channel} + GuildMediaChannel({ + required super.id, + required super.manager, + required this.topic, + required this.rateLimitPerUser, + required this.lastThreadId, + required this.lastPinTimestamp, + required this.flags, + required this.availableTags, + required this.defaultReaction, + required this.defaultSortOrder, + required this.defaultAutoArchiveDuration, + required this.defaultThreadRateLimitPerUser, + required this.guildId, + required this.isNsfw, + required this.name, + required this.parentId, + required this.permissionOverwrites, + required this.position, + }); + + @override + PartialGuild get guild => manager.client.guilds[guildId]; + + @override + PartialChannel? get parent => parentId == null ? null : manager.client.channels[parentId!]; + + @override + Future createForumThread(ForumThreadBuilder builder) => manager.createForumThread(id, builder); + + @override + Future createThread(ThreadBuilder builder) => throw UnsupportedError('Cannot create a non forum thread in a forum channel'); + + @override + Future createThreadFromMessage(Snowflake messageId, ThreadFromMessageBuilder builder) => + throw UnsupportedError('Cannot create a non forum thread in a forum channel'); + + @override + Future deletePermissionOverwrite(Snowflake id) => manager.deletePermissionOverwrite(this.id, id); + + @override + Future listPrivateArchivedThreads({DateTime? before, int? limit}) => manager.listPrivateArchivedThreads(id, before: before, limit: limit); + + @override + Future listPublicArchivedThreads({DateTime? before, int? limit}) => manager.listPublicArchivedThreads(id, before: before, limit: limit); + + @override + Future listJoinedPrivateArchivedThreads({DateTime? before, int? limit}) => + manager.listJoinedPrivateArchivedThreads(id, before: before, limit: limit); + + @override + Future updatePermissionOverwrite(PermissionOverwriteBuilder builder) => manager.updatePermissionOverwrite(id, builder); + + @override + Future> fetchWebhooks() => manager.client.webhooks.fetchChannelWebhooks(id); + + @override + Future> listInvites() => manager.listInvites(id); + + @override + Future createInvite(InviteBuilder builder, {String? auditLogReason}) => manager.createInvite(id, builder, auditLogReason: auditLogReason); +} diff --git a/lib/src/models/entitlement.dart b/lib/src/models/entitlement.dart new file mode 100644 index 000000000..428a4b2fc --- /dev/null +++ b/lib/src/models/entitlement.dart @@ -0,0 +1,84 @@ +import 'package:nyxx/src/http/managers/entitlement_manager.dart'; +import 'package:nyxx/src/models/application.dart'; +import 'package:nyxx/src/models/guild/guild.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/models/user/user.dart'; + +/// A partial [Entitlement]. +class PartialEntitlement extends ManagedSnowflakeEntity { + @override + final EntitlementManager manager; + + /// Create a new [PartialEntitlement]. + PartialEntitlement({required this.manager, required super.id}); +} + +/// {@template entitlement} +/// Premium access a user or guild has for an application. +/// {@endtemplate} +class Entitlement extends PartialEntitlement { + /// The ID of the SKU. + final Snowflake skuId; + + /// The ID of the [User] this [Entitlement] is for. + final Snowflake? userId; + + /// The ID of the [Guild] this [Entitlement] is for. + final Snowflake? guildId; + + /// The ID of the [Application] this [Entitlement] is for. + final Snowflake applicationId; + + /// The type of this entitlement. + final EntitlementType type; + + /// Whether this entitlement is consumed. + final bool isConsumed; + + /// The time at which this entitlement becomes valid. + final DateTime? startsAt; + + /// The time at which this entitlement expires. + final DateTime? endsAt; + + /// {@macro entitlement} + Entitlement({ + required super.manager, + required super.id, + required this.skuId, + required this.userId, + required this.guildId, + required this.applicationId, + required this.type, + required this.isConsumed, + required this.startsAt, + required this.endsAt, + }); + + /// The user this entitlement is for. + PartialUser? get user => userId == null ? null : PartialUser(id: userId!, manager: manager.client.users); + + /// The guild this entitlement is for. + PartialGuild? get guild => guildId == null ? null : PartialGuild(id: guildId!, manager: manager.client.guilds); + + /// The application this entitlement is for. + PartialApplication get application => PartialApplication(id: applicationId, manager: manager.client.applications); +} + +/// The type of an [Entitlement]. +enum EntitlementType { + applicationSubscription._(8); + + final int value; + + const EntitlementType._(this.value); + + factory EntitlementType.parse(int value) => EntitlementType.values.firstWhere( + (element) => element.value == value, + orElse: () => throw FormatException('Unknown entitlement type', value), + ); + + @override + String toString() => 'EntitlementType($value)'; +} diff --git a/lib/src/models/gateway/events/entitlement.dart b/lib/src/models/gateway/events/entitlement.dart new file mode 100644 index 000000000..11fd7e6f8 --- /dev/null +++ b/lib/src/models/gateway/events/entitlement.dart @@ -0,0 +1,37 @@ +import 'package:nyxx/src/models/entitlement.dart'; +import 'package:nyxx/src/models/gateway/event.dart'; + +/// {@template entitlement_create_event} +/// Emitted when an entitlement is created. +/// {@endtemplate} +class EntitlementCreateEvent extends DispatchEvent { + /// The entitlement that was created, + final Entitlement entitlement; + + /// {@macro entitlement_create_event} + EntitlementCreateEvent({required super.gateway, required this.entitlement}); +} + +/// {@template entitlement_update_event} +/// Emitted when an entitlement is updated. +/// {@endtemplate} +class EntitlementUpdateEvent extends DispatchEvent { + /// The updated entitlement. + final Entitlement entitlement; + + /// The entitlement as it was cached before it was updated. + final Entitlement? oldEntitlement; + + /// {@macro entitlement_update_event} + EntitlementUpdateEvent({required super.gateway, required this.entitlement, required this.oldEntitlement}); +} + +/// {@template entitlement_delete_event} +/// Emitted when an entitlement is deleted. +/// {@endtemplate} +class EntitlementDeleteEvent extends DispatchEvent { + // TODO: What is the payload here? + + /// {@macro entitlement_delete_event} + EntitlementDeleteEvent({required super.gateway}); +} diff --git a/lib/src/models/gateway/events/message.dart b/lib/src/models/gateway/events/message.dart index 1b8c13295..d7606dbdf 100644 --- a/lib/src/models/gateway/events/message.dart +++ b/lib/src/models/gateway/events/message.dart @@ -131,6 +131,9 @@ class MessageReactionAddEvent extends DispatchEvent { /// The emoji that was added. final Emoji emoji; + /// The ID of the user that sent the message the reaction was added to. + final Snowflake? messageAuthorId; + /// {@macro message_reaction_add_event} MessageReactionAddEvent({ required super.gateway, @@ -140,6 +143,7 @@ class MessageReactionAddEvent extends DispatchEvent { required this.guildId, required this.member, required this.emoji, + required this.messageAuthorId, }); /// The guild the message is in. @@ -153,6 +157,9 @@ class MessageReactionAddEvent extends DispatchEvent { /// The message the reaction was added to. PartialMessage get message => channel.messages[messageId]; + + /// The user that sent the message the reaction was added to + PartialUser? get messageAuthor => messageAuthorId == null ? null : gateway.client.users[messageAuthorId!]; } /// {@template message_reaction_remove_event} diff --git a/lib/src/models/guild/audit_log.dart b/lib/src/models/guild/audit_log.dart index 806647c91..247e07746 100644 --- a/lib/src/models/guild/audit_log.dart +++ b/lib/src/models/guild/audit_log.dart @@ -132,7 +132,9 @@ enum AuditLogEvent { autoModerationRuleDelete._(142), autoModerationBlockMessage._(143), autoModerationFlagToChannel._(144), - autoModerationUserCommunicationDisabled._(145); + autoModerationUserCommunicationDisabled._(145), + creatorMonetizationRequestCreated._(150), + creatorMonetizationTermsAccepted._(151); /// The value of this [AuditLogEvent]. final int value; @@ -191,6 +193,9 @@ class AuditLogEntryInfo with ToStringHelper { // The type of overwrite that was targeted. final PermissionOverwriteType? overwriteType; + /// The type of integration that performed the action. + final String? integrationType; + /// {@macro audit_log_entry_info} AuditLogEntryInfo({ required this.manager, @@ -205,6 +210,7 @@ class AuditLogEntryInfo with ToStringHelper { required this.messageId, required this.roleName, required this.overwriteType, + required this.integrationType, }); /// The application whose permissions were targeted. diff --git a/lib/src/models/guild/guild.dart b/lib/src/models/guild/guild.dart index c47908e73..4a54fab7a 100644 --- a/lib/src/models/guild/guild.dart +++ b/lib/src/models/guild/guild.dart @@ -329,6 +329,9 @@ class Guild extends PartialGuild { // Renamed to avoid conflict with the stickers manager. final List stickerList; + /// The ID of the channel safety alerts are sent to. + final Snowflake? safetyAlertsChannelId; + /// {@macro guild} Guild({ required super.id, @@ -372,6 +375,7 @@ class Guild extends PartialGuild { required this.hasPremiumProgressBarEnabled, required this.emojiList, required this.stickerList, + required this.safetyAlertsChannelId, }); /// The owner of the guild. @@ -399,6 +403,9 @@ class Guild extends PartialGuild { PartialTextChannel? get publicUpdatesChannel => publicUpdatesChannelId == null ? null : manager.client.channels[publicUpdatesChannelId!] as PartialTextChannel?; + /// The channel safety alerts are sent to. + PartialTextChannel? get safetyAlertsChannel => safetyAlertsChannelId == null ? null : manager.client.channels[safetyAlertsChannelId!] as PartialTextChannel; + /// This guild's icon. CdnAsset? get icon => iconHash == null ? null diff --git a/lib/src/models/interaction.dart b/lib/src/models/interaction.dart index 439837f16..ddafecd95 100644 --- a/lib/src/models/interaction.dart +++ b/lib/src/models/interaction.dart @@ -7,6 +7,7 @@ import 'package:nyxx/src/http/managers/interaction_manager.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/commands/application_command.dart'; import 'package:nyxx/src/models/commands/application_command_option.dart'; +import 'package:nyxx/src/models/entitlement.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/guild/member.dart'; import 'package:nyxx/src/models/locale.dart'; @@ -72,6 +73,9 @@ abstract class Interaction with ToStringHelper { /// The preferred locale of the guild in which this interaction was triggered. final Locale? guildLocale; + /// The entitlements for the user and guild of this interaction. + final List entitlements; + /// {@macro interaction} Interaction({ required this.manager, @@ -90,6 +94,7 @@ abstract class Interaction with ToStringHelper { required this.appPermissions, required this.locale, required this.guildLocale, + required this.entitlements, }); /// The guild in which this interaction was triggered. @@ -191,6 +196,7 @@ class PingInteraction extends Interaction { required super.appPermissions, required super.locale, required super.guildLocale, + required super.entitlements, }) : super(data: null); /// Send a pong response to this interaction. @@ -220,6 +226,7 @@ class ApplicationCommandInteraction extends Interaction wit required super.appPermissions, required super.locale, required super.guildLocale, + required super.entitlements, }); } @@ -355,6 +364,7 @@ class ApplicationCommandAutocompleteInteraction extends Interaction? values; + /// Additional data about entities in the payload. + final ResolvedData? resolved; + /// {@macro message_component_interaction_data} - MessageComponentInteractionData({required this.customId, required this.type, required this.values}); + MessageComponentInteractionData({required this.customId, required this.type, required this.values, required this.resolved}); } /// {@template modal_submit_interaction_data} diff --git a/lib/src/models/message/attachment.dart b/lib/src/models/message/attachment.dart index 017aa3558..27fe26bf7 100644 --- a/lib/src/models/message/attachment.dart +++ b/lib/src/models/message/attachment.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/message_manager.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template attachment} @@ -51,6 +52,15 @@ class Attachment with ToStringHelper implements CdnAsset { /// Whether this attachment is ephemeral. final bool isEphemeral; + /// The duration of this audio file for voice messages. + final Duration? duration; + + /// A sampled waveform for voice messages. + final List? waveform; + + /// This attachment's flags. + final AttachmentFlags? flags; + @override Nyxx get client => manager.client; @@ -79,6 +89,9 @@ class Attachment with ToStringHelper implements CdnAsset { required this.height, required this.width, required this.isEphemeral, + required this.duration, + required this.waveform, + required this.flags, }); @override @@ -101,3 +114,15 @@ class Attachment with ToStringHelper implements CdnAsset { yield* response.stream; } } + +/// The flags for an [Attachment]. +class AttachmentFlags extends Flags { + /// The attachment is a remix. + static const isRemix = Flag.fromOffset(2); + + /// Whether this set of flags has the [isRemix] flag. + bool get isARemix => has(isRemix); + + /// Create a new [AttachmentFlags]. + const AttachmentFlags(super.value); +} diff --git a/lib/src/models/message/message.dart b/lib/src/models/message/message.dart index ab55173eb..142486f69 100644 --- a/lib/src/models/message/message.dart +++ b/lib/src/models/message/message.dart @@ -182,6 +182,9 @@ class Message extends PartialMessage { /// Data about the role subscription purchase that prompted this message if this is a [MessageType.roleSubscriptionPurchase] message. final RoleSubscriptionData? roleSubscriptionData; + /// Data about entities in this message's auto-populated select menus. + final ResolvedData? resolved; + /// {@macro message} Message({ required super.id, @@ -214,6 +217,7 @@ class Message extends PartialMessage { required this.position, required this.roleSubscriptionData, required this.stickers, + required this.resolved, }); /// The webhook that sent this message if it was sent by a webhook, `null` otherwise. @@ -311,6 +315,9 @@ class MessageFlags extends Flags { /// This message will not trigger push and desktop notifications. static const suppressNotifications = Flag.fromOffset(12); + /// This message is a voice message. + static const isVoiceMessage = Flag.fromOffset(13); + /// Whether this set of flags has the [crossposted] flag set. bool get wasCrossposted => has(crossposted); @@ -341,6 +348,9 @@ class MessageFlags extends Flags { /// Whether this set of flags has the [suppressNotifications] flag set. bool get suppressesNotifications => has(suppressNotifications); + /// Whether this set of flags has the [isVoiceMessage] flag set. + bool get isAVoiceMessage => has(isVoiceMessage); + /// Create a new [MessageFlags]. const MessageFlags(super.value); } diff --git a/lib/src/models/message/reaction.dart b/lib/src/models/message/reaction.dart index 29ba02e1f..4e8aba662 100644 --- a/lib/src/models/message/reaction.dart +++ b/lib/src/models/message/reaction.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/models/discord_color.dart'; import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; @@ -11,15 +12,42 @@ class Reaction with ToStringHelper { /// The number of times this emoji has been used to react. final int count; + /// Details about this emoji's [count]. + final ReactionCountDetails countDetails; + /// Whether the current user reacted using this emoji. final bool me; + /// Whether the current user super-reacted using this emoji. + final bool meBurst; + + /// The emoji for this reaction. final PartialEmoji emoji; + /// The colors used for this the super reaction. + final List burstColors; + /// {@macro reaction} Reaction({ required this.count, + required this.countDetails, required this.me, + required this.meBurst, required this.emoji, + required this.burstColors, }); } + +/// {@template reaction_count_details} +/// Details about a [Reaction]'s [Reaction.count]. +/// {@endtemplate} +class ReactionCountDetails with ToStringHelper { + /// The number of burst reactions. + final int burst; + + /// The number of normal reactions. + final int normal; + + /// {@macro reaction_count_details} + ReactionCountDetails({required this.burst, required this.normal}); +} diff --git a/lib/src/models/role.dart b/lib/src/models/role.dart index f02e05677..70a8695dd 100644 --- a/lib/src/models/role.dart +++ b/lib/src/models/role.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/models/discord_color.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// A partial [Role]. @@ -53,6 +54,9 @@ class Role extends PartialRole implements CommandOptionMentionable { /// The tags associated with this role. final RoleTags? tags; + /// This role's flags. + final RoleFlags flags; + /// {@macro role} Role({ required super.id, @@ -66,6 +70,7 @@ class Role extends PartialRole implements CommandOptionMentionable { required this.permissions, required this.isMentionable, required this.tags, + required this.flags, }); /// This role's icon. @@ -98,3 +103,15 @@ class RoleTags with ToStringHelper { required this.subscriptionListingId, }); } + +/// The flags for a [Role]. +class RoleFlags extends Flags { + /// Whether the role is in an [Onboarding] prompt. + static const inPrompt = Flag.fromOffset(0); + + /// Whether this set of flags has the [inPrompt] flag set. + bool get isInPrompt => has(inPrompt); + + /// Create a new [RoleFlags]. + const RoleFlags(super.value); +} diff --git a/lib/src/models/sku.dart b/lib/src/models/sku.dart new file mode 100644 index 000000000..e8fe3cfc1 --- /dev/null +++ b/lib/src/models/sku.dart @@ -0,0 +1,61 @@ +import 'package:nyxx/src/http/managers/application_manager.dart'; +import 'package:nyxx/src/models/application.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; + +/// {@template sku} +/// A premium offering that can be made available to your application's users or guilds. +/// {@endtemplate} +class Sku with ToStringHelper { + /// The [Manager] for this SKU. + final ApplicationManager manager; + + /// This SKU's ID. + final Snowflake id; + + /// This SKU's type. + final SkuType type; + + /// The ID of the application this SKU belongs to. + final Snowflake applicationId; + + /// The name of this SKU. + final String name; + + /// The URL slug for this SKU. + final String slug; + + /// {@macro sku} + Sku({ + required this.manager, + required this.id, + required this.type, + required this.applicationId, + required this.name, + required this.slug, + }); + + /// The application this SKU belongs to. + PartialApplication get application => PartialApplication(id: applicationId, manager: manager); +} + +/// The type of an [Sku]. +enum SkuType { + subscription._(5), + subscriptionGroup._(6); + + final int value; + + const SkuType._(this.value); + + /// Parse an [SkuType] from an [int]. + /// + /// The [value] must be a valid sku type. + factory SkuType.parse(int value) => SkuType.values.firstWhere( + (type) => type.value == value, + orElse: () => throw FormatException('Unknown SKU type', value), + ); + + @override + String toString() => 'SkuType($value)'; +} diff --git a/lib/src/models/team.dart b/lib/src/models/team.dart index 4c665b39e..486e80a1d 100644 --- a/lib/src/models/team.dart +++ b/lib/src/models/team.dart @@ -66,11 +66,15 @@ class TeamMember with ToStringHelper { /// The user associated with this team member. final PartialUser user; + /// This team member's role. + final TeamMemberRole role; + /// {@macro team_member} TeamMember({ required this.membershipState, required this.teamId, required this.user, + required this.role, }); } @@ -95,3 +99,26 @@ enum TeamMembershipState { @override String toString() => 'TeamMembershipState($value)'; } + +/// The role of a [TeamMember]. +enum TeamMemberRole { + admin._('admin'), + developer._('developer'), + readOnly._('read_only'); + + /// The value of this [TeamMemberRole]. + final String value; + + const TeamMemberRole._(this.value); + + /// Parse a [TeamMemberRole] from a [String]. + /// + /// The [value] must be a valid team member role. + factory TeamMemberRole.parse(String value) => TeamMemberRole.values.firstWhere( + (role) => role.value == value, + orElse: () => throw FormatException('Unknown team member role', value), + ); + + @override + String toString() => 'TeamMemberRole($value)'; +} diff --git a/lib/src/models/user/user.dart b/lib/src/models/user/user.dart index 09126ad82..7597c9610 100644 --- a/lib/src/models/user/user.dart +++ b/lib/src/models/user/user.dart @@ -68,6 +68,9 @@ class User extends PartialUser implements MessageAuthor, CommandOptionMentionabl /// The public [UserFlags] on the user's account. final UserFlags? publicFlags; + /// The hash of this user's avatar decoration. + final String? avatarDecorationHash; + /// {@macro user} User({ required super.manager, @@ -85,6 +88,7 @@ class User extends PartialUser implements MessageAuthor, CommandOptionMentionabl required this.flags, required this.nitroType, required this.publicFlags, + required this.avatarDecorationHash, }); /// This user's banner. @@ -113,6 +117,14 @@ class User extends PartialUser implements MessageAuthor, CommandOptionMentionabl base: HttpRoute()..avatars(id: id.toString()), hash: avatarHash!, ); + + CdnAsset? get avatarDecoration => avatarDecorationHash == null + ? null + : CdnAsset( + client: manager.client, + base: HttpRoute()..avatarDecorations(id: id.toString()), + hash: avatarDecorationHash!, + ); } /// A set of [Flags] a user can have. diff --git a/lib/src/utils/parsing_helpers.dart b/lib/src/utils/parsing_helpers.dart index a2023c664..e0f09363e 100644 --- a/lib/src/utils/parsing_helpers.dart +++ b/lib/src/utils/parsing_helpers.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:runtime_type/runtime_type.dart'; /// An internal helper which parses [object] using [parse] if it is not null. @@ -29,8 +31,9 @@ List parseMany(List objects, [T Function(U)? parse]) { parse = (value) => value as T; } - return List.generate( + return UnmodifiableListView(List.generate( objects.length, + growable: false, (index) { final raw = objects[index]; @@ -40,7 +43,7 @@ List parseMany(List objects, [T Function(U)? parse]) { return parse!(raw); }, - ); + )); } /// An internal helper which parses each element of [object] using [parse] if it is not null. diff --git a/test/integration/rest_integration_test.dart b/test/integration/rest_integration_test.dart index 8da35eab2..bf60540bb 100644 --- a/test/integration/rest_integration_test.dart +++ b/test/integration/rest_integration_test.dart @@ -61,7 +61,11 @@ void main() { }); test('applications', () async { - await expectLater(client.applications.fetchCurrentApplication(), completes); + late Application application; + + await expectLater(() async => application = await client.applications.fetchCurrentApplication(), completes); + await expectLater(application.listSkus(), completes); + await expectLater(client.applications.updateCurrentApplication(ApplicationUpdateBuilder(description: application.description)), completes); }); test('users', () async { diff --git a/test/mocks/client.dart b/test/mocks/client.dart index f37af154a..7742eb43a 100644 --- a/test/mocks/client.dart +++ b/test/mocks/client.dart @@ -4,7 +4,10 @@ import 'package:nyxx/src/manager_mixin.dart'; import 'gateway.dart'; -class MockNyxx with Mock, ManagerMixin implements NyxxRest {} +class MockNyxx with Mock, ManagerMixin implements NyxxRest { + @override + PartialApplication get application => applications[Snowflake.zero]; +} class MockNyxxGateway with Mock, ManagerMixin implements NyxxGateway { @override diff --git a/test/unit/http/managers/application_manager_test.dart b/test/unit/http/managers/application_manager_test.dart index d83a3c0b6..c7aa6103e 100644 --- a/test/unit/http/managers/application_manager_test.dart +++ b/test/unit/http/managers/application_manager_test.dart @@ -25,9 +25,9 @@ final sampleApplication = { "members": [ { "membership_state": 2, - "permissions": ["*"], "team_id": "531992624043786253", - "user": {"avatar": "d9e261cd35999608eb7e3de1fae3688b", "discriminator": "0001", "id": "511972282709709995", "username": "Mr Owner"} + "user": {"avatar": "d9e261cd35999608eb7e3de1fae3688b", "discriminator": "0001", "id": "511972282709709995", "username": "Mr Owner"}, + "role": "admin", } ], @@ -78,6 +78,30 @@ void checkRoleConnectionMetadata(ApplicationRoleConnectionMetadata metadata) { expect(metadata.localizedDescriptions, isNull); } +final sampleSku = { + "id": "1088510058284990888", + "type": 5, + "dependent_sku_id": null, + "application_id": "788708323867885999", + "manifest_labels": null, + "access_type": 1, + "name": "Test Premium", + "features": [], + "release_date": null, + "premium": false, + "slug": "test-premium", + "flags": 128, + "show_age_gate": false +}; + +void checkSku(Sku sku) { + expect(sku.id, equals(Snowflake(1088510058284990888))); + expect(sku.type, equals(SkuType.subscription)); + expect(sku.applicationId, equals(Snowflake(788708323867885999))); + expect(sku.name, equals('Test Premium')); + expect(sku.slug, equals('test-premium')); +} + void main() { group('ApplicationManager', () { test('parse', () { @@ -106,6 +130,19 @@ void main() { ).runWithManager(ApplicationManager(client)); }); + test('parseSku', () { + final client = MockNyxx(); + when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'TEST_TOKEN')); + when(() => client.options).thenReturn(RestClientOptions()); + + ParsingTest>( + name: 'parseSku', + source: sampleSku, + parse: (manager) => manager.parseSku, + check: checkSku, + ).runWithManager(ApplicationManager(client)); + }); + testEndpoint( '/applications/0/role-connections/metadata', name: 'fetchApplicationRoleConnectionMetadata', @@ -122,9 +159,22 @@ void main() { ); testEndpoint( - '/oauth2/applications/@me', + '/applications/@me', (client) => client.applications.fetchCurrentApplication(), response: sampleApplication, ); + + testEndpoint( + '/applications/@me', + method: 'PATCH', + (client) => client.applications.updateCurrentApplication(ApplicationUpdateBuilder()), + response: sampleApplication, + ); + + testEndpoint( + '/applications/0/skus', + (client) => client.applications.listSkus(Snowflake.zero), + response: [sampleSku], + ); }); } diff --git a/test/unit/http/managers/entitlement_manager_test.dart b/test/unit/http/managers/entitlement_manager_test.dart new file mode 100644 index 000000000..1674503a6 --- /dev/null +++ b/test/unit/http/managers/entitlement_manager_test.dart @@ -0,0 +1,75 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:test/test.dart'; + +import '../../../test_manager.dart'; + +final sampleEntitlement = { + "id": "1", + "sku_id": "1019475255913222144", + "application_id": "1019370614521200640", + "user_id": "771129655544643584", + "promotion_id": null, + "type": 8, + "deleted": false, + "gift_code_flags": 0, + "consumed": false, + "starts_at": "2022-09-14T17:00:18.704163+00:00", + "ends_at": "2022-10-14T17:00:18.704163+00:00", + "guild_id": "1015034326372454400", + "subscription_id": "1019653835926409216" +}; + +void checkEntitlement(Entitlement entitlement) { + expect(entitlement.id, equals(Snowflake(1))); + expect(entitlement.skuId, equals(Snowflake(1019475255913222144))); + expect(entitlement.userId, equals(Snowflake(771129655544643584))); + expect(entitlement.guildId, equals(Snowflake(1015034326372454400))); + expect(entitlement.applicationId, equals(Snowflake(1019370614521200640))); + expect(entitlement.type, equals(EntitlementType.applicationSubscription)); + expect(entitlement.isConsumed, isFalse); + expect(entitlement.startsAt, equals(DateTime.utc(2022, 09, 14, 17, 0, 18, 704, 163))); + expect(entitlement.endsAt, equals(DateTime.utc(2022, 10, 14, 17, 0, 18, 704, 163))); +} + +void main() { + testReadOnlyManager( + 'EntitlementManager', + (config, client) => EntitlementManager(config, client, applicationId: Snowflake.zero), + // fetch() artificially creates a before field as before = id + 1 - testing ID is 1 so before is 2 + '/applications/0/entitlements?before=2', + sampleObject: sampleEntitlement, + // Fetch implementation internally uses `list()`, so we return a full audit log + fetchObjectOverride: [sampleEntitlement], + sampleMatches: checkEntitlement, + additionalParsingTests: [], + additionalEndpointTests: [ + EndpointTest, List>( + name: 'list', + source: [sampleEntitlement], + urlMatcher: '/applications/0/entitlements', + execute: (manager) => manager.list(), + check: (list) { + expect(list, hasLength(1)); + checkEntitlement(list.single); + }, + ), + EndpointTest>( + name: 'createTestEntitlement', + method: 'POST', + source: sampleEntitlement, + urlMatcher: '/applications/0/entitlements', + execute: (manager) => manager + .createTestEntitlement(TestEntitlementBuilder(skuId: Snowflake.zero, ownerId: Snowflake.zero, ownerType: TestEntitlementType.userSubscription)), + check: checkEntitlement, + ), + EndpointTest( + name: 'deleteTestEntitlement', + method: 'DELETE', + source: null, + urlMatcher: '/applications/0/entitlements/1', + execute: (manager) => manager.deleteTestEntitlement(Snowflake(1)), + check: (_) {}, + ), + ], + ); +} diff --git a/test/unit/http/managers/guild_manager_test.dart b/test/unit/http/managers/guild_manager_test.dart index 9af1ee74b..1af7397d9 100644 --- a/test/unit/http/managers/guild_manager_test.dart +++ b/test/unit/http/managers/guild_manager_test.dart @@ -129,7 +129,16 @@ final sampleGuild2 = { "VIP_REGIONS", ], "emojis": [ - {"name": "ultrafastparrot", "roles": [], "id": "393564762228785161", "require_colons": true, "managed": false, "animated": true, "available": true} + { + "name": "ultrafastparrot", + "roles": [], + "id": "393564762228785161", + "require_colons": true, + "managed": false, + "animated": true, + "available": true, + "flags": 0, + } ], "banner": "5c3cb8d1bc159937fffe7e641ec96ca7", "owner_id": "53908232506183680", @@ -150,7 +159,8 @@ final sampleGuild2 = { "color": 0, "hoist": false, "managed": false, - "mentionable": false + "mentionable": false, + "flags": 0, } ], "default_message_notifications": 1, @@ -447,6 +457,7 @@ final sampleGuildTemplate = { "color": 0, "hoist": false, "mentionable": false, + "flags": 0, } ], "channels": [ diff --git a/test/unit/http/managers/interaction_manager_test.dart b/test/unit/http/managers/interaction_manager_test.dart index 6cc7eabdd..f434ebdc3 100644 --- a/test/unit/http/managers/interaction_manager_test.dart +++ b/test/unit/http/managers/interaction_manager_test.dart @@ -37,6 +37,7 @@ final sampleCommandInteraction = { "id": "771825006014889984" }, "channel_id": "645027906669510667", + "entitlements": [], // Fields not present in the example but documented "application_id": "0", @@ -61,6 +62,7 @@ void checkCommandInteraction(Interaction interaction) { expect(interaction.appPermissions, equals(Permissions(442368))); expect(interaction.locale, equals(Locale.enUs)); expect(interaction.guildLocale, equals(Locale.enUs)); + expect(interaction.entitlements, equals([])); } final sampleCommandInteraction2 = { diff --git a/test/unit/http/managers/message_manager_test.dart b/test/unit/http/managers/message_manager_test.dart index e4224fc98..9502f87ec 100644 --- a/test/unit/http/managers/message_manager_test.dart +++ b/test/unit/http/managers/message_manager_test.dart @@ -7,8 +7,14 @@ final sampleMessage = { "reactions": [ { "count": 1, + "count_details": { + "burst": 0, + "normal": 1, + }, "me": false, - "emoji": {"id": null, "name": "🔥"} + "me_burst": false, + "emoji": {"id": null, "name": "🔥"}, + "burst_colors": [], } ], "attachments": [], @@ -68,8 +74,14 @@ final sampleCrosspostedMessage = { "reactions": [ { "count": 1, + "count_details": { + "burst": 0, + "normal": 1, + }, "me": false, - "emoji": {"id": null, "name": "🔥"} + "me_burst": false, + "emoji": {"id": null, "name": "🔥"}, + "burst_colors": [], } ], "attachments": [], diff --git a/test/unit/http/managers/role_manager_test.dart b/test/unit/http/managers/role_manager_test.dart index 36a26e056..89356ed95 100644 --- a/test/unit/http/managers/role_manager_test.dart +++ b/test/unit/http/managers/role_manager_test.dart @@ -14,7 +14,8 @@ final sampleRole = { "position": 1, "permissions": "66321471", "managed": false, - "mentionable": false + "mentionable": false, + "flags": 0, }; void checkRole(Role role) {