From d30e9b5ede090adcc756e140c8bd7fa23e2431cb Mon Sep 17 00:00:00 2001 From: Anton Date: Sun, 19 Nov 2023 14:50:29 +0200 Subject: [PATCH 1/4] add permissions (#590) --- lib/src/models/permissions.dart | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/src/models/permissions.dart b/lib/src/models/permissions.dart index b844312f7..1e63f2920 100644 --- a/lib/src/models/permissions.dart +++ b/lib/src/models/permissions.dart @@ -95,7 +95,7 @@ class Permissions extends Flags { /// Allows management and editing of webhooks. static const manageWebhooks = Flag.fromOffset(29); - /// Allows management and editing of emojis, stickers, and soundboard sounds. + /// Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users. static const manageEmojisAndStickers = Flag.fromOffset(30); /// Allows members to use application commands, including slash commands and context menu commands.. @@ -104,7 +104,7 @@ class Permissions extends Flags { /// Allows for requesting to speak in stage channels. (This permission is under active development and may be changed or removed.). static const requestToSpeak = Flag.fromOffset(32); - /// Allows for creating, editing, and deleting scheduled events. + /// Allows for editing and deleting scheduled events created by all users. static const manageEvents = Flag.fromOffset(33); /// Allows for deleting and archiving threads, and viewing all private threads. @@ -134,8 +134,20 @@ class Permissions extends Flags { /// Allows for using soundboard in a voice channel. static const useSoundboard = Flag.fromOffset(42); + /// Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user. + static const createEmojiAndStickers = Flag.fromOffset(43); + + /// Allows for creating scheduled events, and editing and deleting those created by the current user. + static const createEvents = Flag.fromOffset(44); + + /// Allows the usage of custom soundboard sounds from other servers. + static const useExternalSounds = Flag.fromOffset(45); + + /// Allows sending voice messages. + static const sendVoiceMessages = Flag.fromOffset(46); + /// A [Permissions] with all permissions enabled. - static const allPermissions = Permissions(1099511627775); + static const allPermissions = Permissions(140737488355327); /// Whether this set of permissions has the [createInstantInvite] permission. bool get canCreateInstantInvite => has(createInstantInvite); @@ -266,6 +278,18 @@ class Permissions extends Flags { /// Whether this set of permissions has the [useSoundboard] permission. bool get canUseSoundboard => has(useSoundboard); + /// Whether this set of permissions has the [createEmojiAndStickers] permission. + bool get canCreateEmojiAndStickers => has(createEmojiAndStickers); + + /// Whether this set of permissions has the [createEvents] permission. + bool get canCreateEvents => has(createEvents); + + /// Whether this set of permissions has the [useExternalSounds] permission. + bool get canUseExternalSounds => has(useExternalSounds); + + /// Whether this set of permissions has the [sendVoiceMessages] permission. + bool get canSendVoiceMessages => has(sendVoiceMessages); + /// Create a new [Permissions] from a permissions value. const Permissions(super.value); } From 573c9bc6046474b6dfc48c152d938f9bb3e8af1a Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:38:40 +0100 Subject: [PATCH 2/4] Correctly build autocompletion responses (#591) * Correctly build autocompletion responses * Add tests --- lib/src/builders/interaction_response.dart | 2 +- .../builders/interaction_response_test.dart | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/unit/builders/interaction_response_test.dart diff --git a/lib/src/builders/interaction_response.dart b/lib/src/builders/interaction_response.dart index b0263949f..7771cf612 100644 --- a/lib/src/builders/interaction_response.dart +++ b/lib/src/builders/interaction_response.dart @@ -49,7 +49,7 @@ class InteractionResponseBuilder extends CreateBuilder> choices) => InteractionResponseBuilder( type: InteractionCallbackType.applicationCommandAutocompleteResult, - data: {'choices': choices}, + data: {'choices': choices.map((e) => e.build()).toList()}, ); factory InteractionResponseBuilder.modal(ModalBuilder modal) => InteractionResponseBuilder(type: InteractionCallbackType.modal, data: modal); diff --git a/test/unit/builders/interaction_response_test.dart b/test/unit/builders/interaction_response_test.dart new file mode 100644 index 000000000..e5606a588 --- /dev/null +++ b/test/unit/builders/interaction_response_test.dart @@ -0,0 +1,20 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:test/test.dart'; + +void main() { + group('InteractionResponseBuilder', () { + test('autocompleteResult', () { + expect( + InteractionResponseBuilder.autocompleteResult([CommandOptionChoiceBuilder(name: 'foo', value: 'bar')]).build(), + equals({ + 'type': 8, + 'data': { + 'choices': [ + {'name': 'foo', 'value': 'bar'}, + ] + } + }), + ); + }); + }); +} From 46f128c9d165775ec1cee6d129238ee8ec8ae0d8 Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sun, 26 Nov 2023 20:09:43 +0100 Subject: [PATCH 3/4] Try to fix invalid sessions received when reconnecting to the Gateway (#592) * Adjust log level for unresumable sessions * Add delay when catching errors on the shard runner * Fix logging plugin not directing logs to the right output at stderrLevel * Remove duplicate error logging * Add logging for payloads sent by shard * Use error close code when reconnecting --- lib/nyxx.dart | 2 +- lib/src/gateway/gateway.dart | 9 ++------- lib/src/gateway/message.dart | 9 +++++++++ lib/src/gateway/shard.dart | 10 +++++++--- lib/src/gateway/shard_runner.dart | 29 +++++++++++++++++++---------- lib/src/plugin/logging.dart | 2 +- 6 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 3c6582746..870b1d66a 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -100,7 +100,7 @@ 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; +export 'src/gateway/message.dart' show Disconnecting, Dispose, ErrorReceived, EventReceived, GatewayMessage, Send, Sent, ShardData, ShardMessage; export 'src/gateway/shard.dart' show Shard; export 'src/models/discord_color.dart' show DiscordColor; diff --git a/lib/src/gateway/gateway.dart b/lib/src/gateway/gateway.dart index ca886858e..e20a8cc2b 100644 --- a/lib/src/gateway/gateway.dart +++ b/lib/src/gateway/gateway.dart @@ -92,13 +92,7 @@ class Gateway extends GatewayManager with EventParser { Gateway(this.client, this.gatewayBot, this.shards, this.totalShards, this.shardIds) : super.create() { for (final shard in shards) { shard.listen( - (message) { - if (message is ErrorReceived) { - shard.logger.warning('Received error: ${message.error}', message.error, message.stackTrace); - } - - _messagesController.add(message); - }, + _messagesController.add, onError: _messagesController.addError, onDone: () async { if (_closing) { @@ -109,6 +103,7 @@ class Gateway extends GatewayManager with EventParser { throw ShardDisconnectedError(shard); }, + cancelOnError: false, ); } diff --git a/lib/src/gateway/message.dart b/lib/src/gateway/message.dart index 1da7da02e..098d36d35 100644 --- a/lib/src/gateway/message.dart +++ b/lib/src/gateway/message.dart @@ -61,6 +61,15 @@ class Disconnecting extends ShardMessage { Disconnecting({required this.reason}); } +/// A shard message sent when the shard adds a payload to the connection. +class Sent extends ShardMessage { + /// The payload that was sent. + final Send payload; + + /// Create a new [Sent]. + Sent({required this.payload}); +} + /// The base class for all control messages sent from the client to the shard. abstract class GatewayMessage with ToStringHelper {} diff --git a/lib/src/gateway/shard.dart b/lib/src/gateway/shard.dart index f70697f8a..1261abc51 100644 --- a/lib/src/gateway/shard.dart +++ b/lib/src/gateway/shard.dart @@ -45,7 +45,11 @@ class Shard extends Stream implements StreamSink { /// Create a new [Shard]. Shard(this.id, this.isolate, this.receiveStream, this.sendPort, this.client) { final subscription = listen((message) { - if (message is ErrorReceived) { + if (message is Sent) { + logger + ..fine('Sent payload: ${message.payload.opcode.name}') + ..finer('Opcode: ${message.payload.opcode.value}, Data: ${message.payload.data}'); + } else if (message is ErrorReceived) { logger.warning('Error: ${message.error}', message.error, message.stackTrace); } else if (message is Disconnecting) { logger.info('Disconnecting: ${message.reason}'); @@ -61,7 +65,7 @@ class Shard extends Stream implements StreamSink { if (isResumable) { logger.info('Reconnecting: invalid session'); } else { - logger.severe('Unresumable invalid session, disconnecting'); + logger.warning('Reconnecting: unresumable invalid session'); } case HelloEvent(:final heartbeatInterval): logger.finest('Heartbeat Interval: $heartbeatInterval'); @@ -141,7 +145,7 @@ class Shard extends Stream implements StreamSink { void add(GatewayMessage event) { if (event is Send) { logger - ..fine('Send: ${event.opcode.name}') + ..fine('Sending: ${event.opcode.name}') ..finer('Opcode: ${event.opcode.value}, Data: ${event.data}'); } else if (event is Dispose) { logger.info('Disposing'); diff --git a/lib/src/gateway/shard_runner.dart b/lib/src/gateway/shard_runner.dart index 80655bdab..6dc85d44f 100644 --- a/lib/src/gateway/shard_runner.dart +++ b/lib/src/gateway/shard_runner.dart @@ -58,7 +58,7 @@ class ShardRunner { final controller = StreamController(); // The subscription to the control messages stream. - // This subscription is paused whenever the shard is not successfully connected,. + // This subscription is paused whenever the shard is not successfully connected. final controlSubscription = messages.listen((message) { if (message is Send) { connection!.add(message); @@ -87,6 +87,7 @@ class ShardRunner { // Open the websocket connection. connection = await ShardConnection.connect(gatewayUri.toString(), this); + connection!.onSent.listen(controller.add); // Obtain the heartbeat interval from the HELLO event and start heartbeating. final hello = await connection!.first; @@ -105,7 +106,7 @@ class ShardRunner { sendIdentify(); } - canResume = false; + canResume = true; // We are connected, start handling control messages. controlSubscription.resume(); @@ -127,7 +128,7 @@ class ShardRunner { } } else if (event is ReconnectEvent) { canResume = true; - connection!.close(); + connection!.close(4000); } else if (event is InvalidSessionEvent) { if (event.isResumable) { canResume = true; @@ -136,7 +137,8 @@ class ShardRunner { gatewayUri = originalGatewayUri; } - connection!.close(); + // Don't use 4000 as it will always try to resume + connection!.close(4999); } else if (event is HeartbeatAckEvent) { lastHeartbeatAcked = true; heartbeatStopwatch = null; @@ -159,8 +161,7 @@ class ShardRunner { // Check if we can resume based on close code. // A manual close where we set closeCode earlier would have a close code of 1000, so this // doesn't change closeCode if we set it manually. - // 1001 is the close code used for a ping failure, so include it in the resumable codes. - const resumableCodes = [null, 1001, 4000, 4001, 4002, 4003, 4007, 4008, 4009]; + const resumableCodes = [null, 4000, 4001, 4002, 4003, 4007, 4008, 4009]; final closeCode = connection!.websocket.closeCode; canResume = canResume || resumableCodes.contains(closeCode); @@ -171,9 +172,11 @@ class ShardRunner { } } catch (error, stackTrace) { controller.add(ErrorReceived(error: error, stackTrace: stackTrace)); + // Prevents the while-true loop from looping too often when no internet is available. + await Future.delayed(Duration(milliseconds: 100)); } finally { // Reset connection properties. - connection?.close(); + connection?.close(4000); connection = null; heartbeatTimer?.cancel(); heartbeatTimer = null; @@ -246,11 +249,13 @@ class ShardConnection extends Stream implements StreamSink { final Stream events; final ShardRunner runner; + final StreamController _sentController = StreamController(); + Stream get onSent => _sentController.stream; + ShardConnection(this.websocket, this.events, this.runner); static Future connect(String gatewayUri, ShardRunner runner) async { final connection = await WebSocket.connect(gatewayUri); - connection.pingInterval = const Duration(seconds: 20); final uncompressedStream = switch (runner.data.apiOptions.compression) { GatewayCompression.transport => decompressTransport(connection.cast>()), @@ -293,6 +298,7 @@ class ShardConnection extends Stream implements StreamSink { }; websocket.add(encoded); + _sentController.add(Sent(payload: event)); } @override @@ -302,10 +308,13 @@ class ShardConnection extends Stream implements StreamSink { Future addStream(Stream stream) => stream.forEach(add); @override - Future close([int? code]) => websocket.close(code ?? 1000); + Future close([int? code]) async { + await websocket.close(code ?? 1000); + await _sentController.close(); + } @override - Future get done => websocket.done; + Future get done => websocket.done.then((_) => _sentController.done); } Stream decompressTransport(Stream> raw) { diff --git a/lib/src/plugin/logging.dart b/lib/src/plugin/logging.dart index a2ce512a2..7567ef32e 100644 --- a/lib/src/plugin/logging.dart +++ b/lib/src/plugin/logging.dart @@ -108,7 +108,7 @@ class Logging extends NyxxPlugin { } } - final outSink = rec.level > stderrLevel ? stderr : stdout; + final outSink = rec.level >= stderrLevel ? stderr : stdout; outSink.write(messageString); }); } From 43df51bb4922aa486f055924db5df647992cdb75 Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sun, 26 Nov 2023 20:41:05 +0100 Subject: [PATCH 4/4] Release 6.0.3 (#593) --- CHANGELOG.md | 6 ++++++ lib/src/api_options.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460d5d640..3f150f1a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.3 +__26.11.2023__ + +- bug: Fix incorrect serialization of autocompletion interaction responses (again) +- bug: Try to fix invalid sessions triggered by Gateway reconnects + ## 6.0.2 __16.11.2023__ diff --git a/lib/src/api_options.dart b/lib/src/api_options.dart index 48ee92b0a..b1ce81d43 100644 --- a/lib/src/api_options.dart +++ b/lib/src/api_options.dart @@ -6,7 +6,7 @@ import 'package:oauth2/oauth2.dart'; /// Options for connecting to the Discord API. abstract class ApiOptions { /// The version of nyxx used in [defaultUserAgent]. - static const nyxxVersion = '6.0.2'; + static const nyxxVersion = '6.0.3'; /// The URL to the nyxx repository used in [defaultUserAgent]. static const nyxxRepositoryUrl = 'https://github.com/nyxx-discord/nyxx'; diff --git a/pubspec.yaml b/pubspec.yaml index 715f26c8d..4c7231eef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 6.0.2 +version: 6.0.3 description: A complete, robust and efficient wrapper around Discord's API for bots & applications. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx