diff --git a/lib/api.dart b/lib/api.dart index 1e4be12f..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; @@ -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/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 new file mode 100644 index 00000000..1736033d --- /dev/null +++ b/lib/src/api/interactions/command_interaction.dart @@ -0,0 +1,95 @@ +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 { + 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']; + } + + 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['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..8bd63164 --- /dev/null +++ b/lib/src/api/interactions/interaction.dart @@ -0,0 +1,25 @@ +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; + 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/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/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..a7e3b4e9 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,24 @@ 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 +103,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..501a15ef --- /dev/null +++ b/lib/src/internal/websockets/packets/interaction_create.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +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; + print(jsonEncode(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); + } + } + } + + if (payload['data']['options'] != null) { + 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) {