diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 8ff513fb7..78cece37c 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -192,16 +192,7 @@ export 'src/models/voice/voice_region.dart' show VoiceRegion; export 'src/models/role.dart' show PartialRole, Role, RoleTags; export 'src/models/gateway/gateway.dart' show GatewayBot, GatewayConfiguration, SessionStartLimit; export 'src/models/gateway/event.dart' - show - DispatchEvent, - GatewayEvent, - HeartbeatAckEvent, - HeartbeatEvent, - HelloEvent, - InvalidSessionEvent, - RawDispatchEvent, - ReconnectEvent, - UnknownDispatchEvent; + show DispatchEvent, Event, HeartbeatAckEvent, HeartbeatEvent, HelloEvent, InvalidSessionEvent, RawDispatchEvent, ReconnectEvent, UnknownDispatchEvent; export 'src/models/gateway/opcode.dart' show Opcode; export 'src/models/gateway/events/application_command.dart' show ApplicationCommandPermissionsUpdateEvent; export 'src/models/gateway/events/auto_moderation.dart' diff --git a/lib/src/event_mixin.dart b/lib/src/event_mixin.dart index 628f8a5ac..604247778 100644 --- a/lib/src/event_mixin.dart +++ b/lib/src/event_mixin.dart @@ -21,9 +21,9 @@ import 'package:nyxx/src/utils/iterable_extension.dart'; mixin EventRestMixin implements Nyxx { @internal - StreamController onEventController = StreamController.broadcast(); + StreamController onEventController = StreamController.broadcast(); - Stream get onEvent => onEventController.stream; + Stream get onEvent => onEventController.stream; /// A [Stream] of [InteractionCreateEvent]s received by this client. Stream get onInteractionCreate => onEvent.whereType(); @@ -33,7 +33,7 @@ mixin EventRestMixin implements Nyxx { mixin EventMixin implements Nyxx, EventRestMixin { /// A [Stream] of gateway dispatch events received by this client. @override - Stream get onEvent => (this as NyxxGateway).gateway.events; + Stream get onEvent => (this as NyxxGateway).gateway.events; /// A [Stream] of [DispatchEvent]s which are unknown to the current version of nyxx. Stream get onUnknownEvent => onEvent.whereType(); diff --git a/lib/src/gateway/event_parser.dart b/lib/src/gateway/event_parser.dart index cc07aed1b..673d49d72 100644 --- a/lib/src/gateway/event_parser.dart +++ b/lib/src/gateway/event_parser.dart @@ -4,7 +4,7 @@ import 'package:nyxx/src/models/gateway/opcode.dart'; /// An internal class which allows the shard runner to parse gateway events /// without having a reference to the client's [GatewayManager]. mixin class EventParser { - GatewayEvent parseGatewayEvent(Map raw, {Duration? heartbeatLatency}) { + Event parseGatewayEvent(Map raw, {Duration? heartbeatLatency}) { final mapping = { Opcode.dispatch.value: parseDispatch, Opcode.heartbeat.value: parseHeartbeat, diff --git a/lib/src/gateway/gateway.dart b/lib/src/gateway/gateway.dart index a0f3b535b..4445a1c49 100644 --- a/lib/src/gateway/gateway.dart +++ b/lib/src/gateway/gateway.dart @@ -67,7 +67,7 @@ class Gateway extends GatewayManager with EventParser { final StreamController _messagesController = StreamController.broadcast(); /// A stream of dispatch events received from all shards. - Stream get events => messages.map((message) { + Stream get events => messages.map((message) { if (message is! EventReceived) { return null; } @@ -259,7 +259,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)); - DispatchEvent parseDispatchEvent(RawDispatchEvent raw) { + Event parseDispatchEvent(RawDispatchEvent raw) { final mapping = { 'READY': parseReady, 'RESUMED': parseResumed, @@ -949,14 +949,12 @@ class Gateway extends GatewayManager with EventParser { // Needed to get proper type promotion. return switch (interaction.type) { - InteractionType.ping => InteractionCreateEvent(gateway: this, interaction: interaction as PingInteraction), - InteractionType.applicationCommand => - InteractionCreateEvent(gateway: this, interaction: interaction as ApplicationCommandInteraction), - InteractionType.messageComponent => - InteractionCreateEvent(gateway: this, interaction: interaction as MessageComponentInteraction), - InteractionType.modalSubmit => InteractionCreateEvent(gateway: this, interaction: interaction as ModalSubmitInteraction), + InteractionType.ping => InteractionCreateEvent(interaction: interaction as PingInteraction), + InteractionType.applicationCommand => InteractionCreateEvent(interaction: interaction as ApplicationCommandInteraction), + InteractionType.messageComponent => InteractionCreateEvent(interaction: interaction as MessageComponentInteraction), + InteractionType.modalSubmit => InteractionCreateEvent(interaction: interaction as ModalSubmitInteraction), InteractionType.applicationCommandAutocomplete => - InteractionCreateEvent(gateway: this, interaction: interaction as ApplicationCommandAutocompleteInteraction), + InteractionCreateEvent(interaction: interaction as ApplicationCommandAutocompleteInteraction), } as InteractionCreateEvent>; } diff --git a/lib/src/gateway/message.dart b/lib/src/gateway/message.dart index 1da7da02e..ac6db92a2 100644 --- a/lib/src/gateway/message.dart +++ b/lib/src/gateway/message.dart @@ -34,7 +34,7 @@ abstract class ShardMessage with ToStringHelper {} /// A shard message sent when an event is received on the Gateway. class EventReceived extends ShardMessage { /// The event that was received. - final GatewayEvent event; + final Event event; /// Create a new [EventReceived]. EventReceived({required this.event}); diff --git a/lib/src/gateway/shard.dart b/lib/src/gateway/shard.dart index f70697f8a..3b22378eb 100644 --- a/lib/src/gateway/shard.dart +++ b/lib/src/gateway/shard.dart @@ -52,7 +52,7 @@ class Shard extends Stream implements StreamSink { } else if (message is EventReceived) { final event = message.event; - if (event is! RawDispatchEvent) { + if (event is GatewayEvent) { logger.finer('Receive: ${event.opcode.name}'); switch (event) { @@ -72,7 +72,7 @@ class Shard extends Stream implements StreamSink { default: break; } - } else { + } else if (event is RawDispatchEvent) { logger ..fine('Receive event: ${event.name}') ..finer('Seq: ${event.seq}, Data: ${event.payload}'); @@ -82,6 +82,8 @@ class Shard extends Stream implements StreamSink { } else if (event.name == 'RESUMED') { logger.info('Reconnected to Gateway'); } + } else { + logger.fine('Receive event: ${event.runtimeType}'); // TODO: Proper logging } } }); diff --git a/lib/src/gateway/shard_runner.dart b/lib/src/gateway/shard_runner.dart index 80655bdab..73c6a7ee7 100644 --- a/lib/src/gateway/shard_runner.dart +++ b/lib/src/gateway/shard_runner.dart @@ -241,9 +241,9 @@ class ShardRunner { } } -class ShardConnection extends Stream implements StreamSink { +class ShardConnection extends Stream implements StreamSink { final WebSocket websocket; - final Stream events; + final Stream events; final ShardRunner runner; ShardConnection(this.websocket, this.events, this.runner); @@ -271,8 +271,8 @@ class ShardConnection extends Stream implements StreamSink { } @override - StreamSubscription listen( - void Function(GatewayEvent event)? onData, { + StreamSubscription listen( + void Function(Event event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, diff --git a/lib/src/models/gateway/event.dart b/lib/src/models/gateway/event.dart index 1f969c45e..3f5b698ea 100644 --- a/lib/src/models/gateway/event.dart +++ b/lib/src/models/gateway/event.dart @@ -5,11 +5,12 @@ import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template gateway_event} /// The base class for all events received from the Gateway. /// {@endtemplate} -abstract class GatewayEvent with ToStringHelper { +abstract class Event with ToStringHelper {} + +abstract class GatewayEvent extends Event { /// The opcode of this event. final Opcode opcode; - /// {@macro gateway_event} GatewayEvent({required this.opcode}); } diff --git a/lib/src/models/gateway/events/interaction.dart b/lib/src/models/gateway/events/interaction.dart index cc73280a6..bd4eaeb09 100644 --- a/lib/src/models/gateway/events/interaction.dart +++ b/lib/src/models/gateway/events/interaction.dart @@ -4,10 +4,10 @@ import 'package:nyxx/src/models/interaction.dart'; /// {@template interaction_create_event} /// Emitted when an interaction is received by the client. /// {@endtemplate} -class InteractionCreateEvent> extends DispatchEvent { +class InteractionCreateEvent> extends Event { // The created interaction. final T interaction; /// {@macro interaction_create_event} - InteractionCreateEvent({required super.gateway, required this.interaction}); + InteractionCreateEvent({required this.interaction}); } diff --git a/lib/src/plugin/http_interactions.dart b/lib/src/plugin/http_interactions.dart index 7fefba9cb..5587dbce9 100644 --- a/lib/src/plugin/http_interactions.dart +++ b/lib/src/plugin/http_interactions.dart @@ -5,6 +5,8 @@ import 'package:nyxx/nyxx.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:pinenacl/ed25519.dart'; + class HttpInteractions extends NyxxPlugin { @override String get name => runtimeType.toString(); @@ -12,34 +14,53 @@ class HttpInteractions extends NyxxPlugin { final String _webhookPath; final int _port; final String _host; + final String _publicKey; - HttpInteractions(this._webhookPath, this._host, this._port); + HttpInteractions(this._webhookPath, this._host, this._port, this._publicKey); @override FutureOr afterConnect(NyxxRest client) async { - var handler = const shelf.Pipeline() - .addMiddleware(shelf.logRequests()) - .addHandler((shelf.Request request) => _echoRequest(request, client)); + final handler = const shelf.Pipeline().addMiddleware(shelf.logRequests()).addHandler((shelf.Request request) => _echoRequest(request, client)); - var server = await shelf_io.serve(handler, _host, _port); + final server = await shelf_io.serve(handler, _host, _port); server.autoCompress = true; } Future _echoRequest(shelf.Request request, NyxxRest client) async { + if (!_webhookPath.startsWith(request.url.toString())) { + return shelf.Response.badRequest(); + } + if (request.method != 'POST') { return shelf.Response.badRequest(); } - final body = jsonDecode( - await request.readAsString() - ) as Map; + final requestBody = await request.readAsString(); + + final isRequestValid = await _validateSignature(request.headers['X-Signature-Ed25519']!, request.headers['X-Signature-Timestamp']!, requestBody.trim()); + if (!isRequestValid) { + return shelf.Response.unauthorized({}); + } + + final body = jsonDecode(requestBody) as Map; final interaction = client.interactions.parse(body); + client.onEventController.add(InteractionCreateEvent(interaction: interaction)); + + if (interaction.type == InteractionType.ping) { + print("Responding to ping!"); + print(jsonEncode({"type": 1})); + + return shelf.Response.ok(jsonEncode({"type": 1})); + } + + return shelf.Response.ok({}); + } - client.onEventController.add( - InteractionCreateEvent() - ) + Future _validateSignature(String signature, String timestamp, String requestBody) async { + const encoder = Base16Encoder.instance; - return shelf.Response.badRequest(); + return VerifyKey(encoder.decode(_publicKey)) + .verify(signature: Signature(encoder.decode(signature)), message: Uint8List.fromList(utf8.encode("$timestamp$requestBody"))); } } diff --git a/pubspec.yaml b/pubspec.yaml index c58f4bd88..56df7ae5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,9 @@ dependencies: runtime_type: ^1.0.1 meta: ^1.9.1 oauth2: ^2.0.2 + shelf: ^1.4.1 + pinenacl: ^0.5.0 dev_dependencies: test: ^1.22.0