diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 8ff3c8435..a8a3dc63b 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -97,7 +97,7 @@ export 'src/http/managers/gateway_manager.dart' show GatewayManager; export 'src/http/managers/scheduled_event_manager.dart' show ScheduledEventManager; export 'src/http/managers/auto_moderation_manager.dart' show AutoModerationManager; export 'src/http/managers/integration_manager.dart' show IntegrationManager; -export 'src/http/managers/emoji_manager.dart' show EmojiManager; +export 'src/http/managers/emoji_manager.dart' show EmojiManager, ApplicationEmojiManager, GuildEmojiManager; 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; diff --git a/lib/src/builders/emoji/emoji.dart b/lib/src/builders/emoji/emoji.dart index e58033dd7..c9589ae07 100644 --- a/lib/src/builders/emoji/emoji.dart +++ b/lib/src/builders/emoji/emoji.dart @@ -46,3 +46,36 @@ class EmojiUpdateBuilder implements UpdateBuilder { if (!identical(roles, sentinelList)) 'roles': roles?.map((s) => s.toString()).toList(), }; } + +class ApplicationEmojiBuilder implements CreateBuilder { + /// The name of the emoji. + String name; + + /// The 128x128 emoji image. + ImageBuilder image; + + ApplicationEmojiBuilder({ + required this.name, + required this.image, + }); + + @override + Map build() => { + 'name': name, + 'image': image.buildDataString(), + }; +} + +class ApplicationEmojiUpdateBuilder implements UpdateBuilder { + /// The name of the emoji. + String? name; + + ApplicationEmojiUpdateBuilder({ + this.name, + }); + + @override + Map build() => { + if (name != null) 'name': name, + }; +} diff --git a/lib/src/builders/sentinels.dart b/lib/src/builders/sentinels.dart index 4870b13c4..c8462e6c6 100644 --- a/lib/src/builders/sentinels.dart +++ b/lib/src/builders/sentinels.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/builders/image.dart'; import 'package:nyxx/src/models/channel/types/forum.dart'; +import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/guild/scheduled_event.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/utils/flags.dart'; @@ -99,3 +100,12 @@ class _SentinelUri implements Uri { @override void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } + +class _SentinelEmoji implements Emoji { + const _SentinelEmoji(); + + @override + void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +const sentinelEmoji = _SentinelEmoji(); diff --git a/lib/src/http/managers/emoji_manager.dart b/lib/src/http/managers/emoji_manager.dart index 40b7acb10..e889a0587 100644 --- a/lib/src/http/managers/emoji_manager.dart +++ b/lib/src/http/managers/emoji_manager.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:nyxx/src/builders/emoji/emoji.dart'; +import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/emoji.dart'; @@ -11,10 +12,8 @@ import 'package:nyxx/src/utils/parsing_helpers.dart'; import 'manager.dart'; -class EmojiManager extends Manager { - final Snowflake guildId; - - EmojiManager(super.config, super.client, {required this.guildId}) : super(identifier: '$guildId.emojis'); +abstract class EmojiManager extends Manager { + EmojiManager(super.config, super.client, {required super.identifier}); @override PartialEmoji operator [](Snowflake id) => PartialEmoji(id: id, manager: this); @@ -31,6 +30,128 @@ class EmojiManager extends Manager { ); } + return sentinelEmoji; + } + + /// List the emojis. + Future> list(); +} + +class ApplicationEmojiManager extends EmojiManager { + final Snowflake applicationId; + + ApplicationEmojiManager(super.config, super.client, {required this.applicationId}) : super(identifier: 'applications.$applicationId.emojis'); + + @override + Emoji parse(Map raw) { + final emoji = super.parse(raw); + + if (!identical(emoji, sentinelEmoji)) { + return emoji; + } + + return ApplicationEmoji( + id: Snowflake.parse(raw['id']!), + manager: this, + isAnimated: raw['animated'] as bool, + isAvailable: raw['available'] as bool, + isManaged: raw['managed'] as bool, + requiresColons: raw['require_colons'] as bool, + name: raw['name'] as String, + user: maybeParse(raw['user'], client.users.parse), + ); + } + + @override + Future get(Snowflake id) async => await super.get(id) as ApplicationEmoji; + + @override + Future fetch(Snowflake id) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(id: id.toString()); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + final emoji = parse(response.jsonBody as Map) as ApplicationEmoji; + + client.updateCacheWith(emoji); + return emoji; + } + + /// List the emojis in the application. + @override + Future> list() async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + + final emojis = parseMany( + response.jsonBody['items'] as List, + (raw) => parse(raw as Map) as ApplicationEmoji, + ); + + emojis.forEach(client.updateCacheWith); + + return emojis; + } + + @override + Future create(ApplicationEmojiBuilder builder) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(); + final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build())); + + final response = await client.httpHandler.executeSafe(request); + final emoji = parse(response.jsonBody as Map) as ApplicationEmoji; + + client.updateCacheWith(emoji); + return emoji; + } + + @override + Future update(Snowflake id, ApplicationEmojiUpdateBuilder builder) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(id: id.toString()); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); + + final response = await client.httpHandler.executeSafe(request); + final emoji = parse(response.jsonBody as Map) as ApplicationEmoji; + + client.updateCacheWith(emoji); + return emoji; + } + + @override + Future delete(Snowflake id) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(id: id.toString()); + final request = BasicRequest(route, method: 'DELETE'); + + await client.httpHandler.executeSafe(request); + cache.remove(id); + } +} + +class GuildEmojiManager extends EmojiManager { + final Snowflake guildId; + + GuildEmojiManager(super.config, super.client, {required this.guildId}) : super(identifier: 'guilds.$guildId.emojis'); + + @override + Emoji parse(Map raw) { + final emoji = super.parse(raw); + + if (!identical(emoji, sentinelEmoji)) { + return emoji; + } + return GuildEmoji( id: Snowflake.parse(raw['id']!), manager: this, @@ -74,6 +195,7 @@ class EmojiManager extends Manager { } /// List the emojis in the guild. + @override Future> list() async { _checkIsConcrete(); diff --git a/lib/src/models/application.dart b/lib/src/models/application.dart index a9a7501f7..8cd8eb0c7 100644 --- a/lib/src/models/application.dart +++ b/lib/src/models/application.dart @@ -1,8 +1,10 @@ import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/application_manager.dart'; +import 'package:nyxx/src/http/managers/emoji_manager.dart'; import 'package:nyxx/src/http/managers/entitlement_manager.dart'; import 'package:nyxx/src/http/managers/sku_manager.dart'; import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; @@ -27,6 +29,9 @@ class PartialApplication with ToStringHelper { /// An [EntitlementManager] for this application's [Entitlement]s. EntitlementManager get entitlements => EntitlementManager(manager.client.options.entitlementConfig, manager.client, applicationId: id); + /// An [ApplicationEmojiManager] for this application's [Emoji]s. + ApplicationEmojiManager get emojis => ApplicationEmojiManager(manager.client.options.emojiCacheConfig, manager.client, applicationId: id); + /// An [SkuManager] for this application's [Sku]s. SkuManager get skus => SkuManager(manager.client.options.skuConfig, manager.client, applicationId: id); diff --git a/lib/src/models/emoji.dart b/lib/src/models/emoji.dart index 26dff10a2..c8e14610b 100644 --- a/lib/src/models/emoji.dart +++ b/lib/src/models/emoji.dart @@ -16,7 +16,7 @@ class PartialEmoji extends WritableSnowflakeEntity { PartialEmoji({required super.id, required this.manager}); } -/// An emoji. Either a [TextEmoji] or a [GuildEmoji]. +/// An emoji. Either a [TextEmoji], an [ApplicationEmoji] or a [GuildEmoji]. abstract class Emoji extends PartialEmoji { /// The emoji's name. Can be `dartlang` for a custom emoji, or `❤️` for a text emoji. String? get name; @@ -45,6 +45,48 @@ class TextEmoji extends Emoji { Future fetch() async => this; } +// Apparently an ApplicationEmoji contains a `roles` field, but it's always an empty list, so we don't include it here. +/// A custom emoji created on the application's emoji tab. +class ApplicationEmoji extends Emoji { + @override + final String name; + + /// The user that created this emoji. `null` if it was the first time it was created. + final User? user; + + /// Whether this emoji must be wrapped in colons. + final bool requiresColons; + + /// Whether this emoji is managed. + final bool isManaged; + + /// Whether this emoji is animated. + final bool isAnimated; + + /// Whether this emoji can be used, always true for ApplicationEmojis. + final bool isAvailable; + + /// @nodoc + ApplicationEmoji({ + required super.id, + required ApplicationEmojiManager super.manager, + required this.name, + required this.user, + required this.requiresColons, + required this.isManaged, + required this.isAnimated, + required this.isAvailable, + }); + + /// This emoji's image. + CdnAsset get image => CdnAsset( + client: manager.client, + base: HttpRoute()..emojis(), + hash: id.toString(), + isAnimated: isAnimated, + ); +} + /// A custom guild emoji. class GuildEmoji extends Emoji { @override @@ -71,7 +113,7 @@ class GuildEmoji extends Emoji { /// @nodoc GuildEmoji({ required super.id, - required super.manager, + required GuildEmojiManager super.manager, required this.name, required this.roleIds, required this.user, @@ -82,7 +124,7 @@ class GuildEmoji extends Emoji { }); /// The roles allowed to use this emoji. - List? get roles => roleIds?.map((e) => manager.client.guilds[manager.guildId].roles[e]).toList(); + List? get roles => roleIds?.map((e) => manager.client.guilds[(manager as GuildEmojiManager).guildId].roles[e]).toList(); /// This emoji's image. CdnAsset get image => CdnAsset( diff --git a/lib/src/models/guild/guild.dart b/lib/src/models/guild/guild.dart index 5ae4fd2bb..888b66d1d 100644 --- a/lib/src/models/guild/guild.dart +++ b/lib/src/models/guild/guild.dart @@ -66,8 +66,8 @@ class PartialGuild extends WritableSnowflakeEntity { /// An [IntegrationManager] for the integrations of this guild. IntegrationManager get integrations => IntegrationManager(manager.client.options.integrationConfig, manager.client, guildId: id); - /// An [EmojiManager] for the emojis of this guild. - EmojiManager get emojis => EmojiManager(manager.client.options.emojiCacheConfig, manager.client, guildId: id); + /// A [GuildEmojiManager] for the emojis of this guild. + GuildEmojiManager get emojis => GuildEmojiManager(manager.client.options.emojiCacheConfig, manager.client, guildId: id); /// An [GuildStickerManager] for the stickers of this guild. GuildStickerManager get stickers => GuildStickerManager(manager.client.options.stickerCacheConfig, manager.client, guildId: id); diff --git a/lib/src/utils/cache_helpers.dart b/lib/src/utils/cache_helpers.dart index b4adc94ac..3b343ce61 100644 --- a/lib/src/utils/cache_helpers.dart +++ b/lib/src/utils/cache_helpers.dart @@ -88,6 +88,10 @@ extension CacheUpdates on NyxxRest { if (entity case GuildEmoji(:final user?)) { updateCacheWith(user); } + + if (entity case ApplicationEmoji(:final user)) { + updateCacheWith(user); + } }(), Guild() => () { entity.manager.cache[entity.id] = entity; diff --git a/test/unit/http/managers/emoji_manager_test.dart b/test/unit/http/managers/emoji_manager_test.dart index 52e0b8c29..448be4e5f 100644 --- a/test/unit/http/managers/emoji_manager_test.dart +++ b/test/unit/http/managers/emoji_manager_test.dart @@ -1,4 +1,5 @@ import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/http/managers/emoji_manager.dart'; import 'package:test/test.dart'; import '../../../test_manager.dart'; @@ -39,7 +40,7 @@ void checkTextEmoji(Emoji emoji) { void main() { testManager( 'EmojiManager', - (config, client) => EmojiManager(config, client, guildId: Snowflake(1)), + (config, client) => GuildEmojiManager(config, client, guildId: Snowflake(1)), RegExp(r'/guilds/1/emojis/\d+'), '/guilds/1/emojis', sampleObject: sampleGuildEmoji, @@ -53,7 +54,7 @@ void main() { ), ], additionalEndpointTests: [ - EndpointTest, List>( + EndpointTest, List>( name: 'list', source: [sampleGuildEmoji], urlMatcher: '/guilds/1/emojis',