From f29d3d2dbb80d1335852fb5c0ae1608acbfa62b4 Mon Sep 17 00:00:00 2001 From: Freeze455 Date: Thu, 9 Jun 2022 15:08:37 +0200 Subject: [PATCH 1/2] feat: Implement command handlers --- lib/api.dart | 3 + .../api/interactions/command_interaction.dart | 62 +++++++++++++++++++ lib/src/api/interactions/interaction.dart | 12 ++++ lib/src/api/utils.dart | 20 ++++++ lib/src/constants.dart | 2 + .../internal/entities/command_manager.dart | 45 +++++++++----- lib/src/internal/entities/event_manager.dart | 4 +- .../websockets/packets/guild_create.dart | 5 +- .../packets/interaction_create.dart | 59 ++++++++++++++++++ .../websockets/websocket_dispatcher.dart | 2 + 10 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 lib/src/api/interactions/command_interaction.dart create mode 100644 lib/src/api/interactions/interaction.dart create mode 100644 lib/src/internal/websockets/packets/interaction_create.dart diff --git a/lib/api.dart b/lib/api.dart index 1e4be12f..e93cb4a0 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -31,6 +31,9 @@ export 'src/api/components/modal.dart' show Modal; export 'src/api/components/text_input.dart' show TextInputStyle; export 'src/api/components/button.dart' show Button, ButtonStyle; export 'src/api/components/link.dart' show Link; + +export 'src/api/interactions/command_interaction.dart' show CommandInteraction; + export 'src/api/utils.dart'; typedef Snowflake = String; diff --git a/lib/src/api/interactions/command_interaction.dart b/lib/src/api/interactions/command_interaction.dart new file mode 100644 index 00000000..6918d771 --- /dev/null +++ b/lib/src/api/interactions/command_interaction.dart @@ -0,0 +1,62 @@ +import 'package:mineral/api.dart'; +import 'package:mineral/src/api/interactions/interaction.dart'; + +class CommandInteraction extends Interaction { + Snowflake id; + String identifier; + Map data = {}; + + CommandInteraction({ + required this.identifier, + required this.id, + required InteractionType type, + required Snowflake applicationId, + required int version, + required String token, + required User user + }) : super(version: version, token: token, type: type, user: user, applicationId: applicationId); + + T? getChannel (String optionName) { + return guild?.channels.cache.get(data[optionName]['value']); + } + + int? getInteger (String optionName) { + return data[optionName]['value']; + } + + String? getString (String optionName) { + return data[optionName]['value']; + } + + GuildMember? getMember (String optionName) { + return guild?.members.cache.get(data[optionName]['value']); + } + + bool? getBoolean (String optionName) { + return data[optionName]['value']; + } + + Role? getRole (String optionName) { + return guild?.roles.cache.get(data[optionName]['value']); + } + + T? getChoice (String optionName) { + return data[optionName]['value']; + } + + dynamic getMentionable (String optionName) { + return data[optionName]['value']; + } + + factory CommandInteraction.from({ required User user, required dynamic payload }) { + return CommandInteraction( + id: payload['data']['id'], + applicationId: payload['application_id'], + type: InteractionType.values.firstWhere((type) => type.value == payload['type']), + identifier: payload['data']['name'], + version: payload['version'], + token: payload['token'], + user: user, + ); + } +} diff --git a/lib/src/api/interactions/interaction.dart b/lib/src/api/interactions/interaction.dart new file mode 100644 index 00000000..3ca26987 --- /dev/null +++ b/lib/src/api/interactions/interaction.dart @@ -0,0 +1,12 @@ +import 'package:mineral/api.dart'; + +class Interaction { + Snowflake applicationId; + int version; + InteractionType type; + String token; + User user; + Guild? guild; + + Interaction({ required this.applicationId, required this.version, required this.type, required this.token, required this.user }); +} diff --git a/lib/src/api/utils.dart b/lib/src/api/utils.dart index b4e2eaee..2121ac3f 100644 --- a/lib/src/api/utils.dart +++ b/lib/src/api/utils.dart @@ -1,3 +1,23 @@ +enum InteractionType { + ping(1), + applicationCommand(2), + messageComponent(3), + applicationCommandAutocomplete(4), + modalSubmit(5); + + final int value; + const InteractionType(this.value); +} + +enum ApplicationCommandType { + chatInput(1), + user(2), + message(3); + + final int value; + const ApplicationCommandType(this.value); +} + enum NotificationLevel { allMessages(0), onlyMentions(1); diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 2042bc96..c32e55f7 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -30,6 +30,8 @@ enum PacketType { channelUpdate('CHANNEL_UPDATE'), channelDelete('CHANNEL_DELETE'), + interactionCreate('INTERACTION_CREATE'), + memberUpdate('GUILD_MEMBER_UPDATE'); final String _value; diff --git a/lib/src/internal/entities/command_manager.dart b/lib/src/internal/entities/command_manager.dart index c251c28c..2522a96f 100644 --- a/lib/src/internal/entities/command_manager.dart +++ b/lib/src/internal/entities/command_manager.dart @@ -3,17 +3,9 @@ import 'dart:mirrors'; import 'package:mineral/api.dart'; import 'package:mineral/core.dart'; -enum ApplicationCommandType { - chatInput(1), - user(2), - message(3); - - final int value; - const ApplicationCommandType(this.value); -} - class CommandManager { - final Collection _commands = Collection(); + final Collection commands = Collection(); + final Map handlers = {}; CommandManager add (Object object) { MineralCommand command = MineralCommand(name: '', description: '', scope: '', options: []); @@ -28,13 +20,13 @@ class CommandManager { object: object ); - _commands.set(command.name, command); + commands.set(command.name, command); return this; } List getFromGuild (Guild guild) { List commands = []; - _commands.forEach((name, command) { + this.commands.forEach((name, command) { if (command.scope == guild.id || command.scope == 'GUILD') { commands.add(command); } @@ -45,7 +37,7 @@ class CommandManager { List getGlobals () { List commands = []; - _commands.forEach((name, command) { + this.commands.forEach((name, command) { if (command.scope == 'GLOBAL') { commands.add(command); } @@ -55,7 +47,10 @@ class CommandManager { } void _registerCommands ({ required MineralCommand command, required Object object }) { - reflect(object).type.metadata.forEach((element) { + ClassMirror classMirror = reflect(object).type; + dynamic classCommand = reflect(object).reflectee; + + for (InstanceMirror element in classMirror.metadata) { dynamic reflectee = element.reflectee; if (reflectee is CommandGroup) { @@ -72,15 +67,25 @@ class CommandManager { ..name = reflectee.name ..description = reflectee.description ..scope = reflectee.scope; + + if (classMirror.instanceMembers.values.toList().where((element) => element.simpleName == Symbol('handle')).isNotEmpty) { + MethodMirror handle = classMirror.instanceMembers.values.toList().firstWhere((element) => element.simpleName == Symbol('handle')); + handlers.putIfAbsent(command.name, () => { + 'symbol': handle.simpleName, + 'commandClass': classCommand, + }); + } + } if (reflectee is Option) { command.options.add(reflectee); } - }); + } } void _registerCommandMethods ({ required MineralCommand command, required Object object }) { + dynamic classCommand = reflect(object).reflectee; reflect(object).type.declarations.forEach((key, value) { if (value.metadata.isEmpty) { return; @@ -99,9 +104,19 @@ class CommandManager { if (groupName != null) { MineralCommand group = command.groups.firstWhere((group) => group.name == groupName); group.subcommands.add(subcommand); + + handlers.putIfAbsent("${command.name}.${group.name}.${subcommand.name}", () => { + 'symbol': Symbol(subcommand.name), + 'commandClass': classCommand, + }); } else { command.subcommands.add(subcommand); + handlers.putIfAbsent("${command.name}.${subcommand.name}", () => { + 'symbol': Symbol(subcommand.name), + 'commandClass': classCommand, + }); } + } if (reflectee is Option) { diff --git a/lib/src/internal/entities/event_manager.dart b/lib/src/internal/entities/event_manager.dart index 072331e0..46f79930 100644 --- a/lib/src/internal/entities/event_manager.dart +++ b/lib/src/internal/entities/event_manager.dart @@ -51,7 +51,9 @@ enum Events { memberUpdate('update::member'), memberRolesUpdate('update::roles-member'), - acceptRules('accept::rules'); + acceptRules('accept::rules'), + + commandCreate('create::commandInteraction'); final String event; const Events(this.event); diff --git a/lib/src/internal/websockets/packets/guild_create.dart b/lib/src/internal/websockets/packets/guild_create.dart index c5e8485e..56d37624 100644 --- a/lib/src/internal/websockets/packets/guild_create.dart +++ b/lib/src/internal/websockets/packets/guild_create.dart @@ -40,8 +40,9 @@ class GuildCreate implements WebsocketPacket { ChannelManager channelManager = ChannelManager(guildId: websocketResponse.payload['id']); for(dynamic payload in websocketResponse.payload['channels']) { - if (channels.containsKey(payload['type'])) { - Channel Function(dynamic payload) item = channels[payload['type']] as Channel Function(dynamic payload); + ChannelType channelType = ChannelType.values.firstWhere((type) => type.value == payload['type']); + if (channels.containsKey(channelType)) { + Channel Function(dynamic payload) item = channels[channelType] as Channel Function(dynamic payload); Channel channel = item(payload); channelManager.cache.putIfAbsent(channel.id, () => channel); diff --git a/lib/src/internal/websockets/packets/interaction_create.dart b/lib/src/internal/websockets/packets/interaction_create.dart new file mode 100644 index 00000000..11d00756 --- /dev/null +++ b/lib/src/internal/websockets/packets/interaction_create.dart @@ -0,0 +1,59 @@ +import 'dart:mirrors'; + +import 'package:mineral/api.dart'; +import 'package:mineral/core.dart'; +import 'package:mineral/src/internal/entities/command_manager.dart'; +import 'package:mineral/src/internal/websockets/websocket_packet.dart'; +import 'package:mineral/src/internal/websockets/websocket_response.dart'; + +class InteractionCreate implements WebsocketPacket { + @override + PacketType packetType = PacketType.interactionCreate; + + @override + Future handle(WebsocketResponse websocketResponse) async { + CommandManager manager = ioc.singleton(Service.command); + MineralClient client = ioc.singleton(Service.client); + + dynamic payload = websocketResponse.payload; + + Guild? guild = client.guilds.cache.get(payload['guild_id']); + GuildMember? member = guild?.members.cache.get(payload['member']['user']['id']); + + CommandInteraction commandInteraction = CommandInteraction.from(user: member!.user, payload: payload); + commandInteraction.guild = guild; + + String identifier = commandInteraction.identifier; + + walk (List objects) { + for (dynamic object in objects) { + if (object['type'] == 1 || object['type'] == 2) { + identifier += ".${object['name']}"; + if (object['options'] != null) { + walk(object['options']); + } + } else { + commandInteraction.data.putIfAbsent(object['name'], () => object); + } + } + } + + walk(payload['data']['options']); + + dynamic handle = manager.handlers[identifier]; + reflect(handle['commandClass']).invoke(handle['symbol'], [commandInteraction]); + + // Channel? channel = guild?.channels.cache.get(payload['id']); + // + // if (channel == null) { + // channel = _dispatch(guild, payload); + // + // channel?.guildId = guild?.id; + // channel?.guild = guild; + // channel?.parent = channel.parentId != null ? guild?.channels.cache.get(channel.parentId) : null; + // + // guild?.channels.cache.putIfAbsent(channel!.id, () => channel!); + // } + // + } +} diff --git a/lib/src/internal/websockets/websocket_dispatcher.dart b/lib/src/internal/websockets/websocket_dispatcher.dart index 2add84a7..080c9589 100644 --- a/lib/src/internal/websockets/websocket_dispatcher.dart +++ b/lib/src/internal/websockets/websocket_dispatcher.dart @@ -5,6 +5,7 @@ import 'package:mineral/src/internal/websockets/packets/channel_delete.dart'; import 'package:mineral/src/internal/websockets/packets/channel_update.dart'; import 'package:mineral/src/internal/websockets/packets/guild_create.dart'; import 'package:mineral/src/internal/websockets/packets/guild_member_update.dart'; +import 'package:mineral/src/internal/websockets/packets/interaction_create.dart'; import 'package:mineral/src/internal/websockets/packets/message_create.dart'; import 'package:mineral/src/internal/websockets/packets/message_delete.dart'; import 'package:mineral/src/internal/websockets/packets/message_update.dart'; @@ -27,6 +28,7 @@ class WebsocketDispatcher { register(PacketType.channelDelete, ChannelDelete()); register(PacketType.channelUpdate, ChannelUpdate()); register(PacketType.memberUpdate, GuildMemberUpdate()); + register(PacketType.interactionCreate, InteractionCreate()); } void register (PacketType type, WebsocketPacket packet) { From e53d0400fd54b0f5ec2cb00c5ca2f780ce89c75d Mon Sep 17 00:00:00 2001 From: Freeze455 Date: Thu, 16 Jun 2022 13:31:04 +0200 Subject: [PATCH 2/2] feat: Work in progress --- lib/api.dart | 2 +- lib/src/api/client/mineral_client.dart | 9 +++-- .../api/interactions/command_interaction.dart | 35 ++++++++++++++++++- lib/src/api/interactions/interaction.dart | 13 +++++++ lib/src/api/message_embed.dart | 2 +- lib/src/api/user.dart | 8 +++++ .../internal/entities/command_manager.dart | 1 - .../packets/interaction_create.dart | 6 +++- 8 files changed, 66 insertions(+), 10 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index e93cb4a0..6a42305f 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -19,7 +19,7 @@ export 'src/api/channels/text_channel.dart' show TextChannel; export 'src/api/channels/category_channel.dart' show CategoryChannel; export 'src/api/message.dart' show Message; -export 'src/api/message_embed.dart' show MessageEmbed; +export 'src/api/message_embed.dart' show MessageEmbed, Footer, Image, Author, Field; export 'src/api/color.dart' show Color; export 'src/api/emoji.dart' show Emoji; diff --git a/lib/src/api/client/mineral_client.dart b/lib/src/api/client/mineral_client.dart index 9a0cdc5b..0c1f1764 100644 --- a/lib/src/api/client/mineral_client.dart +++ b/lib/src/api/client/mineral_client.dart @@ -1,5 +1,4 @@ -import 'dart:convert'; - +import 'package:http/http.dart'; import 'package:mineral/api.dart'; import 'package:mineral/core.dart'; import 'package:mineral/src/api/managers/guild_manager.dart'; @@ -65,12 +64,12 @@ class MineralClient { Future registerGuildCommands ({ required Guild guild, required List commands}) async { Http http = ioc.singleton(Service.http); - print(jsonEncode(commands.map((command) => command.toJson()).toList())); - - await http.put( + Response response = await http.put( url: "/applications/${application.id}/guilds/${guild.id}/commands", payload: commands.map((command) => command.toJson()).toList() ); + + print(response.body); } factory MineralClient.from({ required dynamic payload }) { diff --git a/lib/src/api/interactions/command_interaction.dart b/lib/src/api/interactions/command_interaction.dart index 6918d771..1736033d 100644 --- a/lib/src/api/interactions/command_interaction.dart +++ b/lib/src/api/interactions/command_interaction.dart @@ -1,4 +1,6 @@ +import 'package:http/http.dart'; import 'package:mineral/api.dart'; +import 'package:mineral/core.dart'; import 'package:mineral/src/api/interactions/interaction.dart'; class CommandInteraction extends Interaction { @@ -48,9 +50,40 @@ class CommandInteraction extends Interaction { return data[optionName]['value']; } + Future reply ({ String? content, List? embeds, List? components, bool? tts, bool? private }) async { + Http http = ioc.singleton(Service.http); + + List embedList = []; + if (embeds != null) { + for (MessageEmbed element in embeds) { + embedList.add(element.toJson()); + } + } + + List componentList = []; + if (components != null) { + for (Row element in components) { + componentList.add(element.toJson()); + } + } + + Response response = await http.post(url: "/interactions/$id/$token/callback", payload: { + 'type': InteractionCallbackType.channelMessageWithSource.value, + 'data': { + 'tts': tts ?? false, + 'content': content, + 'embeds': embeds != null ? embedList : [], + 'components': components != null ? componentList : [], + 'flags': private != null && private == true ? 1 << 6 : null, + } + }); + + print(response.body); + } + factory CommandInteraction.from({ required User user, required dynamic payload }) { return CommandInteraction( - id: payload['data']['id'], + id: payload['id'], applicationId: payload['application_id'], type: InteractionType.values.firstWhere((type) => type.value == payload['type']), identifier: payload['data']['name'], diff --git a/lib/src/api/interactions/interaction.dart b/lib/src/api/interactions/interaction.dart index 3ca26987..8bd63164 100644 --- a/lib/src/api/interactions/interaction.dart +++ b/lib/src/api/interactions/interaction.dart @@ -1,5 +1,18 @@ import 'package:mineral/api.dart'; +enum InteractionCallbackType { + pong(1), + channelMessageWithSource(4), + deferredChannelMessageWithSource(5), + deferredUpdateMessage(6), + updateMessage(7), + applicationCommandAutocompleteResult(8), + modal(9); + + final int value; + const InteractionCallbackType(this.value); +} + class Interaction { Snowflake applicationId; int version; diff --git a/lib/src/api/message_embed.dart b/lib/src/api/message_embed.dart index 9db60e68..7199110f 100644 --- a/lib/src/api/message_embed.dart +++ b/lib/src/api/message_embed.dart @@ -51,7 +51,7 @@ class Field { String value; bool? inline; - Field({ required this.name, required this.value, required this.inline }); + Field({ required this.name, required this.value, this.inline }); Object toJson () => { 'name': name, diff --git a/lib/src/api/user.dart b/lib/src/api/user.dart index 24d9c87f..d791798a 100644 --- a/lib/src/api/user.dart +++ b/lib/src/api/user.dart @@ -1,4 +1,5 @@ import 'package:mineral/api.dart'; +import 'package:mineral/src/constants.dart'; class User { Snowflake id; @@ -20,6 +21,13 @@ class User { required this.avatar, }); + String getDisplayAvatarUrl () { + return "${Constants.cdnUrl}/avatars/$id/$avatar"; + } + + @override + String toString () => "<@$id>"; + factory User.from(dynamic payload) { return User( id: payload['id'], diff --git a/lib/src/internal/entities/command_manager.dart b/lib/src/internal/entities/command_manager.dart index 2522a96f..a7e3b4e9 100644 --- a/lib/src/internal/entities/command_manager.dart +++ b/lib/src/internal/entities/command_manager.dart @@ -75,7 +75,6 @@ class CommandManager { 'commandClass': classCommand, }); } - } if (reflectee is Option) { diff --git a/lib/src/internal/websockets/packets/interaction_create.dart b/lib/src/internal/websockets/packets/interaction_create.dart index 11d00756..501a15ef 100644 --- a/lib/src/internal/websockets/packets/interaction_create.dart +++ b/lib/src/internal/websockets/packets/interaction_create.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:mirrors'; import 'package:mineral/api.dart'; @@ -16,6 +17,7 @@ class InteractionCreate implements WebsocketPacket { MineralClient client = ioc.singleton(Service.client); dynamic payload = websocketResponse.payload; + print(jsonEncode(payload)); Guild? guild = client.guilds.cache.get(payload['guild_id']); GuildMember? member = guild?.members.cache.get(payload['member']['user']['id']); @@ -38,7 +40,9 @@ class InteractionCreate implements WebsocketPacket { } } - walk(payload['data']['options']); + if (payload['data']['options'] != null) { + walk(payload['data']['options']); + } dynamic handle = manager.handlers[identifier]; reflect(handle['commandClass']).invoke(handle['symbol'], [commandInteraction]);