🚀 Overhaul to files, made easier to use! Missing documentation for all public APIS.

This commit is contained in:
HarryET 2021-02-09 11:20:17 +00:00 committed by Szymon Uglis
parent 1d6f937f96
commit 23dfa06ac0
No known key found for this signature in database
GPG key ID: 112376C5BEE91FE2
18 changed files with 305 additions and 333 deletions

3
.gitignore vendored
View file

@ -32,4 +32,5 @@ pubspec.lock
private-*.dart
test-*.dart
[Rr]pc*
**/doc/api/**
**/doc/api/**
old.*

View file

@ -1,26 +1,28 @@
library nyxx_interactions;
import 'dart:async';
import "dart:async";
import "package:logging/logging.dart";
import "package:nyxx/nyxx.dart";
// Root
part "src/Interactions.dart";
// Events
part "src/events/InteractionEvent.dart";
// Internal
part "src/internal/_EventController.dart";
// Models
part "src/models/CommandInteractionData.dart";
part "src/models/CommandInteractionOption.dart";
part "src/models/Interaction.dart";
part "src/models/InteractionResponse/IInteractionResponse.dart";
part "src/models/SlashArg.dart";
part "src/models/SlashCommand.dart";
part "src/models/SlashArgChoice.dart";
part 'src/Models/SlashCommand.dart';
part "src/Models/Interaction.dart";
part "src/Models/InteractionOption.dart";
// Command Args
part 'src/Models/CommandArgs/ArgChoice.dart';
part 'src/Models/CommandArgs/CommandArg.dart';
// Internal
part 'src/Internal/_EventController.dart';
// Events
part "src/Events/InteractionEvent.dart";
// Exceptions
part "src/models/exceptions/AlreadyResponded.dart";
part "src/models/exceptions/InteractionExpired.dart";
part "src/models/exceptions/SlashArgMisconfiguration.dart";
part "src/models/exceptions/ArgLength.dart";
part "src/Exceptions/InteractionExpired.dart";
part "src/Exceptions/AlreadyResponded.dart";

View file

@ -9,44 +9,44 @@ class InteractionEvent {
InteractionEvent._new(Nyxx client, Map<String, dynamic> rawJson) {
this._client = client;
interaction = Interaction._new(client, rawJson);
if(interaction.type == 1) {
this._pong();
}
}
/// Should be sent when you receive a ping from interactions. Used to acknowledge a ping.
Future Pong() {
Future _pong() {
if (DateTime.now()
.isBefore(this.recievedAt.add(const Duration(minutes: 15)))) {
String url;
if (hasResponded) {
url =
"/interactions/${this.interaction.id.toString()}/${this.interaction.token}";
} else {
url =
"/interactions/${this.interaction.id.toString()}/${this.interaction.token}/callback";
}
final response = this._client.httpEndpoints.sendRawRequest(
url,
"POST",
body: {
"type": 1,
"data": null,
},
);
if (response is HttpResponseError) {
return Future.error(response);
}
if (!hasResponded) {
hasResponded = true;
final response = this._client.httpEndpoints.sendRawRequest(
"/interactions/${this.interaction.id.toString()}/${this.interaction.token}/callback",
"POST",
body: {
"type": 1,
"data": null,
},
);
if (response is HttpResponseError) {
return Future.error(response);
}
if (!hasResponded) {
hasResponded = true;
}
return Future.value(null);
} else {
return Future.error(InteractionExpired());
}
return Future.value(null);
} else {
return Future.error(InteractionExpired());
}
}
/// Used to acknowledge a Interaction but not send any response yet. Once this is sent you can then only send ChannelMessages with or without source.
Future Acknowledge({
Future acknowledge({
bool showSource = false,
}) {
if (DateTime.now()
@ -80,32 +80,31 @@ class InteractionEvent {
}
}
Future Reply({
Future reply({
dynamic content,
EmbedBuilder? embed,
bool? tts,
AllowedMentions? allowedMentions,
bool showSource = false,
}) {
}) async {
if (DateTime.now()
.isBefore(this.recievedAt.add(const Duration(minutes: 15)))) {
String url;
if (hasResponded) {
url =
"/interactions/${this.interaction.id.toString()}/${this.interaction.token}";
"/webhooks/${this.interaction.id.toString()}/${this.interaction.token}";
} else {
url =
"/interactions/${this.interaction.id.toString()}/${this.interaction.token}/callback";
"/interactions/${this.interaction.id.toString()}/${this.interaction.token}/callback";
}
final response = this._client.httpEndpoints.sendRawRequest(
final response = await this._client.httpEndpoints.sendRawRequest(
url,
"POST",
body: {
"type": showSource ? 4 : 3,
"data": {
"content": content,
"embeds":
embed != null ? BuilderUtility.buildRawEmbed(embed) : null,
"embeds": embed != null ? [ BuilderUtility.buildRawEmbed(embed) ] : null,
"allowed_mentions": allowedMentions != null
? BuilderUtility.buildRawAllowedMentions(allowedMentions)
: null,
@ -118,6 +117,8 @@ class InteractionEvent {
return Future.error(response);
}
print((response as HttpResponseSuccess).body);
if (!hasResponded) {
hasResponded = true;
}

View file

@ -9,4 +9,4 @@ class AlreadyResponded implements Error {
@override
StackTrace? get stackTrace => StackTrace.empty;
}
}

View file

@ -9,4 +9,4 @@ class InteractionExpired implements Error {
@override
StackTrace? get stackTrace => StackTrace.empty;
}
}

View file

@ -4,19 +4,25 @@ part of nyxx_interactions;
class Interactions {
late final Nyxx _client;
final Logger _logger = Logger("Interactions");
final List<SlashCommand> _commands = List.empty(growable: true);
late final _EventController _events;
/// Emitted when a a slash command is sent.
late Stream<InteractionEvent> onSlashCommand;
/// Emitted when a a slash command is sent.
late Stream<SlashCommand> onSlashCommandCreated;
///
Interactions(Nyxx client) {
client.options.dispatchRawShardEvent = true;
this._client = client;
_events = _EventController(this);
_client.options.dispatchRawShardEvent = true;
_logger.info("Interactions ready");
client.onReady.listen((event) {
client.shardManager.rawEvent.listen((event) {
print(event);
print(event.rawData);
if (event.rawData["op"] as int == 0) {
if (event.rawData["t"] as String == "INTERACTION_CREATE") {
_events.onSlashCommand.add(
@ -26,42 +32,58 @@ class Interactions {
}
}
});
_logger.info("Interactions ready");
});
}
Future<SlashCommand> registerSlashGlobal(
String name, String description, List<SlashArg> args) async {
final command =
SlashCommand._new(this._client, name, args, description, null);
final response = await this._client.httpEndpoints.sendRawRequest(
"/applications/${this._client.app.id.toString()}/commands",
"POST",
body: command._build(),
);
SlashCommand createCommand(String name, String description, List<CommandArg> args, {String? guild})
=> SlashCommand._new(_client, name, description, args, guild: guild != null ? CacheUtility.createCacheableGuild(_client, Snowflake(guild)) : null);
if (response is HttpResponseError) {
return Future.error(response);
}
return Future.value(command);
/// Registers a single command.
///
/// @param command A [SlashCommand] to register.
void registerCommand(SlashCommand command) {
_commands.add(command);
}
Future<SlashCommand> registerSlashGuild(String name, String description,
Snowflake guildId, List<SlashArg> args) async {
final command =
SlashCommand._new(this._client, name, args, description, guildId);
command._build();
final response = await this._client.httpEndpoints.sendRawRequest(
"/applications/${this._client.app.id.toString()}/guilds/${guildId.toString()}/commands",
"POST",
body: command._build(),
);
if (response is HttpResponseError) {
return Future.error(response);
/// Registers multiple commands at one.
///
/// This wraps around [Interactions.registerCommand()] running [Interactions.registerCommand] for each command in the list.
/// @param commands A list of [SlashCommand]s to register.
void registerCommands(List<SlashCommand> commands) {
for(var i = 0; i < commands.length; i++) {
this.registerCommand(commands[i]);
}
return Future.value(command);
}
List<SlashCommand> getCommands({bool registeredOnly = false}) {
if(!registeredOnly) { return _commands; }
final registeredCommands = List<SlashCommand>.empty(growable: true);
for(var i = 0; i < _commands.length; i++) {
final el = _commands[i];
if(el.isRegistered) {
registeredCommands.add(el);
}
}
return registeredCommands;
}
Future<void> sync() async {
var success = 0;
var failed = 0;
for(var i = 0; i < _commands.length; i++) {
final el = _commands[i];
if(!el.isRegistered) {
final command = await el._register();
if(command != null) {
_commands[i] = command;
this._events.onSlashCommandCreated.add(_commands[i]);
success++;
} else {
failed++;
}
}
}
_logger.info("Successfully registered $success ${success > 1 ? "commands" : "command"}. Failed registering $failed ${failed > 1 ? "commands" : "command"}.");
}
}

View file

@ -4,13 +4,20 @@ class _EventController implements Disposable {
/// Emitted when a a slash command is sent.
late final StreamController<InteractionEvent> onSlashCommand;
/// Emitted when a a slash command is sent.
late final StreamController<SlashCommand> onSlashCommandCreated;
_EventController(Interactions _client) {
this.onSlashCommand = StreamController.broadcast();
_client.onSlashCommand = this.onSlashCommand.stream;
this.onSlashCommandCreated = StreamController.broadcast();
_client.onSlashCommandCreated = this.onSlashCommandCreated.stream;
}
@override
Future<void> dispose() async {
await this.onSlashCommand.close();
await this.onSlashCommandCreated.close();
}
}
}

View file

@ -1,31 +1,31 @@
part of nyxx_interactions;
class SlashArgChoice {
/// This options name.
late final String name;
/// This options int value. If there is an int value there can't be a [SlashArgChoice.stringValue]
late final int? intValue;
/// This options string value. If there is an string value there can't be a [SlashArgChoice.intValue]
late final String? stringValue;
/// A Choice for the user to input in int & string args. You can only have an int or string option.
SlashArgChoice(this.name, {this.intValue, this.stringValue}) {
if(this.intValue != null && this.stringValue != null) {
throw SlashArgMisconfiguration._new("stringValue & intValue");
}
if(this.intValue == null && this.stringValue == null) {
throw "MissingArgError: All SlashArgChoice need an int or string value";
}
if(this.name.length > 100) {
throw ArgLength._new("SlashArgChoice.name", "1", "100");
}
}
Map<String, dynamic> _build() => {
"name": this.name,
"value": this.stringValue ?? this.intValue
};
}
part of nyxx_interactions;
class ArgChoice {
/// This options name.
late final String name;
/// This options int value. If there is an int value there can't be a [SlashArgChoice.stringValue]
late final int? intValue;
/// This options string value. If there is an string value there can't be a [SlashArgChoice.intValue]
late final String? stringValue;
/// A Choice for the user to input in int & string args. You can only have an int or string option.
ArgChoice(this.name, dynamic value) {
if(value is String) {
this.stringValue = value;
}
if(value is int) {
this.intValue = value;
}
if(value is! int && value is! String) {
throw 'InvalidParamTypeError: Please send a string if its a string arg or an int if its an int arg.';
}
}
Map<String, dynamic> _build() => {
"name": this.name,
"value": this.stringValue ?? this.intValue
};
}

View file

@ -1,99 +1,75 @@
part of nyxx_interactions;
/// The type that a user should input for a [SlashArg]
enum SlashArgType {
SUB_COMMAND,
SUB_COMMAND_GROUP,
STRING,
INTEGER,
BOOLEAN,
USER,
CHANNEL,
ROLE,
}
/// An argument for a [SlashCommand].
class SlashArg {
/// The type of arg that will be later changed to an INT value, their values can be seen in the table below:
/// | Name | Value |
/// |-------------------|-------|
/// | SUB_COMMAND | 1 |
/// | SUB_COMMAND_GROUP | 2 |
/// | STRING | 3 |
/// | INTEGER | 4 |
/// | BOOLEAN | 5 |
/// | USER | 6 |
/// | CHANNEL | 7 |
/// | ROLE | 8 |
late final SlashArgType type;
/// The name of your argument / sub-group.
late final String name;
/// The description of your argument / sub-group.
late final String description;
/// If this should be the fist required option the user picks
late final bool defaultArg;
/// If this argument is required
late final bool required;
/// Choices for [SlashArgType.STRING] and [SlashArgType.INTEGER] types for the user to pick from
late final List<SlashArgChoice>? choices;
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters
late final List<SlashArg>? options;
/// Used to create an argument for a [SlashCommand]. Thease are used in [Interactions.registerSlashGlobal] and [Interactions.registerSlashGuild]
SlashArg(this.type, this.name, this.description,
{this.defaultArg = false,
this.required = false,
this.choices,
this.options}) {
if (this.options != null &&
(this.type != SlashArgType.SUB_COMMAND ||
this.type != SlashArgType.SUB_COMMAND_GROUP)) {
throw SlashArgMisconfiguration._new("Options & Type");
}
if (this.choices != null &&
(this.type != SlashArgType.STRING ||
this.type != SlashArgType.INTEGER)) {
throw SlashArgMisconfiguration._new("Choices & Type");
}
if (!this.required && this.defaultArg) {
throw SlashArgMisconfiguration._new("Required & Default Arg");
}
if (this.name.length > 32) {
throw ArgLength._new("SlashArg.name", "1", "32");
}
if (this.description.length > 100) {
throw ArgLength._new("SlashArg.description", "1", "100");
}
}
Map<String, dynamic> _build() {
final subOptions = this.options != null
? List<Map<String, dynamic>>.generate(
this.options!.length, (i) => this.options![i]._build())
: null;
final rawChoices = this.choices != null
? List<Map<String, dynamic>>.generate(
this.choices!.length, (i) => this.choices![i]._build())
: null;
return {
"type": (this.type.index) + 1,
"name": this.name,
"description": this.description,
"default": this.defaultArg,
"required": this.required,
"choices": rawChoices,
"options": subOptions
};
}
}
part of nyxx_interactions;
/// The type that a user should input for a [SlashArg]
enum CommandArgType {
SUB_COMMAND,
SUB_COMMAND_GROUP,
STRING,
INTEGER,
BOOLEAN,
USER,
CHANNEL,
ROLE,
}
/// An argument for a [SlashCommand].
class CommandArg {
/// The type of arg that will be later changed to an INT value, their values can be seen in the table below:
/// | Name | Value |
/// |-------------------|-------|
/// | SUB_COMMAND | 1 |
/// | SUB_COMMAND_GROUP | 2 |
/// | STRING | 3 |
/// | INTEGER | 4 |
/// | BOOLEAN | 5 |
/// | USER | 6 |
/// | CHANNEL | 7 |
/// | ROLE | 8 |
late final CommandArgType type;
/// The name of your argument / sub-group.
late final String name;
/// The description of your argument / sub-group.
late final String description;
/// If this should be the fist required option the user picks
late final bool defaultArg;
/// If this argument is required
late final bool required;
/// Choices for [SlashArgType.STRING] and [SlashArgType.INTEGER] types for the user to pick from
late final List<ArgChoice>? choices;
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters
late final List<CommandArg>? options;
/// Used to create an argument for a [SlashCommand]. Thease are used in [Interactions.registerSlashGlobal] and [Interactions.registerSlashGuild]
CommandArg(this.type, this.name, this.description,
{this.defaultArg = false,
this.required = false,
this.choices,
this.options});
Map<String, dynamic> _build() {
final subOptions = this.options != null
? List<Map<String, dynamic>>.generate(
this.options!.length, (i) => this.options![i]._build())
: null;
final rawChoices = this.choices != null
? List<Map<String, dynamic>>.generate(
this.choices!.length, (i) => this.choices![i]._build())
: null;
return {
"type": (this.type.index) + 1,
"name": this.name,
"description": this.description,
"default": this.defaultArg,
"required": this.required,
"choices": rawChoices,
"options": subOptions
};
}
}

View file

@ -6,8 +6,6 @@ class Interaction extends SnowflakeEntity implements Disposable {
late final int type;
late final CommandInteractionData? data;
late final Cacheable<Snowflake, Guild> guild;
late final Cacheable<Snowflake, TextChannel> channel;
@ -18,26 +16,16 @@ class Interaction extends SnowflakeEntity implements Disposable {
late final int version;
CommandInteractionData? _generateData(Map<String, dynamic> raw) {
return raw["data"] != null
? CommandInteractionData._new(
Snowflake(raw["data"]["id"]),
raw["data"]["name"] as String,
null, //TODO Add Tree Implimentation to show options
)
: null;
}
late final String name; // TODO
late final Map<String, InteractionOption> args; // TODO
Interaction._new(
this.client,
Map<String, dynamic> raw,
) : super(Snowflake(raw["id"])) {
this.client,
Map<String, dynamic> raw,
) : super(Snowflake(raw["id"])) {
this.type = raw["type"] as int;
this.data = _generateData(
raw,
);
this.guild = CacheUtility.createCacheableGuild(
client,
Snowflake(
@ -63,8 +51,29 @@ class Interaction extends SnowflakeEntity implements Disposable {
this.token = raw["token"] as String;
this.version = raw["version"] as int;
this.name = raw["data"]["name"] as String;
this.args = _generateArgs(raw["data"] as Map<String, dynamic>);
}
Map<String, InteractionOption> _generateArgs(Map<String, dynamic> rawData) {
final args = <String, InteractionOption>{};
if(rawData["options"] != null) {
final l = rawData["options"] as List;
for(var i = 0; i < l.length; i++) {
final el = l[i];
args[el["name"] as String] = InteractionOption._new(
el["value"] as dynamic,
(el["options"] ?? List<dynamic>.empty() ) as List,
);
}
}
return args;
}
@override
Future<void> dispose() => Future.value(null);
}
}

View file

@ -0,0 +1,15 @@
part of nyxx_interactions;
class InteractionOption {
final dynamic? value;
final Map<String, InteractionOption> args = <String, InteractionOption>{};
InteractionOption._new(this.value, List rawOptions) {
for(var i = 0; i < rawOptions.length; i++) {
final el = rawOptions[i];
this.args[el["name"] as String] = InteractionOption._new(
el["value"] as dynamic,
(el["options"] ?? List<dynamic>.empty() ) as List,
);
}
}
}

View file

@ -0,0 +1,39 @@
part of nyxx_interactions;
class SlashCommand {
late final String name;
late final String description;
late final Cacheable<Snowflake, Guild>? guild;
late final List<CommandArg> args;
late final bool isGlobal;
late bool isRegistered = false;
late final Nyxx _client;
SlashCommand._new(this._client, this.name, this.description, this.args, {this.guild}) {
this.isGlobal = guild == null;
}
Future<SlashCommand> _register() async {
final options = List<Map<String, dynamic>>.generate(
this.args.length, (i) => this.args[i]._build());
final response = await this._client.httpEndpoints.sendRawRequest(
"/applications/${this._client.app.id.toString()}/commands",
"POST",
body: {
"name": this.name,
"description": this.description,
"options": options.isNotEmpty ? options : null
},
);
if (response is HttpResponseError) {
return Future.error(response);
}
this.isRegistered = true;
return Future.value(this);
}
}

View file

@ -1,11 +0,0 @@
part of nyxx_interactions;
class CommandInteractionData {
final Snowflake id;
final String name;
final List<CommandInteractionOption>? options;
CommandInteractionData._new(this.id, this.name, this.options);
}

View file

@ -1,8 +0,0 @@
part of nyxx_interactions;
class CommandInteractionOption {
final String name;
final int? value;
final List<CommandInteractionOption>? options;
CommandInteractionOption._new(this.name, this.value, this.options);
}

View file

@ -1,3 +0,0 @@
part of nyxx_interactions;
abstract class IInteractionResponse implements Disposable {}

View file

@ -1,38 +0,0 @@
part of nyxx_interactions;
/// This class contains data about a slash command and is returned after registering one.
class SlashCommand {
/// The name of your command.
late final String name;
/// The description of your command.
late final String description;
/// The args registered for a command
late final List<SlashArg> args;
/// If you command is registered globally
late final bool isGlobal;
/// When you command is only for one guild the [SlashCommand.guild] will contain a [Cacheable] for
late final Cacheable<Snowflake, Guild>? guild;
SlashCommand._new(
Nyxx client, this.name, this.args, this.description, Snowflake? guildId) {
this.isGlobal = guildId == null;
this.guild = guildId != null
? CacheUtility.createCacheableGuild(client, Snowflake(guildId))
: null;
}
Map<String, dynamic> _build() {
final options = List<Map<String, dynamic>>.generate(
this.args.length, (i) => this.args[i]._build());
return {
"name": this.name,
"description": this.description,
"options": options.isNotEmpty ? options : null
};
}
}

View file

@ -1,23 +0,0 @@
part of nyxx_interactions;
/// Thrown when your slash are is configured in a way that is not possible
class ArgLength implements Error {
/// The max length for the incorrect length variable/param.
late final String maxLength;
/// The min length for the incorrect length variable/param.
late final String minLength;
/// The name of the incorrect length variable/param.
late final String name;
ArgLength._new(this.name, this.minLength, this.maxLength);
/// Returns a string representation of this object.
@override
String toString() =>
"ArgLengthError: $name isn't the correct length. It must be larger than $minLength and smaller than $maxLength ($minLength-$maxLength)";
@override
StackTrace? get stackTrace => StackTrace.empty;
}

View file

@ -1,17 +0,0 @@
part of nyxx_interactions;
/// Thrown when your slash are is configured in a way that is not possible
class SlashArgMisconfiguration implements Error {
/// The params that are incorrect.
late final String params;
SlashArgMisconfiguration._new(this.params);
/// Returns a string representation of this object.
@override
String toString() =>
"SlashArgMisconfigurationError: ${this.params} are mismatched. Please refer to the nyxx.interaction docs and the discord developer docs to make sure that all your slash args arguments are compatible together.";
@override
StackTrace? get stackTrace => StackTrace.empty;
}