Skip to content

Commit

Permalink
add support for soundboard (#708)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lexedia authored Oct 16, 2024
1 parent aaef80a commit d93aa12
Show file tree
Hide file tree
Showing 24 changed files with 750 additions and 18 deletions.
4 changes: 2 additions & 2 deletions lib/src/builders/channel/forum_tag.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:nyxx/src/builders/builder.dart';
import 'package:nyxx/src/models/channel/types/forum.dart';
import 'package:nyxx/src/models/snowflake.dart';
import 'package:nyxx/src/utils/building_helpers.dart';

class ForumTagBuilder extends CreateBuilder<ForumTag> {
String name;
Expand All @@ -17,7 +18,6 @@ class ForumTagBuilder extends CreateBuilder<ForumTag> {
Map<String, Object?> build() => {
'name': name,
if (isModerated != null) 'moderated': isModerated,
if (emojiId != null) 'emoji_id': emojiId!.toString(),
if (emojiName != null) 'emoji_name': emojiName,
...makeEmojiMap(emojiId: emojiId, emojiName: emojiName),
};
}
15 changes: 3 additions & 12 deletions lib/src/builders/channel/guild_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:nyxx/src/models/channel/types/guild_voice.dart';
import 'package:nyxx/src/models/channel/voice_channel.dart';
import 'package:nyxx/src/models/permission_overwrite.dart';
import 'package:nyxx/src/models/snowflake.dart';
import 'package:nyxx/src/utils/building_helpers.dart';
import 'package:nyxx/src/utils/flags.dart';

class GuildChannelBuilder<T extends GuildChannel> extends CreateBuilder<T> {
Expand Down Expand Up @@ -231,12 +232,7 @@ class ForumChannelBuilder extends GuildChannelBuilder<ForumChannel> {
if (isNsfw != null) 'nsfw': isNsfw,
if (defaultAutoArchiveDuration != null) 'default_auto_archive_duration': defaultAutoArchiveDuration!.inMinutes,
if (!identical(defaultReaction, sentinelDefaultReaction))
'default_reaction_emoji': defaultReaction == null
? null
: {
if (defaultReaction!.emojiId != null) 'emoji_id': defaultReaction!.emojiId!.toString(),
if (defaultReaction!.emojiName != null) 'emoji_name': defaultReaction!.emojiName,
},
'default_reaction_emoji': defaultReaction == null ? null : makeEmojiMap(emojiId: defaultReaction!.emojiId, emojiName: defaultReaction!.emojiName),
if (tags != null) 'available_tags': tags!.map((e) => e.build()).toList(),
if (defaultSortOrder != null) 'default_sort_order': defaultSortOrder!.value,
};
Expand Down Expand Up @@ -293,12 +289,7 @@ class ForumChannelUpdateBuilder extends GuildChannelUpdateBuilder<ForumChannel>
if (flags != null) 'flags': flags!.value,
if (tags != null) 'available_tags': tags!.map((e) => e.build()).toList(),
if (!identical(defaultReaction, sentinelDefaultReaction))
'default_reaction_emoji': defaultReaction == null
? null
: {
if (defaultReaction!.emojiId != null) 'emoji_id': defaultReaction!.emojiId!.toString(),
if (defaultReaction!.emojiName != null) 'emoji_name': defaultReaction!.emojiName,
},
'default_reaction_emoji': defaultReaction == null ? null : makeEmojiMap(emojiId: defaultReaction!.emojiId, emojiName: defaultReaction!.emojiName),
if (defaultThreadRateLimitPerUser != null) 'default_thread_rate_limit_per_user': defaultThreadRateLimitPerUser!.inSeconds,
if (defaultSortOrder != null) 'default_sort_order': defaultSortOrder!.value,
if (defaultLayout != null) 'default_forum_layout': defaultLayout!.value,
Expand Down
4 changes: 2 additions & 2 deletions lib/src/builders/guild/welcome_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:nyxx/src/builders/builder.dart';
import 'package:nyxx/src/builders/sentinels.dart';
import 'package:nyxx/src/models/guild/welcome_screen.dart';
import 'package:nyxx/src/utils/building_helpers.dart';

class WelcomeScreenUpdateBuilder extends UpdateBuilder<WelcomeScreen> {
bool? isEnabled;
Expand All @@ -20,8 +21,7 @@ class WelcomeScreenUpdateBuilder extends UpdateBuilder<WelcomeScreen> {
{
'channel_id': channel.channelId.toString(),
'description': channel.description,
'emoji_id': channel.emojiId?.toString(),
'emoji_name': channel.emojiName,
...makeEmojiMap(emojiId: channel.emojiId, emojiName: channel.emojiName),
},
],
if (!identical(description, sentinelString)) 'description': description,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/builders/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ImageBuilder {
ImageBuilder.gif(this.data) : format = 'gif';

static Future<ImageBuilder> fromFile(File file, {String? format}) async {
format ??= p.extension(file.path);
format ??= p.extension(file.path).substring(1);

const formats = {
'png': 'png',
Expand Down
2 changes: 2 additions & 0 deletions lib/src/builders/sentinels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:nyxx/src/utils/flags.dart';
// ASCII encoded "nyxx"
const sentinelInteger = 0x6E797878;

const sentinelDouble = sentinelInteger * 1.0;

// ESC-"nyxx"
const sentinelString = '\u{1B}nyxx';

Expand Down
38 changes: 38 additions & 0 deletions lib/src/builders/sound.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;

class SoundBuilder {
List<int> data;
String format;

SoundBuilder({required this.data, required this.format});

SoundBuilder.mp3(this.data) : format = 'mpeg';

SoundBuilder.ogg(this.data) : format = 'ogg';

static Future<SoundBuilder> fromFile(File file, {String? format}) async {
format ??= p.extension(file.path).substring(1);

const formats = {
'mp3': 'mpeg',
'mpeg': 'mpeg',
'ogg': 'ogg',
};

final actualFormat = formats[format];

if (actualFormat == null) {
throw ArgumentError.value(format, 'format', 'Unsupported format');
}

final data = await file.readAsBytes();

return SoundBuilder(data: data, format: actualFormat);
}

List<int> buildRawData() => data;

String buildDataString() => 'data:audio/$format;base64,${base64Encode(data)}';
}
47 changes: 47 additions & 0 deletions lib/src/builders/soundboard.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:nyxx/src/builders/builder.dart';
import 'package:nyxx/src/builders/sentinels.dart';
import 'package:nyxx/src/builders/sound.dart';
import 'package:nyxx/src/models/snowflake.dart';
import 'package:nyxx/src/models/soundboard/soundboard.dart';
import 'package:nyxx/src/utils/building_helpers.dart';

class SoundboardSoundBuilder extends CreateBuilder<SoundboardSound> {
String name;

SoundBuilder sound;

double? volume;

String? emojiName;

Snowflake? emojiId;

SoundboardSoundBuilder({required this.name, required this.sound, this.volume, this.emojiName, this.emojiId});

@override
Map<String, Object?> build() => {
'name': name,
'sound': sound.buildDataString(),
if (volume != null) 'volume': volume,
...makeEmojiMap(emojiId: emojiId, emojiName: emojiName),
};
}

class SoundboardSoundUpdateBuilder extends UpdateBuilder<SoundboardSound> {
String name;

double? volume;

String? emojiName;

Snowflake? emojiId;

SoundboardSoundUpdateBuilder({required this.name, this.volume = sentinelDouble, this.emojiName = sentinelString, this.emojiId = sentinelSnowflake});

@override
Map<String, Object?> build() => {
'name': name,
if (volume != sentinelDouble) 'volume': volume,
if (!(identical(emojiName, sentinelString) || identical(emojiId, sentinelSnowflake))) ...makeEmojiMap(emojiId: emojiId, emojiName: emojiName),
};
}
11 changes: 10 additions & 1 deletion lib/src/client_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:nyxx/src/models/guild/scheduled_event.dart';
import 'package:nyxx/src/models/message/message.dart';
import 'package:nyxx/src/models/role.dart';
import 'package:nyxx/src/models/sku.dart';
import 'package:nyxx/src/models/soundboard/soundboard.dart';
import 'package:nyxx/src/models/sticker/global_sticker.dart';
import 'package:nyxx/src/models/sticker/guild_sticker.dart';
import 'package:nyxx/src/models/subscription.dart';
Expand Down Expand Up @@ -143,9 +144,15 @@ class RestClientOptions extends ClientOptions {
/// The [CacheConfig] to use for the [Application.skus] manager.
final CacheConfig<Sku> skuConfig;

/// Tje [CacheConfig] to use for the [Sku.subscriptions] manager.
/// The [CacheConfig] to use for the [Sku.subscriptions] manager.
final CacheConfig<Subscription> subscriptionConfig;

/// The [CacheConfig] to use for the [NyxxRest.soundboard] manager.
final CacheConfig<SoundboardSound> globalSoundboardCacheConfig;

/// The [CacheConfig] to use for the [Guild.soundboard] manager.
final CacheConfig<SoundboardSound> soundboardCacheConfig;

/// Create a new [RestClientOptions].
const RestClientOptions({
super.plugins,
Expand Down Expand Up @@ -187,6 +194,8 @@ class RestClientOptions extends ClientOptions {
this.entitlementConfig = _defaultCacheConfig,
this.skuConfig = _defaultCacheConfig,
this.subscriptionConfig = _defaultCacheConfig,
this.globalSoundboardCacheConfig = _smallCacheConfig,
this.soundboardCacheConfig = _smallCacheConfig,
});
}

Expand Down
7 changes: 7 additions & 0 deletions lib/src/errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,10 @@ class ClientClosedError extends Error {
@override
String toString() => 'Client is closed';
}

class SoundboardSoundNotFoundException extends NyxxException {
/// The ID of the sound.
final Snowflake soundId;

SoundboardSoundNotFoundException(this.soundId) : super('Soundboard sound $soundId not found');
}
16 changes: 16 additions & 0 deletions lib/src/event_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:nyxx/src/models/gateway/events/invite.dart';
import 'package:nyxx/src/models/gateway/events/message.dart';
import 'package:nyxx/src/models/gateway/events/presence.dart';
import 'package:nyxx/src/models/gateway/events/ready.dart';
import 'package:nyxx/src/models/gateway/events/soundboard.dart';
import 'package:nyxx/src/models/gateway/events/stage_instance.dart';
import 'package:nyxx/src/models/gateway/events/voice.dart';
import 'package:nyxx/src/models/gateway/events/webhook.dart';
Expand Down Expand Up @@ -261,4 +262,19 @@ mixin EventMixin implements Nyxx {

/// A [Stream] of [MessagePollVoteRemoveEvent]s received by this client.
Stream<MessagePollVoteRemoveEvent> get onMessagePollVoteRemove => onEvent.whereType<MessagePollVoteRemoveEvent>();

/// A [Stream] of [SoundboardSoundDeleteEvent]s received by this client.
Stream<SoundboardSoundDeleteEvent> get onSoundboardSoundDelete => onEvent.whereType<SoundboardSoundDeleteEvent>();

/// A [Stream] of [SoundboardSoundUpdateEvent]s received by this client.
Stream<SoundboardSoundUpdateEvent> get onSoundboardSoundUpdate => onEvent.whereType<SoundboardSoundUpdateEvent>();

/// A [Stream] of [SoundboardSoundCreateEvent]s received by this client.
Stream<SoundboardSoundCreateEvent> get onSoundboardSoundCreate => onEvent.whereType<SoundboardSoundCreateEvent>();

/// A [Stream] of [SoundboardSoundsUpdateEvent]s received by this client.
Stream<SoundboardSoundsUpdateEvent> get onSoundboardSoundsUpdate => onEvent.whereType<SoundboardSoundsUpdateEvent>();

/// A [Stream] of [VoiceChannelEffectSendEvent]s received by this client.
Stream<VoiceChannelEffectSendEvent> get onVoiceChannelEffectSend => onEvent.whereType<VoiceChannelEffectSendEvent>();
}
69 changes: 69 additions & 0 deletions lib/src/gateway/gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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/events/soundboard.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';
Expand Down Expand Up @@ -284,6 +285,7 @@ class Gateway extends GatewayManager with EventParser {
'PRESENCE_UPDATE': parsePresenceUpdate,
'TYPING_START': parseTypingStart,
'USER_UPDATE': parseUserUpdate,
'VOICE_CHANNEL_EFFECT_SEND': parseVoiceChannelEffectSend,
'VOICE_STATE_UPDATE': parseVoiceStateUpdate,
'VOICE_SERVER_UPDATE': parseVoiceServerUpdate,
'WEBHOOKS_UPDATE': parseWebhooksUpdate,
Expand All @@ -296,6 +298,10 @@ class Gateway extends GatewayManager with EventParser {
'ENTITLEMENT_DELETE': parseEntitlementDelete,
'MESSAGE_POLL_VOTE_ADD': parseMessagePollVoteAdd,
'MESSAGE_POLL_VOTE_REMOVE': parseMessagePollVoteRemove,
'GUILD_SOUNDBOARD_SOUND_CREATE': parseSoundboardSoundCreate,
'GUILD_SOUNDBOARD_SOUND_UPDATE': parseSoundboardSoundUpdate,
'GUILD_SOUNDBOARD_SOUND_DELETE': parseSoundboardSoundDelete,
'GUILD_SOUNDBOARD_SOUNDS_UPDATE': parseSoundboardSoundsUpdate,
};

return mapping[raw.name]?.call(raw.payload) ?? UnknownDispatchEvent(gateway: this, raw: raw);
Expand Down Expand Up @@ -984,6 +990,23 @@ class Gateway extends GatewayManager with EventParser {
);
}

/// Parse a [VoiceChannelEffectSendEvent] from [raw].
VoiceChannelEffectSendEvent parseVoiceChannelEffectSend(Map<String, Object?> raw) {
final guildId = Snowflake.parse(raw['guild_id']!);

return VoiceChannelEffectSendEvent(
gateway: this,
channelId: Snowflake.parse(raw['channel_id']!),
guildId: guildId,
userId: Snowflake.parse(raw['user_id']!),
emoji: maybeParse(raw['emoji'], client.guilds[guildId].emojis.parse),
animationType: maybeParse(raw['animation_type'], AnimationType.new),
animationId: raw['animation_id'] as int?,
soundId: maybeParse(raw['sound_id'], Snowflake.parse),
soundVolume: raw['sound_volume'] as double?,
);
}

/// Parse a [VoiceStateUpdateEvent] from [raw].
VoiceStateUpdateEvent parseVoiceStateUpdate(Map<String, Object?> raw) {
final voiceState = client.voice.parseVoiceState(raw);
Expand Down Expand Up @@ -1162,6 +1185,52 @@ class Gateway extends GatewayManager with EventParser {
}
}

SoundboardSoundCreateEvent parseSoundboardSoundCreate(Map<String, Object?> raw) {
final guildId = maybeParse(raw['guild_id'], Snowflake.parse);

return SoundboardSoundCreateEvent(
gateway: this,
sound: client.guilds[guildId ?? Snowflake.zero].soundboard.parse(raw),
);
}

SoundboardSoundUpdateEvent parseSoundboardSoundUpdate(Map<String, Object?> raw) {
final guildId = maybeParse(raw['guild_id'], Snowflake.parse);

return SoundboardSoundUpdateEvent(
gateway: this,
oldSound: client.guilds[guildId ?? Snowflake.zero].soundboard.cache[Snowflake.parse(raw['sound_id']!)],
sound: client.guilds[guildId ?? Snowflake.zero].soundboard.parse(raw),
);
}

SoundboardSoundDeleteEvent parseSoundboardSoundDelete(Map<String, Object?> raw) {
final guildId = Snowflake.parse(raw['guild_id']!);
final soundId = Snowflake.parse(raw['sound_id']!);

return SoundboardSoundDeleteEvent(
gateway: this,
sound: client.guilds[guildId].soundboard.cache[soundId],
guildId: guildId,
soundId: soundId,
);
}

SoundboardSoundsUpdateEvent parseSoundboardSoundsUpdate(Map<String, Object?> raw) {
final guildId = Snowflake.parse(raw['guild_id']!);

final sounds = parseMany(raw['sounds'] as List<Object?>, client.guilds[guildId].soundboard.parse);

final oldSounds = sounds.map((sound) => client.guilds[guildId].soundboard.cache[sound.id]).toList();

return SoundboardSoundsUpdateEvent(
gateway: this,
guildId: guildId,
sounds: sounds,
oldSounds: oldSounds,
);
}

/// Update the client's voice state in the guild with ID [guildId].
void updateVoiceState(Snowflake guildId, GatewayVoiceStateBuilder builder) => shardFor(guildId).updateVoiceState(guildId, builder);

Expand Down
Loading

0 comments on commit d93aa12

Please sign in to comment.