Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add application emojis #678

Merged
merged 13 commits into from
Sep 21, 2024
2 changes: 1 addition & 1 deletion lib/nyxx.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions lib/src/builders/emoji/emoji.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,36 @@ class EmojiUpdateBuilder implements UpdateBuilder<GuildEmoji> {
if (!identical(roles, sentinelList)) 'roles': roles?.map((s) => s.toString()).toList(),
};
}

class ApplicationEmojiBuilder implements CreateBuilder<ApplicationEmoji> {
/// The name of the emoji.
String name;

/// The 128x128 emoji image.
ImageBuilder image;

ApplicationEmojiBuilder({
required this.name,
required this.image,
});

@override
Map<String, Object?> build() => {
'name': name,
'image': image.buildDataString(),
};
}

class ApplicationEmojiUpdateBuilder implements UpdateBuilder<ApplicationEmoji> {
/// The name of the emoji.
String? name;

ApplicationEmojiUpdateBuilder({
this.name,
});

@override
Map<String, Object?> build() => {
if (name != null) 'name': name,
};
}
10 changes: 10 additions & 0 deletions lib/src/builders/sentinels.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
130 changes: 126 additions & 4 deletions lib/src/http/managers/emoji_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -11,10 +12,8 @@ import 'package:nyxx/src/utils/parsing_helpers.dart';

import 'manager.dart';

class EmojiManager extends Manager<Emoji> {
final Snowflake guildId;

EmojiManager(super.config, super.client, {required this.guildId}) : super(identifier: '$guildId.emojis');
abstract class EmojiManager extends Manager<Emoji> {
EmojiManager(super.config, super.client, {required super.identifier});

@override
PartialEmoji operator [](Snowflake id) => PartialEmoji(id: id, manager: this);
Expand All @@ -31,6 +30,128 @@ class EmojiManager extends Manager<Emoji> {
);
}

return sentinelEmoji;
}

/// List the emojis.
Future<List<Emoji>> 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<String, Object?> 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<ApplicationEmoji> get(Snowflake id) async => await super.get(id) as ApplicationEmoji;

@override
Future<ApplicationEmoji> 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<String, Object?>) as ApplicationEmoji;

client.updateCacheWith(emoji);
return emoji;
}

/// List the emojis in the application.
@override
Future<List<ApplicationEmoji>> 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<String, Object?>) as ApplicationEmoji,
);

emojis.forEach(client.updateCacheWith);

return emojis;
}

@override
Future<ApplicationEmoji> 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<String, Object?>) as ApplicationEmoji;

client.updateCacheWith(emoji);
return emoji;
}

@override
Future<ApplicationEmoji> 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<String, Object?>) as ApplicationEmoji;

client.updateCacheWith(emoji);
return emoji;
}

@override
Future<void> 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<String, Object?> raw) {
final emoji = super.parse(raw);

if (!identical(emoji, sentinelEmoji)) {
return emoji;
}

return GuildEmoji(
id: Snowflake.parse(raw['id']!),
manager: this,
Expand Down Expand Up @@ -74,6 +195,7 @@ class EmojiManager extends Manager<Emoji> {
}

/// List the emojis in the guild.
@override
Future<List<GuildEmoji>> list() async {
_checkIsConcrete();

Expand Down
5 changes: 5 additions & 0 deletions lib/src/models/application.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand Down
48 changes: 45 additions & 3 deletions lib/src/models/emoji.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class PartialEmoji extends WritableSnowflakeEntity<Emoji> {
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;
Expand Down Expand Up @@ -45,6 +45,48 @@ class TextEmoji extends Emoji {
Future<TextEmoji> 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
Expand All @@ -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,
Expand All @@ -82,7 +124,7 @@ class GuildEmoji extends Emoji {
});

/// The roles allowed to use this emoji.
List<PartialRole>? get roles => roleIds?.map((e) => manager.client.guilds[manager.guildId].roles[e]).toList();
List<PartialRole>? get roles => roleIds?.map((e) => manager.client.guilds[(manager as GuildEmojiManager).guildId].roles[e]).toList();

/// This emoji's image.
CdnAsset get image => CdnAsset(
Expand Down
4 changes: 2 additions & 2 deletions lib/src/models/guild/guild.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ class PartialGuild extends WritableSnowflakeEntity<Guild> {
/// 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);
Expand Down
4 changes: 4 additions & 0 deletions lib/src/utils/cache_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions test/unit/http/managers/emoji_manager_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -39,7 +40,7 @@ void checkTextEmoji(Emoji emoji) {
void main() {
testManager<Emoji, EmojiManager>(
'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,
Expand All @@ -53,7 +54,7 @@ void main() {
),
],
additionalEndpointTests: [
EndpointTest<EmojiManager, List<Emoji>, List<Object?>>(
EndpointTest<GuildEmojiManager, List<Emoji>, List<Object?>>(
name: 'list',
source: [sampleGuildEmoji],
urlMatcher: '/guilds/1/emojis',
Expand Down
Loading