From 05f2adccb0e20fc762e1d3ef45e4250b5c27f83b Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 3 Nov 2021 22:07:29 +0100 Subject: [PATCH 01/25] First commit on 3.0.0 rewrite --- .gitignore | 39 +++ .packages | 60 +++- Makefile | 40 +++ example/example.dart | 4 +- example/per_guild_prefix.dart | 12 +- lib/commander.dart | 14 - lib/nyxx_commander.dart | 6 + lib/src/CommandContext.dart | 196 ------------ lib/src/CommandHandler.dart | 118 ------- lib/src/command_context.dart | 338 +++++++++++++++++++++ lib/src/command_handler.dart | 202 ++++++++++++ lib/src/{Commander.dart => commander.dart} | 74 ++--- lib/src/utils.dart | 38 ++- pubspec.lock | 96 ------ pubspec.yaml | 15 +- test/commander-test.dart | 158 +++++----- test/integration/.gitkeep | 0 test/private-test.dart | 28 -- test/unit/.gitkeep | 0 19 files changed, 835 insertions(+), 603 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile delete mode 100644 lib/commander.dart create mode 100644 lib/nyxx_commander.dart delete mode 100644 lib/src/CommandContext.dart delete mode 100644 lib/src/CommandHandler.dart create mode 100644 lib/src/command_context.dart create mode 100644 lib/src/command_handler.dart rename lib/src/{Commander.dart => commander.dart} (67%) delete mode 100644 pubspec.lock create mode 100644 test/integration/.gitkeep delete mode 100644 test/private-test.dart create mode 100644 test/unit/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..295e586 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +local/ +.atom/ +.vscode/ +index.html +docs/ +.buildlog +.packages +.project +.pub +**/build +**/packages +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json +doc/api/ +pubspec.lock +*.iml +.idea +*~ +*# +.#* +.dart_tool/ +/README.html +/log.txt +/nyxx.wiki/ +/test/private.dart +/publish_docs.sh +/test/mirrors.dart +/private +private-*.dart +test-*.dart +[Rr]pc* +**/doc/api/** +**/coverage/** +coverage.json +lcov.info +pubspec.lock diff --git a/.packages b/.packages index 8df2fd9..0ffcf2e 100644 --- a/.packages +++ b/.packages @@ -3,18 +3,74 @@ # # For more info see: https://dart.dev/go/dot-packages-deprecation # -# Generated by pub on 2021-10-04 16:58:39.287303. +# Generated by pub on 2021-11-03 22:04:20.131154. +_fe_analyzer_shared:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-30.0.0/lib/ +analyzer:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/analyzer-2.7.0/lib/ +args:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ async:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ +boolean_selector:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/ +build:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build-2.1.1/lib/ +build_config:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_config-1.0.0/lib/ +build_daemon:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_daemon-3.0.1/lib/ +build_resolvers:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_resolvers-2.0.4/lib/ +build_runner:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_runner-2.1.4/lib/ +build_runner_core:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_runner_core-7.2.2/lib/ +built_collection:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/built_collection-5.1.1/lib/ +built_value:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/built_value-8.1.3/lib/ charcode:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ +checked_yaml:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/ +cli_util:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/cli_util-0.3.5/lib/ +code_builder:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/code_builder-4.1.0/lib/ collection:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0/lib/ +convert:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ +coverage:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/coverage-1.0.3/lib/ +crypto:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ +dart_style:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.0/lib/ +file:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ +fixnum:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/fixnum-1.0.0/lib/ +frontend_server_client:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/frontend_server_client-2.1.2/lib/ +glob:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ +graphs:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/graphs-2.1.0/lib/ http:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/http-0.13.4/lib/ +http_multi_server:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/http_multi_server-3.0.1/lib/ http_parser:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/http_parser-4.0.0/lib/ +io:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/io-1.0.3/lib/ +js:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/js-0.6.3/lib/ +json_annotation:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.3.0/lib/ +lints:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/lints-1.0.1/lib/ logging:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2/lib/ +matcher:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/ meta:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ -nyxx:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/nyxx-2.0.2/lib/ +mime:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/mime-1.0.1/lib/ +mockito:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/mockito-5.0.16/lib/ +node_preamble:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/node_preamble-2.0.1/lib/ +nyxx:file:///home/lusha/.pub-cache/git/nyxx-6a6947ef87bc720195c1c7456ee6022dcf163f98/lib/ +package_config:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ path:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/ +pool:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/pool-1.5.0/lib/ +pub_semver:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.0/lib/ +pubspec_parse:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.1.0/lib/ +shelf:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf-1.2.0/lib/ +shelf_packages_handler:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf_packages_handler-3.0.0/lib/ +shelf_static:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf_static-1.1.0/lib/ +shelf_web_socket:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf_web_socket-1.0.1/lib/ +source_gen:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_gen-1.1.1/lib/ +source_map_stack_trace:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_map_stack_trace-2.1.0/lib/ +source_maps:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_maps-0.10.10/lib/ source_span:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib/ +stack_trace:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/ +stream_channel:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/ +stream_transform:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/stream_transform-2.0.0/lib/ string_scanner:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ term_glyph:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ +test:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test-1.19.3/lib/ +test_api:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.7/lib/ +test_core:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test_core-0.4.8/lib/ +timing:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/timing-1.0.0/lib/ typed_data:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ +vm_service:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/vm_service-7.3.0/lib/ +watcher:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ +web_socket_channel:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/web_socket_channel-2.1.0/lib/ +webkit_inspection_protocol:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/webkit_inspection_protocol-1.0.0/lib/ +yaml:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ nyxx_commander:lib/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f7fea2 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.PHONY: help +help: + @fgrep -h "##" $(MAKEFILE_LIST) | sed -e 's/\(\:.*\#\#\)/\:\ /' | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' + +.PHONY: app-check ## Run basic format checks and then generate code coverage +app-check: format-check generate-coverage + +.PHONY: format-check ## Check basic format +format-check: format analyze + +.PHONY: generate-coverage +generate-coverage: integration-tests unit-tests coverage-format coverage-gen-html ## Run all test and generate html code coverage + +.PHONY: integration-tests +integration-tests: ## Run integration tests with coverage + (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration/integration.dart; exit 0) + +.PHONY: unit-tests +unit-tests: ## Run unit tests with coverage + (timeout 10s dart run test --coverage="coverage" --timeout=none test/unit; exit 0) + +.PHONY: coverage-format +coverage-format: ## Format dart coverage output to lcov + dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib + +.PHONY: coverage-gen-html +coverage-gen-html: ## Generate html coverage from lcov data + genhtml coverage/coverage.lcov -o coverage/coverage_gen + +.PHONY: format +format: ## Run dart format + dart format --set-exit-if-changed -l 160 ./lib + +.PHONY: format-apply +format-apply: ## Run dart format + dart format --fix -l 160 ./lib + +.PHONY: analyze +analyze: ## Run dart analyze + dart analyze diff --git a/example/example.dart b/example/example.dart index 862ba03..e76bd37 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,9 +1,9 @@ import "package:nyxx/nyxx.dart"; -import "package:nyxx_commander/commander.dart"; +import "package:nyxx_commander/nyxx_commander.dart"; void main() { // Start bot - final bot = Nyxx("TOKEN", GatewayIntents.allUnprivileged); + final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); // Start commander with prefix `!` Commander(bot, prefix: "!") diff --git a/example/per_guild_prefix.dart b/example/per_guild_prefix.dart index d105cda..1668791 100644 --- a/example/per_guild_prefix.dart +++ b/example/per_guild_prefix.dart @@ -1,7 +1,7 @@ import "dart:async"; import "package:nyxx/nyxx.dart"; -import "package:nyxx_commander/commander.dart"; +import "package:nyxx_commander/nyxx_commander.dart"; // Temporary storage for prefixes per guild // Note that this implementation is just proof of concept @@ -10,21 +10,19 @@ final prefixes = {}; const defaultPrefix = "!"; -FutureOr prefixHandler(Message message) { +FutureOr prefixHandler(IMessage message) { // Check if we are in DMs, if true then return default prefix - if (message is DMMessage) { + if (message.guild == null) { return defaultPrefix; } - final guildMessage = message as GuildMessage; // case message to GuildMessage since we are sure we are not in DMs - final prefixForGuild = prefixes[guildMessage.guild.id]; // Get prefix for guild id. Will return null if not present - + final prefixForGuild = prefixes[message.guild!.id]; // Get prefix for guild id. Will return null if not present return prefixForGuild ?? defaultPrefix; // return prefix for guild if not null or default prefix otherwise } void main() { // Start bot - final bot = Nyxx("TOKEN", GatewayIntents.allUnprivileged); + final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); // Start commander with prefix `!` Commander(bot, prefixHandler: prefixHandler) // prefixHandler will handle deciding which guild can use which prefix diff --git a/lib/commander.dart b/lib/commander.dart deleted file mode 100644 index 57476eb..0000000 --- a/lib/commander.dart +++ /dev/null @@ -1,14 +0,0 @@ -library nyxx_commander; - -import "dart:async"; -import "dart:io"; - -import "package:logging/logging.dart"; - -import "package:nyxx/nyxx.dart"; - -part "src/Commander.dart"; -part "src/CommandContext.dart"; -part "src/CommandHandler.dart"; - -part "src/utils.dart"; diff --git a/lib/nyxx_commander.dart b/lib/nyxx_commander.dart new file mode 100644 index 0000000..a77bd86 --- /dev/null +++ b/lib/nyxx_commander.dart @@ -0,0 +1,6 @@ +library nyxx_commander; + +export 'src/command_context.dart' show ICommandContext; +export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler; +export 'src/commander.dart' show Commander; +export 'src/utils.dart' show AfterHandlerFunction, CommandHandlerFunction, LoggerHandlerFunction,PassHandlerFunction, PrefixHandlerFunction, CommandExecutionError; diff --git a/lib/src/CommandContext.dart b/lib/src/CommandContext.dart deleted file mode 100644 index c4ecd4f..0000000 --- a/lib/src/CommandContext.dart +++ /dev/null @@ -1,196 +0,0 @@ -part of nyxx_commander; - -/// Helper class which describes context in which command is executed -class CommandContext { - /// Channel from where message come from - final TextChannel channel; - - /// Author of message - final IMessageAuthor author; - - /// Message that was sent - final Message message; - - /// Guild in which message was sent - final Guild? guild; - - /// Returns author as guild member - Member? get member => this.message is GuildMessage - ? (message as GuildMessage).member - : null; - - /// Reference to client - Nyxx get client => channel.client as Nyxx; - - /// Shard on which message was sent - int get shardId => this.guild != null ? this.guild!.shard.id : 0; - - /// Substring by which command was matched - final String commandMatcher; - - CommandContext._new(this.channel, this.author, this.guild, this.message, this.commandMatcher); - - static final _argumentsRegex = RegExp('([^"\' ]+)|["\']([^"]*)["\']'); - static final _quotedTextRegex = RegExp('["\']([^"]*)["\']'); - static final _codeBlocksRegex = RegExp(r"```(\w+)?(\s)?(((.+)(\s)?)+)```"); - - /// Creates inline reply for message - Future reply(MessageBuilder builder, {bool mention = false, bool reply = false }) async { - if (mention) { - if (builder.allowedMentions != null) { - builder.allowedMentions!.allow(reply: true); - } else { - builder.allowedMentions = AllowedMentions()..allow(reply: true); - } - } - - if (reply) { - builder.replyBuilder = ReplyBuilder.fromMessage(this.message); - } - - return channel.sendMessage(builder); - } - - /// Reply to message. It allows to send regular message, Embed or both. - /// - /// ``` - /// Future getAv(CommandContext context) async { - /// await context.reply(content: context.user.avatarURL()); - /// } - /// ``` - Future sendMessage(MessageBuilder builder) => channel.sendMessage(builder); - - /// Reply to messages, then delete it when [duration] expires. - /// - /// ``` - /// Future getAv(CommandContext context) async { - /// await context.replyTemp(content: user.avatarURL()); - /// } - /// ``` - Future sendMessageTemp(Duration duration, MessageBuilder builder) => channel - .sendMessage(builder) - .then((msg) { - Timer(duration, () => msg.delete()); - return msg; - }); - - /// Replies to message after delay specified with [duration] - /// ``` - /// Future getAv(CommandContext context async { - /// await context.replyDelayed(Duration(seconds: 2), content: user.avatarURL()); - /// } - /// ``` - Future sendMessageDelayed(Duration duration, MessageBuilder builder) => - Future.delayed(duration, () => channel.sendMessage(builder)); - - /// Awaits for emoji under given [msg] - Future awaitEmoji(Message msg) async => - (await this.client.onMessageReactionAdded.where((event) => event.message == msg).first).emoji; - - /// Collects emojis within given [duration]. Returns empty map if no reaction received - /// - /// ``` - /// Future getAv(CommandContext context) async { - /// final msg = await context.replyDelayed(content: context.user.avatarURL()); - /// final emojis = await context.awaitEmojis(msg, Duration(seconds: 15)); - /// - /// } - /// ``` - Future> awaitEmojis(Message msg, Duration duration){ - final collectedEmoji = {}; - return Future>(() async { - await for (final event in client.onMessageReactionAdded.where((evnt) => evnt.message != null && evnt.message!.id == msg.id)) { - if (collectedEmoji.containsKey(event.emoji)) { - // TODO: NNBD: weird stuff - var value = collectedEmoji[event.emoji]; - - if (value != null) { - value += 1; - collectedEmoji[event.emoji] = value; - } - } else { - collectedEmoji[event.emoji] = 1; - } - } - - return collectedEmoji; - }).timeout(duration, onTimeout: () => collectedEmoji); - } - - - /// Waits for first [TypingEvent] and returns it. If timed out returns null. - /// Can listen to specific user by specifying [user] - Future waitForTyping(User user, {Duration timeout = const Duration(seconds: 30)}) => - Future(() => client.onTyping.firstWhere((e) => e.user == user && e.channel == this.channel)).timeout(timeout, onTimeout: () => null); - - /// Gets all context channel messages that satisfies [predicate]. - /// - /// ``` - /// Future getAv(CommandContext context) async { - /// final messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck")); - /// } - /// ``` - Stream nextMessagesWhere(bool Function(MessageReceivedEvent msg) predicate, {int limit = 1}) => - client.onMessageReceived.where((event) => event.message.channel.id == channel.id).where(predicate).take(limit); - - /// Gets next [num] number of any messages sent within one context (same channel). - /// - /// ``` - /// Future getAv(CommandContext context) async { - /// // gets next 10 messages - /// final messages = await context.nextMessages(10); - /// } - /// ``` - Stream nextMessages(int num) => - client.onMessageReceived.where((event) => event.message.channel.id == channel.id).take(num); - - /// Starts typing loop and ends when [callback] resolves. - Future enterTypingState(Future Function() callback) async { - this.channel.startTypingLoop(); - final result = await callback(); - this.channel.stopTypingLoop(); - - return result; - } - - /// Returns list of words separated with space and/or text surrounded by quotes - /// Text: `hi this is "example stuff" which 'can be parsed'` will return - /// `List [hi, this, is, example stuff, which, can be parsed]` - Iterable getArguments() sync* { - final matches = _argumentsRegex.allMatches(this.message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); - - for(final match in matches) { - final group1 = match.group(1); - - yield group1 ?? match.group(2)!; - } - } - - /// Returns list which content of quotes. - /// Text: `hi this is "example stuff" which 'can be parsed'` will return - /// `List [example stuff, can be parsed]` - Iterable getQuotedText() sync* { - final matches = _quotedTextRegex.allMatches(this.message.content.replaceFirst(commandMatcher, "")); - for(final match in matches) { - yield match.group(1)!; - } - } - - /// Returns list of all code blocks in message - /// Language string `dart, java` will be ignored and not included - /// """ - /// n> eval ```(dart)? - /// await reply(content: 'no to elo'); - /// ``` - /// """ - Iterable getCodeBlocks() sync* { - final matches = _codeBlocksRegex.allMatches(message.content); - for (final match in matches) { - final matchedText = match.group(3); - - if (matchedText != null) { - yield matchedText; - } - } - } -} diff --git a/lib/src/CommandHandler.dart b/lib/src/CommandHandler.dart deleted file mode 100644 index fdd0b40..0000000 --- a/lib/src/CommandHandler.dart +++ /dev/null @@ -1,118 +0,0 @@ -part of nyxx_commander; - -/// Base object for [CommandHandler] and [CommandGroup] -abstract class CommandEntity { - /// Executed before executing command. - /// Used to check if command can be executed in current context. - PassHandlerFunction? get beforeHandler => null; - - /// Callback executed after executing command - AfterHandlerFunction? get afterHandler => null; - - /// Name of [CommandEntity] - String get name; - - /// Aliases of [CommandEntity] - List get aliases; - - /// Parent of entity - CommandEntity? get parent; - - /// A list of valid command names - List get commandNames => [if (this.name.isNotEmpty) this.name.toLowerCase(), ...aliases.map((e) => e.toLowerCase())]; - - /// RegEx matching the fully qualified command name with its parents and all aliases - String getFullCommandMatch() { - var parentMatch = ""; - - if (parent != null) { - parentMatch = "${parent!.getFullCommandMatch()} "; - } - - if (this.commandNames.isNotEmpty) { - parentMatch += "(${this.commandNames.join('|')})"; - } - - return parentMatch.toLowerCase(); - } - - /// Returns true if provided String [str] is entity name or alias - bool isEntityName(String str) => commandNames.contains(str.toLowerCase()); -} - -/// Creates command group. Pass a [name] to crated command and commands added -/// via [registerSubCommand] will be subcommands og that group -// ignore: prefer_mixin -class CommandGroup extends CommandEntity with ICommandRegistrable { - @override - final List _commandEntities = []; - - @override - final PassHandlerFunction? beforeHandler; - - @override - final AfterHandlerFunction? afterHandler; - - /// Default [CommandHandler] for [CommandGroup] - it will be executed then no other command from group match - CommandHandler? defaultHandler; - - @override - final String name; - - @override - final List aliases; - - @override - CommandGroup? parent; - - /// Creates command group. Pass a [name] to crated command and commands added - /// via [registerSubCommand] will be subcommands og that group - CommandGroup({this.name = "", this.aliases = const [], this.defaultHandler, this.beforeHandler, this.afterHandler, this.parent}); - - /// Registers default command handler which will be executed if no subcommand is matched to message content - void registerDefaultCommand(CommandHandlerFunction commandHandler, - {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - this.defaultHandler = BasicCommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); - } - - /// Registers subcommand - void registerSubCommand(String name, CommandHandlerFunction commandHandler, - {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - this.registerCommandEntity(BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); - } - - /// Registers command as implemented [CommandEntity] class - void registerCommandGroup(CommandGroup commandGroup) => this.registerCommandEntity(commandGroup..parent = this); -} - -/// Handles command execution - requires to implement [name] field which -/// returns name of command to match message content, and [commandHandler] callback -/// which is fired when command matches message content. -abstract class CommandHandler extends CommandEntity { - /// Main command callback - CommandHandlerFunction get commandHandler; -} - -/// Basic implementation of command handler. Used internally in library. -class BasicCommandHandler extends CommandHandler { - @override - final PassHandlerFunction? beforeHandler; - - @override - final AfterHandlerFunction? afterHandler; - - @override - CommandHandlerFunction commandHandler; - - @override - final String name; - - @override - final List aliases; - - @override - CommandGroup? parent; - - /// Basic implementation of command handler. Used internally in library. - BasicCommandHandler(this.name, this.commandHandler, {this.aliases = const [], this.beforeHandler, this.afterHandler, this.parent}); -} diff --git a/lib/src/command_context.dart b/lib/src/command_context.dart new file mode 100644 index 0000000..2260816 --- /dev/null +++ b/lib/src/command_context.dart @@ -0,0 +1,338 @@ +import 'dart:async'; + +import 'package:nyxx/nyxx.dart'; + +final argumentsRegex = RegExp('([^"\' ]+)|["\']([^"]*)["\']'); +final quotedTextRegex = RegExp('["\']([^"]*)["\']'); +final codeBlocksRegex = RegExp(r"```(\w+)?(\s)?(((.+)(\s)?)+)```"); + +abstract class ICommandContext { + /// Channel from where message come from + ITextChannel get channel; + + /// Author of message + IMessageAuthor get author; + + /// Message that was sent + IMessage get message; + + /// Guild in which message was sent + IGuild? get guild; + + /// Returns author as guild member + IMember? get member; + + /// Reference to client + INyxxWebsocket get client; + + /// Shard on which message was sent + int get shardId; + + /// Substring by which command was matched + String get commandMatcher; + + /// Creates inline reply for message + Future reply(MessageBuilder builder, {bool mention = false, bool reply = false }); + + /// Reply to message. It allows to send regular message, Embed or both. + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// await context.reply(content: context.user.avatarURL()); + /// } + /// ``` + Future sendMessage(MessageBuilder builder); + + /// Reply to messages, then delete it when [duration] expires. + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// await context.replyTemp(content: user.avatarURL()); + /// } + /// ``` + Future sendMessageTemp(Duration duration, MessageBuilder builder); + + /// Replies to message after delay specified with [duration] + /// ``` + /// Future getAv(CommandContext context async { + /// await context.replyDelayed(Duration(seconds: 2), content: user.avatarURL()); + /// } + /// ``` + Future sendMessageDelayed(Duration duration, MessageBuilder builder); + + /// Awaits for emoji under given [msg] + Future awaitEmoji(IMessage msg); + + /// Collects emojis within given [duration]. Returns empty map if no reaction received + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// final msg = await context.replyDelayed(content: context.user.avatarURL()); + /// final emojis = await context.awaitEmojis(msg, Duration(seconds: 15)); + /// + /// } + /// ``` + Future> awaitEmojis(IMessage msg, Duration duration); + + /// Waits for first [TypingEvent] and returns it. If timed out returns null. + /// Can listen to specific user by specifying [user] + Future waitForTyping(IUser user, {Duration timeout = const Duration(seconds: 30)}); + + /// Gets all context channel messages that satisfies [predicate]. + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// final messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck")); + /// } + /// ``` + Stream nextMessagesWhere(bool Function(IMessageReceivedEvent msg) predicate, {int limit = 1}); + + /// Gets next [num] number of any messages sent within one context (same channel). + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// // gets next 10 messages + /// final messages = await context.nextMessages(10); + /// } + /// ``` + Stream nextMessages(int num); + + /// Starts typing loop and ends when [callback] resolves. + Future enterTypingState(Future Function() callback); + + /// Returns list of words separated with space and/or text surrounded by quotes + /// Text: `hi this is "example stuff" which 'can be parsed'` will return + /// `List [hi, this, is, example stuff, which, can be parsed]` + Iterable getArguments(); + + /// Returns list which content of quotes. + /// Text: `hi this is "example stuff" which 'can be parsed'` will return + /// `List [example stuff, can be parsed]` + Iterable getQuotedText(); + + /// Returns list of all code blocks in message + /// Language string `dart, java` will be ignored and not included + /// """ + /// n> eval ```(dart)? + /// await reply(content: 'no to elo'); + /// ``` + /// """ + Iterable getCodeBlocks(); +} + +/// Helper class which describes context in which command is executed +class CommandContext implements ICommandContext { + /// Channel from where message come from + @override + final ITextChannel channel; + + /// Author of message + @override + final IMessageAuthor author; + + /// Message that was sent + @override + final IMessage message; + + /// Guild in which message was sent + @override + final IGuild? guild; + + /// Returns author as guild member + @override + IMember? get member => this.message.member != null + ? message.member! + : null; + + /// Reference to client + @override + INyxxWebsocket get client => channel.client as INyxxWebsocket; + + /// Shard on which message was sent + @override + int get shardId => this.guild != null ? this.guild!.shard.id : 0; + + /// Substring by which command was matched + @override + final String commandMatcher; + + /// Creates na instance of [CommandContext] + CommandContext(this.channel, this.author, this.guild, this.message, this.commandMatcher); + + static final _argumentsRegex = RegExp('([^"\' ]+)|["\']([^"]*)["\']'); + static final _quotedTextRegex = RegExp('["\']([^"]*)["\']'); + static final _codeBlocksRegex = RegExp(r"```(\w+)?(\s)?(((.+)(\s)?)+)```"); + + /// Creates inline reply for message + @override + Future reply(MessageBuilder builder, {bool mention = false, bool reply = false }) async { + if (mention) { + if (builder.allowedMentions != null) { + builder.allowedMentions!.allow(reply: true); + } else { + builder.allowedMentions = AllowedMentions()..allow(reply: true); + } + } + + if (reply) { + builder.replyBuilder = ReplyBuilder.fromMessage(this.message); + } + + return channel.sendMessage(builder); + } + + /// Reply to message. It allows to send regular message, Embed or both. + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// await context.reply(content: context.user.avatarURL()); + /// } + /// ``` + @override + Future sendMessage(MessageBuilder builder) => channel.sendMessage(builder); + + /// Reply to messages, then delete it when [duration] expires. + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// await context.replyTemp(content: user.avatarURL()); + /// } + /// ``` + @override + Future sendMessageTemp(Duration duration, MessageBuilder builder) => channel + .sendMessage(builder) + .then((msg) { + Timer(duration, () => msg.delete()); + return msg; + }); + + /// Replies to message after delay specified with [duration] + /// ``` + /// Future getAv(CommandContext context async { + /// await context.replyDelayed(Duration(seconds: 2), content: user.avatarURL()); + /// } + /// ``` + @override + Future sendMessageDelayed(Duration duration, MessageBuilder builder) => + Future.delayed(duration, () => channel.sendMessage(builder)); + + /// Awaits for emoji under given [msg] + @override + Future awaitEmoji(IMessage msg) async => + (await this.client.eventsWs.onMessageReactionAdded.where((event) => event.message == msg).first).emoji; + + /// Collects emojis within given [duration]. Returns empty map if no reaction received + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// final msg = await context.replyDelayed(content: context.user.avatarURL()); + /// final emojis = await context.awaitEmojis(msg, Duration(seconds: 15)); + /// + /// } + /// ``` + @override + Future> awaitEmojis(IMessage msg, Duration duration){ + final collectedEmoji = {}; + return Future>(() async { + await for (final event in client.eventsWs.onMessageReactionAdded.where((evnt) => evnt.message != null && evnt.message!.id == msg.id)) { + if (collectedEmoji.containsKey(event.emoji)) { + // TODO: NNBD: weird stuff + var value = collectedEmoji[event.emoji]; + + if (value != null) { + value += 1; + collectedEmoji[event.emoji] = value; + } + } else { + collectedEmoji[event.emoji] = 1; + } + } + + return collectedEmoji; + }).timeout(duration, onTimeout: () => collectedEmoji); + } + + + /// Waits for first [TypingEvent] and returns it. If timed out returns null. + /// Can listen to specific user by specifying [user] + @override + Future waitForTyping(IUser user, {Duration timeout = const Duration(seconds: 30)}) => + Future(() => client.eventsWs.onTyping.firstWhere((e) => e.user == user && e.channel == this.channel)).timeout(timeout, onTimeout: () => null); + + /// Gets all context channel messages that satisfies [predicate]. + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// final messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck")); + /// } + /// ``` + @override + Stream nextMessagesWhere(bool Function(IMessageReceivedEvent msg) predicate, {int limit = 1}) => + client.eventsWs.onMessageReceived.where((event) => event.message.channel.id == channel.id).where(predicate).take(limit); + + /// Gets next [num] number of any messages sent within one context (same channel). + /// + /// ``` + /// Future getAv(CommandContext context) async { + /// // gets next 10 messages + /// final messages = await context.nextMessages(10); + /// } + /// ``` + @override + Stream nextMessages(int num) => + client.eventsWs.onMessageReceived.where((event) => event.message.channel.id == channel.id).take(num); + + /// Starts typing loop and ends when [callback] resolves. + @override + Future enterTypingState(Future Function() callback) async { + this.channel.startTypingLoop(); + final result = await callback(); + this.channel.stopTypingLoop(); + + return result; + } + + /// Returns list of words separated with space and/or text surrounded by quotes + /// Text: `hi this is "example stuff" which 'can be parsed'` will return + /// `List [hi, this, is, example stuff, which, can be parsed]` + @override + Iterable getArguments() sync* { + final matches = _argumentsRegex.allMatches(this.message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); + + for(final match in matches) { + final group1 = match.group(1); + + yield group1 ?? match.group(2)!; + } + } + + /// Returns list which content of quotes. + /// Text: `hi this is "example stuff" which 'can be parsed'` will return + /// `List [example stuff, can be parsed]` + @override + Iterable getQuotedText() sync* { + final matches = _quotedTextRegex.allMatches(this.message.content.replaceFirst(commandMatcher, "")); + for(final match in matches) { + yield match.group(1)!; + } + } + + /// Returns list of all code blocks in message + /// Language string `dart, java` will be ignored and not included + /// """ + /// n> eval ```(dart)? + /// await reply(content: 'no to elo'); + /// ``` + /// """ + @override + Iterable getCodeBlocks() sync* { + final matches = _codeBlocksRegex.allMatches(message.content); + for (final match in matches) { + final matchedText = match.group(3); + + if (matchedText != null) { + yield matchedText; + } + } + } +} diff --git a/lib/src/command_handler.dart b/lib/src/command_handler.dart new file mode 100644 index 0000000..1cd9f69 --- /dev/null +++ b/lib/src/command_handler.dart @@ -0,0 +1,202 @@ +import 'package:nyxx_commander/src/utils.dart'; + +abstract class ICommandRegistrable { + List get commandEntities; + + /// Registers [CommandEntity] within context of this instance. Throws error if there is command with same name as provided. + void registerCommandEntity(ICommandEntity entity); +} + +/// Provides common functionality for entities which can register subcommand or sub command groups. +abstract class CommandRegistrableAbstract implements ICommandRegistrable { + @override + List get commandEntities; + + /// Registers [CommandEntity] within context of this instance. Throws error if there is command with same name as provided. + @override + void registerCommandEntity(ICommandEntity entity) { + if (this.commandEntities.any((element) => element.isEntityName(entity.name) )) { + throw Exception("Command name should be unique! There is already command with name: ${entity.name}}"); + } + + if (entity is ICommandGroup && entity.name.isEmpty && entity.aliases.isNotEmpty) { + throw Exception("Command group cannot have aliases if its name is empty! Provided aliases: [${entity.aliases.join(", ")}]"); + } + + this.commandEntities.add(entity); + } +} + +abstract class ICommandEntity { + /// Executed before executing command. + /// Used to check if command can be executed in current context. + PassHandlerFunction? get beforeHandler; + + /// Callback executed after executing command + AfterHandlerFunction? get afterHandler; + + /// Name of [CommandEntityAbstract] + String get name; + + /// Aliases of [CommandEntityAbstract] + List get aliases; + + /// Parent of entity + CommandEntityAbstract? get parent; + + /// A list of valid command names + List get commandNames; + + /// RegEx matching the fully qualified command name with its parents and all aliases + String getFullCommandMatch(); + + /// Returns true if provided String [str] is entity name or alias + bool isEntityName(String str); +} + +/// Base object for [CommandHandlerAbstract] and [CommandGroup] +abstract class CommandEntityAbstract implements ICommandEntity { + /// Executed before executing command. + /// Used to check if command can be executed in current context. + @override + PassHandlerFunction? get beforeHandler => null; + + /// Callback executed after executing command + @override + AfterHandlerFunction? get afterHandler => null; + + /// Name of [CommandEntityAbstract] + @override + String get name; + + /// Aliases of [CommandEntityAbstract] + @override + List get aliases; + + /// Parent of entity + @override + CommandEntityAbstract? get parent; + + /// A list of valid command names + @override + List get commandNames => [if (this.name.isNotEmpty) this.name.toLowerCase(), ...aliases.map((e) => e.toLowerCase())]; + + /// RegEx matching the fully qualified command name with its parents and all aliases + @override + String getFullCommandMatch() { + var parentMatch = ""; + + if (parent != null) { + parentMatch = "${parent!.getFullCommandMatch()} "; + } + + if (this.commandNames.isNotEmpty) { + parentMatch += "(${this.commandNames.join('|')})"; + } + + return parentMatch.toLowerCase(); + } + + /// Returns true if provided String [str] is entity name or alias + @override + bool isEntityName(String str) => commandNames.contains(str.toLowerCase()); +} + +abstract class ICommandGroup implements ICommandEntity, ICommandRegistrable { + /// Default [CommandHandlerAbstract] for [CommandGroup] - it will be executed then no other command from group match + CommandHandlerAbstract? get defaultHandler; + + /// Registers default command handler which will be executed if no subcommand is matched to message content + void registerDefaultCommand(CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}); + + /// Registers subcommand + void registerSubCommand(String name, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}); + + /// Registers command as implemented [CommandEntityAbstract] class + void registerCommandGroup(CommandGroup commandGroup); +} + +/// Creates command group. Pass a [name] to crated command and commands added +/// via [registerSubCommand] will be subcommands og that group +// ignore: prefer_mixin +class CommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract { + @override + final List commandEntities = []; + + @override + final PassHandlerFunction? beforeHandler; + + @override + final AfterHandlerFunction? afterHandler; + + /// Default [CommandHandlerAbstract] for [CommandGroup] - it will be executed then no other command from group match + CommandHandlerAbstract? defaultHandler; + + @override + final String name; + + @override + final List aliases; + + @override + CommandGroup? parent; + + /// Creates command group. Pass a [name] to crated command and commands added + /// via [registerSubCommand] will be subcommands og that group + CommandGroup({this.name = "", this.aliases = const [], this.defaultHandler, this.beforeHandler, this.afterHandler, this.parent}); + + /// Registers default command handler which will be executed if no subcommand is matched to message content + void registerDefaultCommand(CommandHandlerFunction commandHandler, + {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { + this.defaultHandler = BasicCommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); + } + + /// Registers subcommand + void registerSubCommand(String name, CommandHandlerFunction commandHandler, + {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { + this.registerCommandEntity(BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); + } + + /// Registers command as implemented [CommandEntityAbstract] class + void registerCommandGroup(CommandGroup commandGroup) => this.registerCommandEntity(commandGroup..parent = this); +} + +abstract class ICommandHandler implements ICommandEntity { + /// Main command callback + CommandHandlerFunction get commandHandler; +} + +/// Handles command execution - requires to implement [name] field which +/// returns name of command to match message content, and [commandHandler] callback +/// which is fired when command matches message content. +abstract class CommandHandlerAbstract extends CommandEntityAbstract implements ICommandHandler { + +} + +abstract class IBasicCommandHandler implements ICommandHandler { + +} + +/// Basic implementation of command handler. Used internally in library. +class BasicCommandHandler extends CommandHandlerAbstract implements IBasicCommandHandler { + @override + final PassHandlerFunction? beforeHandler; + + @override + final AfterHandlerFunction? afterHandler; + + @override + CommandHandlerFunction commandHandler; + + @override + final String name; + + @override + final List aliases; + + @override + CommandGroup? parent; + + /// Basic implementation of command handler. Used internally in library. + BasicCommandHandler(this.name, this.commandHandler, {this.aliases = const [], this.beforeHandler, this.afterHandler, this.parent}); +} diff --git a/lib/src/Commander.dart b/lib/src/commander.dart similarity index 67% rename from lib/src/Commander.dart rename to lib/src/commander.dart index 547adc0..cde92b4 100644 --- a/lib/src/Commander.dart +++ b/lib/src/commander.dart @@ -1,25 +1,12 @@ -part of nyxx_commander; +import 'dart:async'; +import 'dart:io'; -/// Used to determine if command can be executed in given environment. -/// Return true to allow executing command or false otherwise. -typedef PassHandlerFunction = FutureOr Function(CommandContext context); +import 'package:logging/logging.dart'; +import 'package:nyxx/nyxx.dart'; -/// Handler for executing command logic. -typedef CommandHandlerFunction = FutureOr Function(CommandContext context, String message); - -/// Handler for executing logic after executing command. -typedef AfterHandlerFunction = FutureOr Function(CommandContext context); - -/// Handler used to determine prefix for command in given environment. -/// Can be used to define different prefixes for different guild, users or dms. -/// Return String containing prefix or null if command cannot be executed. -typedef PrefixHandlerFunction = FutureOr Function(Message message); - -/// Callback to customize logger output when command is executed. -typedef LoggerHandlerFunction = FutureOr Function(CommandContext context, String commandName, Logger logger); - -/// Callback called when command executions returns with [Exception] or [Error] ([exception] variable could be either). -typedef CommandExecutionError = FutureOr Function(CommandContext context, dynamic exception); +import 'package:nyxx_commander/src/command_handler.dart'; +import 'package:nyxx_commander/src/command_context.dart'; +import 'package:nyxx_commander/src/utils.dart'; /// Lightweight command framework. Doesn't use `dart:mirrors` and can be used in browser. /// While constructing specify prefix which is string with prefix or @@ -28,7 +15,7 @@ typedef CommandExecutionError = FutureOr Function(CommandContext context, /// Allows to specify callbacks which are executed before and after command - also on per command basis. /// beforeCommandHandler callbacks are executed only command exists and is matched with message content. // ignore: prefer_mixin -class Commander with ICommandRegistrable { +class Commander with CommandRegistrableAbstract { late final PrefixHandlerFunction _prefixHandler; late final PassHandlerFunction? _beforeCommandHandler; late final AfterHandlerFunction? _afterHandlerFunction; @@ -36,21 +23,18 @@ class Commander with ICommandRegistrable { late final CommandExecutionError? _commandExecutionError; @override - final List _commandEntities = []; + final List commandEntities = []; final Logger _logger = Logger("Commander"); /// Resolves prefix for given [message]. Returns null if there is no prefix for given [message] which /// means command wouldn't execute in given context. - FutureOr getPrefixForMessage(Message message) => _prefixHandler(message); - - /// Returns unmodifiable list of registered commands. - List get commands => List.unmodifiable(this._commandEntities); + FutureOr getPrefixForMessage(IMessage message) => _prefixHandler(message); /// Either [prefix] or [prefixHandler] must be specified otherwise program will exit. /// Allows to specify additional [beforeCommandHandler] executed before main command callback, /// and [afterCommandHandler] executed after main command callback. - Commander(Nyxx client, + Commander(INyxxWebsocket client, {String? prefix, PrefixHandlerFunction? prefixHandler, PassHandlerFunction? beforeCommandHandler, @@ -78,7 +62,7 @@ class Commander with ICommandRegistrable { this._commandExecutionError = commandExecutionError; this._loggerHandlerFunction = loggerHandlerFunction ?? _defaultLogger; - client.onMessageReceived.listen(_handleMessage); + client.eventsWs.onMessageReceived.listen(_handleMessage); this._logger.info("Commander ready!"); } @@ -91,7 +75,7 @@ class Commander with ICommandRegistrable { /// Registers command as implemented [CommandEntity] class void registerCommandGroup(CommandGroup commandGroup) => this.registerCommandEntity(commandGroup); - Future _handleMessage(MessageReceivedEvent event) async { + Future _handleMessage(IMessageReceivedEvent event) async { final prefix = await _prefixHandler(event.message); if (prefix == null) { return; @@ -104,7 +88,7 @@ class Commander with ICommandRegistrable { this._logger.finer("Attempting to execute command from message: [${event.message.content}] from [${event.message.author.tag}]"); // Find matching command with given message content - final matchingCommand = _CommandMatcher._findMatchingCommand(event.message.content.toLowerCase().replaceFirst(prefix, "").trim().split(" "), _commandEntities) as CommandHandler?; + final matchingCommand = CommandMatcher.findMatchingCommand(event.message.content.toLowerCase().replaceFirst(prefix, "").trim().split(" "), commandEntities) as ICommandHandler?; if(matchingCommand == null) { return; @@ -121,10 +105,10 @@ class Commander with ICommandRegistrable { this._logger.finer("Preparing command for execution: Command name: $finalCommand"); // construct CommandContext - final context = CommandContext._new( + final context = CommandContext( await event.message.channel.getOrDownload(), event.message.author, - event.message is GuildMessage ? (event.message as GuildMessage).guild.getFromCache()! : null, + event.message.guild?.getFromCache(), event.message, "$prefix$finalCommand", ); @@ -169,7 +153,7 @@ class Commander with ICommandRegistrable { } // Invokes command after handler and its parents - Future _invokeAfterHandler(CommandEntity? commandEntity, CommandContext context) async { + Future _invokeAfterHandler(ICommandEntity? commandEntity, CommandContext context) async { if(commandEntity == null) { return; } @@ -184,7 +168,7 @@ class Commander with ICommandRegistrable { } // Invokes command before handler and its parents. It will check for next before handlers if top handler returns true. - Future _invokeBeforeHandler(CommandEntity? commandEntity, CommandContext context) async { + Future _invokeBeforeHandler(ICommandEntity? commandEntity, CommandContext context) async { if(commandEntity == null) { return true; } @@ -200,31 +184,13 @@ class Commander with ICommandRegistrable { return false; } - FutureOr _defaultLogger(CommandContext ctx, String commandName, Logger logger) { + FutureOr _defaultLogger(ICommandContext ctx, String commandName, Logger logger) { logger.info("Command [$commandName] executed by [${ctx.author.tag}]"); } - bool _hasRequiredIntents(Nyxx client) => + bool _hasRequiredIntents(INyxxWebsocket client) => PermissionsUtils.isApplied(client.intents, GatewayIntents.guildMessages) || PermissionsUtils.isApplied(client.intents, GatewayIntents.directMessages) || PermissionsUtils.isApplied(client.intents, GatewayIntents.guilds) || PermissionsUtils.isApplied(client.intents, GatewayIntents.guilds); } - -/// Provides common functionality for entities which can register subcommand or sub command groups. -abstract class ICommandRegistrable { - List get _commandEntities; - - /// Registers [CommandEntity] within context of this instance. Throws error if there is command with same name as provided. - void registerCommandEntity(CommandEntity entity) { - if (this._commandEntities.any((element) => element.isEntityName(entity.name) )) { - throw Exception("Command name should be unique! There is already command with name: ${entity.name}}"); - } - - if (entity is CommandGroup && entity.name.isEmpty && entity.aliases.isNotEmpty) { - throw Exception("Command group cannot have aliases if its name is empty! Provided aliases: [${entity.aliases.join(", ")}]"); - } - - this._commandEntities.add(entity); - } -} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index e8c1355..a092156 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,11 +1,37 @@ -part of nyxx_commander; +import 'dart:async'; -class _CommandMatcher { +import 'package:logging/logging.dart'; +import 'package:nyxx/nyxx.dart'; +import 'package:nyxx_commander/src/command_context.dart'; +import 'package:nyxx_commander/src/command_handler.dart'; + +/// Used to determine if command can be executed in given environment. +/// Return true to allow executing command or false otherwise. +typedef PassHandlerFunction = FutureOr Function(ICommandContext context); + +/// Handler for executing command logic. +typedef CommandHandlerFunction = FutureOr Function(ICommandContext context, String message); + +/// Handler for executing logic after executing command. +typedef AfterHandlerFunction = FutureOr Function(ICommandContext context); + +/// Handler used to determine prefix for command in given environment. +/// Can be used to define different prefixes for different guild, users or dms. +/// Return String containing prefix or null if command cannot be executed. +typedef PrefixHandlerFunction = FutureOr Function(IMessage message); + +/// Callback to customize logger output when command is executed. +typedef LoggerHandlerFunction = FutureOr Function(ICommandContext context, String commandName, Logger logger); + +/// Callback called when command executions returns with [Exception] or [Error] ([exception] variable could be either). +typedef CommandExecutionError = FutureOr Function(ICommandContext context, dynamic exception); + +class CommandMatcher { /// Matches [commands] from [messageParts]. Performs recursive lookup on available commands and it's children. - static CommandEntity? _findMatchingCommand(Iterable messageParts, Iterable commands) { + static ICommandEntity? findMatchingCommand(Iterable messageParts, Iterable commands) { for (final entity in commands) { if(entity is CommandGroup && entity.name == "") { - final e = _findMatchingCommand(messageParts, entity._commandEntities); + final e = findMatchingCommand(messageParts, entity.commandEntities); if (e != null) { return e; @@ -19,7 +45,7 @@ class _CommandMatcher { return null; } - final e = _findMatchingCommand(messageParts.skip(1), entity._commandEntities); + final e = findMatchingCommand(messageParts.skip(1), entity.commandEntities); if (e != null) { return e; @@ -28,7 +54,7 @@ class _CommandMatcher { } } - if (entity is CommandHandler && entity.isEntityName(messageParts.first)) { + if (entity is CommandHandlerAbstract && entity.isEntityName(messageParts.first)) { return entity; } } diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index ba6c9d5..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,96 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - http: - dependency: "direct main" - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.4" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - logging: - dependency: "direct main" - description: - name: logging - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - nyxx: - dependency: "direct main" - description: - name: nyxx - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" -sdks: - dart: ">=2.14.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f7cd724..8886a5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx_commander -version: 2.0.0 +version: 3.0.0-dev.0 description: Nyxx Commander Module. Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx @@ -13,3 +13,16 @@ dependencies: http: "^0.13.3" logging: "^1.0.1" nyxx: "^2.0.0" + +dev_dependencies: + build_runner: ^2.1.4 + coverage: ^1.0.3 + lints: ^1.0.1 + mockito: ^5.0.16 + test: ^1.19.0 + +dependency_overrides: + nyxx: + git: + url: 'https://github.com/nyxx-discord/nyxx.git' + ref: next diff --git a/test/commander-test.dart b/test/commander-test.dart index f6e04d7..ac33f9f 100644 --- a/test/commander-test.dart +++ b/test/commander-test.dart @@ -2,85 +2,85 @@ import "dart:async"; import "dart:io"; import "package:nyxx/nyxx.dart"; -import "package:nyxx_commander/commander.dart"; +import "package:nyxx_commander/nyxx_commander.dart"; void main() { - final bot = Nyxx(Platform.environment["TEST_TOKEN"]!, GatewayIntents.allUnprivileged, ignoreExceptions: false); - - bot.onMessageReceived.listen((event) async { - if (event.message.content == "Test 1") { - event.message.delete(); // ignore: unawaited_futures - } - - if (event.message.content == "Test 2") { - event.message.delete(); // ignore: unawaited_futures - } - - if (event.message.content == "Test 10") { - event.message.delete(); // ignore: unawaited_futures - } - - if (event.message.content == "Test 11") { - await event.message.delete(); - } - - if (event.message.content == "Test 12") { - await event.message.delete(); - - await event.message.channel.getFromCache()?.sendMessage(MessageBuilder.content("Commander tests completed successfully!")); - exit(0); - } - }); - - bot.onReady.listen((e) async { - final channel = await bot.fetchChannel(Snowflake("846139169818017812")); - - await channel.sendMessage(MessageBuilder.content("Testing Commander")); - - final msg1 = await channel.sendMessage(MessageBuilder.content("test>test1")); - msg1.delete(); // ignore: unawaited_futures - - final msg2 = await channel.sendMessage(MessageBuilder.content("test>test2 arg1")); - msg2.delete(); // ignore: unawaited_futures - - final msg3 = await channel.sendMessage(MessageBuilder.content("test>test3")); - msg3.delete(); // ignore: unawaited_futures - - final msg4 = await channel.sendMessage(MessageBuilder.content("test>test4")); - msg4.delete(); // ignore: unawaited_futures - - final msg5 = await channel.sendMessage(MessageBuilder.content("test>test4 test5")); - msg5.delete(); // ignore: unawaited_futures - }); - - Commander(bot, prefix: "test>", beforeCommandHandler: (context) async { - if (context.message.content.endsWith("test3")) { - await context.channel.sendMessage(MessageBuilder.content("Test 10")); - return true; - } - - return true; - }) - ..registerCommand("test1", (context, message) async { - await context.channel.sendMessage(MessageBuilder.content("Test 1")); - }) - ..registerCommand("test2", (context, message) async { - final args = message.split(" "); - - if (args.length == 2 && args.last == "arg1") { - await context.channel.sendMessage(MessageBuilder.content("Test 2")); - } - }) - ..registerCommand("test3", (context, message) async { - await context.message.delete(); - }) - ..registerCommandGroup(CommandGroup(name: "test4") - ..registerDefaultCommand((context, message) => context.channel.sendMessage(MessageBuilder.content("Test 11"))) - ..registerSubCommand("test5", (context, message) => context.channel.sendMessage(MessageBuilder.content("Test 12"))) - ); - - Timer(const Duration(seconds: 60), () { - print("Timed out waiting for messages"); - exit(1); - }); + // final bot = Nyxx(Platform.environment["TEST_TOKEN"]!, GatewayIntents.allUnprivileged, ignoreExceptions: false); + // + // bot.onMessageReceived.listen((event) async { + // if (event.message.content == "Test 1") { + // event.message.delete(); // ignore: unawaited_futures + // } + // + // if (event.message.content == "Test 2") { + // event.message.delete(); // ignore: unawaited_futures + // } + // + // if (event.message.content == "Test 10") { + // event.message.delete(); // ignore: unawaited_futures + // } + // + // if (event.message.content == "Test 11") { + // await event.message.delete(); + // } + // + // if (event.message.content == "Test 12") { + // await event.message.delete(); + // + // await event.message.channel.getFromCache()?.sendMessage(MessageBuilder.content("Commander tests completed successfully!")); + // exit(0); + // } + // }); + // + // bot.onReady.listen((e) async { + // final channel = await bot.fetchChannel(Snowflake("846139169818017812")); + // + // await channel.sendMessage(MessageBuilder.content("Testing Commander")); + // + // final msg1 = await channel.sendMessage(MessageBuilder.content("test>test1")); + // msg1.delete(); // ignore: unawaited_futures + // + // final msg2 = await channel.sendMessage(MessageBuilder.content("test>test2 arg1")); + // msg2.delete(); // ignore: unawaited_futures + // + // final msg3 = await channel.sendMessage(MessageBuilder.content("test>test3")); + // msg3.delete(); // ignore: unawaited_futures + // + // final msg4 = await channel.sendMessage(MessageBuilder.content("test>test4")); + // msg4.delete(); // ignore: unawaited_futures + // + // final msg5 = await channel.sendMessage(MessageBuilder.content("test>test4 test5")); + // msg5.delete(); // ignore: unawaited_futures + // }); + // + // Commander(bot, prefix: "test>", beforeCommandHandler: (context) async { + // if (context.message.content.endsWith("test3")) { + // await context.channel.sendMessage(MessageBuilder.content("Test 10")); + // return true; + // } + // + // return true; + // }) + // ..registerCommand("test1", (context, message) async { + // await context.channel.sendMessage(MessageBuilder.content("Test 1")); + // }) + // ..registerCommand("test2", (context, message) async { + // final args = message.split(" "); + // + // if (args.length == 2 && args.last == "arg1") { + // await context.channel.sendMessage(MessageBuilder.content("Test 2")); + // } + // }) + // ..registerCommand("test3", (context, message) async { + // await context.message.delete(); + // }) + // ..registerCommandGroup(CommandGroup(name: "test4") + // ..registerDefaultCommand((context, message) => context.channel.sendMessage(MessageBuilder.content("Test 11"))) + // ..registerSubCommand("test5", (context, message) => context.channel.sendMessage(MessageBuilder.content("Test 12"))) + // ); + // + // Timer(const Duration(seconds: 60), () { + // print("Timed out waiting for messages"); + // exit(1); + // }); } diff --git a/test/integration/.gitkeep b/test/integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/private-test.dart b/test/private-test.dart deleted file mode 100644 index 549f023..0000000 --- a/test/private-test.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "dart:io"; - -import "package:nyxx/nyxx.dart"; -import "package:nyxx_commander/commander.dart"; - -void main() { - // final bot = Nyxx(Platform.environment["BOT_TOKEN"]!, GatewayIntents.allUnprivileged, ignoreExceptions: false); - // - // Commander(bot, prefix: "test>") - // ..registerCommand("test1", (context, message) async { - // await context.channel.sendMessage(MessageBuilder.content("Test 1")); - // }) - // ..registerCommand("test2", (context, message) async { - // final args = message.split(" "); - // - // if (args.length == 2 && args.last == "arg1") { - // await context.channel.sendMessage(MessageBuilder.content("Test 2")); - // } - // }) - // ..registerCommand("test3", (context, message) async { - // await context.message.delete(); - // }) - // ..registerCommandGroup(CommandGroup(name: "test4", aliases: ["t4"]) - // ..registerCommandGroup(CommandGroup(name: "test14") - // ..registerSubCommand("test1", (context, message) async => context.channel.sendMessage(MessageBuilder.content("Test 11"))) - // ) - // ); -} diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep new file mode 100644 index 0000000..e69de29 From bfc62c4b8cd35b290e1844281a8c55db59c4a6dd Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 3 Nov 2021 22:07:54 +0100 Subject: [PATCH 02/25] Remove .packages --- .packages | 76 ------------------------------------------------------- 1 file changed, 76 deletions(-) delete mode 100644 .packages diff --git a/.packages b/.packages deleted file mode 100644 index 0ffcf2e..0000000 --- a/.packages +++ /dev/null @@ -1,76 +0,0 @@ -# This file is deprecated. Tools should instead consume -# `.dart_tool/package_config.json`. -# -# For more info see: https://dart.dev/go/dot-packages-deprecation -# -# Generated by pub on 2021-11-03 22:04:20.131154. -_fe_analyzer_shared:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-30.0.0/lib/ -analyzer:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/analyzer-2.7.0/lib/ -args:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ -async:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ -boolean_selector:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/ -build:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build-2.1.1/lib/ -build_config:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_config-1.0.0/lib/ -build_daemon:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_daemon-3.0.1/lib/ -build_resolvers:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_resolvers-2.0.4/lib/ -build_runner:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_runner-2.1.4/lib/ -build_runner_core:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/build_runner_core-7.2.2/lib/ -built_collection:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/built_collection-5.1.1/lib/ -built_value:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/built_value-8.1.3/lib/ -charcode:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ -checked_yaml:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/ -cli_util:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/cli_util-0.3.5/lib/ -code_builder:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/code_builder-4.1.0/lib/ -collection:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0/lib/ -convert:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ -coverage:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/coverage-1.0.3/lib/ -crypto:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ -dart_style:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.0/lib/ -file:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ -fixnum:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/fixnum-1.0.0/lib/ -frontend_server_client:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/frontend_server_client-2.1.2/lib/ -glob:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ -graphs:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/graphs-2.1.0/lib/ -http:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/http-0.13.4/lib/ -http_multi_server:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/http_multi_server-3.0.1/lib/ -http_parser:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/http_parser-4.0.0/lib/ -io:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/io-1.0.3/lib/ -js:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/js-0.6.3/lib/ -json_annotation:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.3.0/lib/ -lints:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/lints-1.0.1/lib/ -logging:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2/lib/ -matcher:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/ -meta:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ -mime:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/mime-1.0.1/lib/ -mockito:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/mockito-5.0.16/lib/ -node_preamble:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/node_preamble-2.0.1/lib/ -nyxx:file:///home/lusha/.pub-cache/git/nyxx-6a6947ef87bc720195c1c7456ee6022dcf163f98/lib/ -package_config:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ -path:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/ -pool:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/pool-1.5.0/lib/ -pub_semver:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.0/lib/ -pubspec_parse:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.1.0/lib/ -shelf:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf-1.2.0/lib/ -shelf_packages_handler:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf_packages_handler-3.0.0/lib/ -shelf_static:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf_static-1.1.0/lib/ -shelf_web_socket:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/shelf_web_socket-1.0.1/lib/ -source_gen:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_gen-1.1.1/lib/ -source_map_stack_trace:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_map_stack_trace-2.1.0/lib/ -source_maps:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_maps-0.10.10/lib/ -source_span:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib/ -stack_trace:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/ -stream_channel:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/ -stream_transform:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/stream_transform-2.0.0/lib/ -string_scanner:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ -term_glyph:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ -test:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test-1.19.3/lib/ -test_api:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.7/lib/ -test_core:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test_core-0.4.8/lib/ -timing:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/timing-1.0.0/lib/ -typed_data:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ -vm_service:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/vm_service-7.3.0/lib/ -watcher:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ -web_socket_channel:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/web_socket_channel-2.1.0/lib/ -webkit_inspection_protocol:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/webkit_inspection_protocol-1.0.0/lib/ -yaml:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ -nyxx_commander:lib/ From 7bf39f4d14ecac815f16ff31ec3a54533f023867 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 3 Nov 2021 22:08:59 +0100 Subject: [PATCH 03/25] Update README.md badges --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1647905..4335a81 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # nyxx_commander -[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx) -[![documentation](https://img.shields.io/badge/Documentation-nyxx-yellow.svg)](https://www.dartdocs.org/documentation/nyxx/latest/) -[![documentation](https://img.shields.io/badge/Documentation-nyxx.commander-yellow.svg)](https://www.dartdocs.org/documentation/nyxx.commander/latest/) -[![documentation](https://img.shields.io/badge/Documentation-nyxx.interactions-yellow.svg)](https://www.dartdocs.org/documentation/nyxx.interactions/latest/) -[![documentation](https://img.shields.io/badge/Documentation-nyxx.extentions-yellow.svg)](https://www.dartdocs.org/documentation/nyxx.extensions/latest/) +[![pub](https://img.shields.io/pub/v/nyxx_commander.svg)](https://pub.dartlang.org/packages/nyxx) +[![documentation](https://img.shields.io/badge/Documentation-nyxx.commander-yellow.svg)](https://www.dartdocs.org/documentation/nyxx.commander/latest/)
Simple, robust framework for creating discord bots for Dart language. From b9fa75a93e5c37b0d43c790fa327e049624dacff Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 3 Nov 2021 22:11:28 +0100 Subject: [PATCH 04/25] Fix Makefile integration tests target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1f7fea2..50ecdf8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ generate-coverage: integration-tests unit-tests coverage-format coverage-gen-htm .PHONY: integration-tests integration-tests: ## Run integration tests with coverage - (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration/integration.dart; exit 0) + (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration; exit 0) .PHONY: unit-tests unit-tests: ## Run unit tests with coverage From 7e214d067a4b6986c7a6e4adc04b514390c38832 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 3 Nov 2021 22:13:41 +0100 Subject: [PATCH 05/25] Fix Makefile integration tests target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 50ecdf8..7e72f4d 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ generate-coverage: integration-tests unit-tests coverage-format coverage-gen-htm .PHONY: integration-tests integration-tests: ## Run integration tests with coverage - (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration; exit 0) + (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration/**; exit 0) .PHONY: unit-tests unit-tests: ## Run unit tests with coverage From 19ca1d944408b1c36dbcc8ba12fde9d23a12cc83 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 3 Nov 2021 23:29:14 +0100 Subject: [PATCH 06/25] Implement first unit tests; Improve style; Add Makefile --- Makefile | 4 ++-- lib/src/command_context.dart | 10 +++------- test/mocks/message.mock.dart | 9 +++++++++ test/mocks/text_channel.mock.dart | 6 ++++++ test/mocks/user.mocks.dart | 6 ++++++ test/unit/.gitkeep | 0 test/unit/command_context_test.dart | 17 +++++++++++++++++ 7 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 test/mocks/message.mock.dart create mode 100644 test/mocks/text_channel.mock.dart create mode 100644 test/mocks/user.mocks.dart delete mode 100644 test/unit/.gitkeep create mode 100644 test/unit/command_context_test.dart diff --git a/Makefile b/Makefile index 7e72f4d..8b96ec0 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,11 @@ generate-coverage: integration-tests unit-tests coverage-format coverage-gen-htm .PHONY: integration-tests integration-tests: ## Run integration tests with coverage - (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration/**; exit 0) + (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration ; exit 0) .PHONY: unit-tests unit-tests: ## Run unit tests with coverage - (timeout 10s dart run test --coverage="coverage" --timeout=none test/unit; exit 0) + (timeout 10s dart run test --coverage="coverage" --timeout=none test/unit ; exit 0) .PHONY: coverage-format coverage-format: ## Format dart coverage output to lcov diff --git a/lib/src/command_context.dart b/lib/src/command_context.dart index 2260816..4befc6a 100644 --- a/lib/src/command_context.dart +++ b/lib/src/command_context.dart @@ -159,10 +159,6 @@ class CommandContext implements ICommandContext { /// Creates na instance of [CommandContext] CommandContext(this.channel, this.author, this.guild, this.message, this.commandMatcher); - static final _argumentsRegex = RegExp('([^"\' ]+)|["\']([^"]*)["\']'); - static final _quotedTextRegex = RegExp('["\']([^"]*)["\']'); - static final _codeBlocksRegex = RegExp(r"```(\w+)?(\s)?(((.+)(\s)?)+)```"); - /// Creates inline reply for message @override Future reply(MessageBuilder builder, {bool mention = false, bool reply = false }) async { @@ -297,7 +293,7 @@ class CommandContext implements ICommandContext { /// `List [hi, this, is, example stuff, which, can be parsed]` @override Iterable getArguments() sync* { - final matches = _argumentsRegex.allMatches(this.message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); + final matches = argumentsRegex.allMatches(this.message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); for(final match in matches) { final group1 = match.group(1); @@ -311,7 +307,7 @@ class CommandContext implements ICommandContext { /// `List [example stuff, can be parsed]` @override Iterable getQuotedText() sync* { - final matches = _quotedTextRegex.allMatches(this.message.content.replaceFirst(commandMatcher, "")); + final matches = quotedTextRegex.allMatches(this.message.content.replaceFirst(commandMatcher, "")); for(final match in matches) { yield match.group(1)!; } @@ -326,7 +322,7 @@ class CommandContext implements ICommandContext { /// """ @override Iterable getCodeBlocks() sync* { - final matches = _codeBlocksRegex.allMatches(message.content); + final matches = codeBlocksRegex.allMatches(message.content); for (final match in matches) { final matchedText = match.group(3); diff --git a/test/mocks/message.mock.dart b/test/mocks/message.mock.dart new file mode 100644 index 0000000..7ec322a --- /dev/null +++ b/test/mocks/message.mock.dart @@ -0,0 +1,9 @@ +import 'package:mockito/mockito.dart'; +import 'package:nyxx/nyxx.dart'; + +class MessageMock extends SnowflakeEntity with Fake implements IMessage { + @override + final String content; + + MessageMock(this.content): super(Snowflake.fromNow()); +} diff --git a/test/mocks/text_channel.mock.dart b/test/mocks/text_channel.mock.dart new file mode 100644 index 0000000..8f15dd5 --- /dev/null +++ b/test/mocks/text_channel.mock.dart @@ -0,0 +1,6 @@ +import 'package:mockito/mockito.dart'; +import 'package:nyxx/nyxx.dart'; + +class TextChannelMock extends SnowflakeEntity with Fake implements ITextChannel { + TextChannelMock(): super(Snowflake.fromNow()); +} diff --git a/test/mocks/user.mocks.dart b/test/mocks/user.mocks.dart new file mode 100644 index 0000000..8d30add --- /dev/null +++ b/test/mocks/user.mocks.dart @@ -0,0 +1,6 @@ +import 'package:mockito/mockito.dart'; +import 'package:nyxx/nyxx.dart'; + +class UserMock extends SnowflakeEntity with Fake implements IUser { + UserMock(): super(Snowflake.fromNow()); +} diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/command_context_test.dart b/test/unit/command_context_test.dart new file mode 100644 index 0000000..9c86319 --- /dev/null +++ b/test/unit/command_context_test.dart @@ -0,0 +1,17 @@ +import 'package:nyxx_commander/src/command_context.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import '../mocks/message.mock.dart'; +import '../mocks/text_channel.mock.dart'; +import '../mocks/user.mocks.dart'; + +main() { + test(".getArguments", () { + final messageMock = MessageMock("\"this is\" first argument \"test\" yeah"); + final commandContext = CommandContext(TextChannelMock(), UserMock(), null, messageMock, "test-command"); + + final result = commandContext.getArguments(); + expect(result, equals(['this is', 'first', 'argument', 'test', 'yeah'])); + }); +} From 3d517dc58134132b1bddddffbf6b34e98947b369 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 5 Nov 2021 22:25:03 +0100 Subject: [PATCH 07/25] Update lints; Fix style --- analysis_options.yaml | 108 ++--------------------------------- lib/src/command_context.dart | 18 +++--- lib/src/command_handler.dart | 16 +++--- lib/src/commander.dart | 34 +++++------ 4 files changed, 39 insertions(+), 137 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index f51b3a9..1f18dab 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,106 +1,8 @@ +include: package:lints/recommended.yaml + analyzer: + exclude: [build/**] + language: + strict-raw-types: true strong-mode: implicit-casts: false -linter: - rules: - - avoid_empty_else - - comment_references - - control_flow_in_finally - - empty_statements - - hash_and_equals - - iterable_contains_unrelated_type - - list_remove_unrelated_type - - avoid_slow_async_io - - cancel_subscriptions - - test_types_in_equals - - throw_in_finally - - valid_regexps - - always_declare_return_types - - annotate_overrides - - avoid_init_to_null - - avoid_return_types_on_setters - - await_only_futures - - camel_case_types - - constant_identifier_names - - empty_constructor_bodies - - library_names - - library_prefixes - - non_constant_identifier_names - - only_throw_errors - - package_api_docs - - package_prefixed_library_names - - prefer_is_not_empty - - slash_for_doc_comments - - type_init_formals - - unnecessary_getters_setters - - package_names - - unnecessary_await_in_return - - use_function_type_syntax_for_parameters - - avoid_returning_null_for_future - - no_duplicate_case_values - - unnecessary_statements - - always_require_non_null_named_parameters - - always_put_required_named_parameters_first - - avoid_catches_without_on_clauses - - avoid_function_literals_in_foreach_calls - - avoid_redundant_argument_values - - avoid_returning_null - - avoid_returning_null_for_void - - avoid_returning_this - - camel_case_extensions - - curly_braces_in_flow_control_structures - - directives_ordering - - empty_catches - - join_return_with_assignment - - leading_newlines_in_multiline_strings - - missing_whitespace_between_adjacent_strings - - no_runtimeType_toString - - null_closures - - omit_local_variable_types - - one_member_abstracts - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_constructors_over_static_methods - - prefer_contains - - prefer_equal_for_default_values - - prefer_expression_function_bodies - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - - prefer_int_literals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - provide_deprecation_message - - prefer_typing_uninitialized_variables - - public_member_api_docs - - type_annotate_public_apis - - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_lambdas - - unnecessary_null_in_if_null_operators - - unnecessary_parenthesis - - unnecessary_raw_strings - - unnecessary_string_escapes - - use_rethrow_when_possible - - use_string_buffers - - void_checks - - use_to_and_as_if_applicable - - sort_pub_dependencies - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_mixin - - prefer_null_aware_operators - - prefer_spread_collections diff --git a/lib/src/command_context.dart b/lib/src/command_context.dart index 4befc6a..a33f39f 100644 --- a/lib/src/command_context.dart +++ b/lib/src/command_context.dart @@ -140,7 +140,7 @@ class CommandContext implements ICommandContext { /// Returns author as guild member @override - IMember? get member => this.message.member != null + IMember? get member => message.member != null ? message.member! : null; @@ -150,7 +150,7 @@ class CommandContext implements ICommandContext { /// Shard on which message was sent @override - int get shardId => this.guild != null ? this.guild!.shard.id : 0; + int get shardId => guild != null ? guild!.shard.id : 0; /// Substring by which command was matched @override @@ -171,7 +171,7 @@ class CommandContext implements ICommandContext { } if (reply) { - builder.replyBuilder = ReplyBuilder.fromMessage(this.message); + builder.replyBuilder = ReplyBuilder.fromMessage(message); } return channel.sendMessage(builder); @@ -215,7 +215,7 @@ class CommandContext implements ICommandContext { /// Awaits for emoji under given [msg] @override Future awaitEmoji(IMessage msg) async => - (await this.client.eventsWs.onMessageReactionAdded.where((event) => event.message == msg).first).emoji; + (await client.eventsWs.onMessageReactionAdded.where((event) => event.message == msg).first).emoji; /// Collects emojis within given [duration]. Returns empty map if no reaction received /// @@ -253,7 +253,7 @@ class CommandContext implements ICommandContext { /// Can listen to specific user by specifying [user] @override Future waitForTyping(IUser user, {Duration timeout = const Duration(seconds: 30)}) => - Future(() => client.eventsWs.onTyping.firstWhere((e) => e.user == user && e.channel == this.channel)).timeout(timeout, onTimeout: () => null); + Future(() => client.eventsWs.onTyping.firstWhere((e) => e.user == user && e.channel == channel)).timeout(timeout, onTimeout: () => null); /// Gets all context channel messages that satisfies [predicate]. /// @@ -281,9 +281,9 @@ class CommandContext implements ICommandContext { /// Starts typing loop and ends when [callback] resolves. @override Future enterTypingState(Future Function() callback) async { - this.channel.startTypingLoop(); + channel.startTypingLoop(); final result = await callback(); - this.channel.stopTypingLoop(); + channel.stopTypingLoop(); return result; } @@ -293,7 +293,7 @@ class CommandContext implements ICommandContext { /// `List [hi, this, is, example stuff, which, can be parsed]` @override Iterable getArguments() sync* { - final matches = argumentsRegex.allMatches(this.message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); + final matches = argumentsRegex.allMatches(message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); for(final match in matches) { final group1 = match.group(1); @@ -307,7 +307,7 @@ class CommandContext implements ICommandContext { /// `List [example stuff, can be parsed]` @override Iterable getQuotedText() sync* { - final matches = quotedTextRegex.allMatches(this.message.content.replaceFirst(commandMatcher, "")); + final matches = quotedTextRegex.allMatches(message.content.replaceFirst(commandMatcher, "")); for(final match in matches) { yield match.group(1)!; } diff --git a/lib/src/command_handler.dart b/lib/src/command_handler.dart index 1cd9f69..4b3f5cd 100644 --- a/lib/src/command_handler.dart +++ b/lib/src/command_handler.dart @@ -15,7 +15,7 @@ abstract class CommandRegistrableAbstract implements ICommandRegistrable { /// Registers [CommandEntity] within context of this instance. Throws error if there is command with same name as provided. @override void registerCommandEntity(ICommandEntity entity) { - if (this.commandEntities.any((element) => element.isEntityName(entity.name) )) { + if (commandEntities.any((element) => element.isEntityName(entity.name) )) { throw Exception("Command name should be unique! There is already command with name: ${entity.name}}"); } @@ -23,7 +23,7 @@ abstract class CommandRegistrableAbstract implements ICommandRegistrable { throw Exception("Command group cannot have aliases if its name is empty! Provided aliases: [${entity.aliases.join(", ")}]"); } - this.commandEntities.add(entity); + commandEntities.add(entity); } } @@ -79,7 +79,7 @@ abstract class CommandEntityAbstract implements ICommandEntity { /// A list of valid command names @override - List get commandNames => [if (this.name.isNotEmpty) this.name.toLowerCase(), ...aliases.map((e) => e.toLowerCase())]; + List get commandNames => [if (name.isNotEmpty) name.toLowerCase(), ...aliases.map((e) => e.toLowerCase())]; /// RegEx matching the fully qualified command name with its parents and all aliases @override @@ -90,8 +90,8 @@ abstract class CommandEntityAbstract implements ICommandEntity { parentMatch = "${parent!.getFullCommandMatch()} "; } - if (this.commandNames.isNotEmpty) { - parentMatch += "(${this.commandNames.join('|')})"; + if (commandNames.isNotEmpty) { + parentMatch += "(${commandNames.join('|')})"; } return parentMatch.toLowerCase(); @@ -148,17 +148,17 @@ class CommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract /// Registers default command handler which will be executed if no subcommand is matched to message content void registerDefaultCommand(CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - this.defaultHandler = BasicCommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); + defaultHandler = BasicCommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); } /// Registers subcommand void registerSubCommand(String name, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - this.registerCommandEntity(BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); + registerCommandEntity(BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); } /// Registers command as implemented [CommandEntityAbstract] class - void registerCommandGroup(CommandGroup commandGroup) => this.registerCommandEntity(commandGroup..parent = this); + void registerCommandGroup(CommandGroup commandGroup) => registerCommandEntity(commandGroup..parent = this); } abstract class ICommandHandler implements ICommandEntity { diff --git a/lib/src/commander.dart b/lib/src/commander.dart index cde92b4..928bfcc 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -57,23 +57,23 @@ class Commander with CommandRegistrableAbstract { _prefixHandler = (_) => prefix; } - this._beforeCommandHandler = beforeCommandHandler; - this._afterHandlerFunction = afterCommandHandler; - this._commandExecutionError = commandExecutionError; - this._loggerHandlerFunction = loggerHandlerFunction ?? _defaultLogger; + _beforeCommandHandler = beforeCommandHandler; + _afterHandlerFunction = afterCommandHandler; + _commandExecutionError = commandExecutionError; + _loggerHandlerFunction = loggerHandlerFunction ?? _defaultLogger; client.eventsWs.onMessageReceived.listen(_handleMessage); - this._logger.info("Commander ready!"); + _logger.info("Commander ready!"); } /// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - this.registerCommandEntity(BasicCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler)); + registerCommandEntity(BasicCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler)); } /// Registers command as implemented [CommandEntity] class - void registerCommandGroup(CommandGroup commandGroup) => this.registerCommandEntity(commandGroup); + void registerCommandGroup(CommandGroup commandGroup) => registerCommandEntity(commandGroup); Future _handleMessage(IMessageReceivedEvent event) async { final prefix = await _prefixHandler(event.message); @@ -85,7 +85,7 @@ class Commander with CommandRegistrableAbstract { return; } - this._logger.finer("Attempting to execute command from message: [${event.message.content}] from [${event.message.author.tag}]"); + _logger.finer("Attempting to execute command from message: [${event.message.content}] from [${event.message.author.tag}]"); // Find matching command with given message content final matchingCommand = CommandMatcher.findMatchingCommand(event.message.content.toLowerCase().replaceFirst(prefix, "").trim().split(" "), commandEntities) as ICommandHandler?; @@ -102,7 +102,7 @@ class Commander with CommandRegistrableAbstract { final match = RegExp("(?${matchingCommand.getFullCommandMatch().trim()})").firstMatch(event.message.content.toLowerCase()); final finalCommand = match?.namedGroup("finalCommand"); - this._logger.finer("Preparing command for execution: Command name: $finalCommand"); + _logger.finer("Preparing command for execution: Command name: $finalCommand"); // construct CommandContext final context = CommandContext( @@ -119,7 +119,7 @@ class Commander with CommandRegistrableAbstract { } // Invoke before handler for commander - if(this._beforeCommandHandler != null && !(await this._beforeCommandHandler!(context))) { + if(_beforeCommandHandler != null && !(await _beforeCommandHandler!(context))) { return; } @@ -127,28 +127,28 @@ class Commander with CommandRegistrableAbstract { try { await matchingCommand.commandHandler(context, event.message.content); } on Exception catch (e) { - if(this._commandExecutionError != null) { + if(_commandExecutionError != null) { await _commandExecutionError!(context, e); } - this._logger.fine("Command [$finalCommand] executed with Exception: $e"); + _logger.fine("Command [$finalCommand] executed with Exception: $e"); } on Error catch (e) { - if(this._commandExecutionError != null) { + if(_commandExecutionError != null) { await _commandExecutionError!(context, e); } - this._logger.fine("Command [$finalCommand] executed with Error: $e"); + _logger.fine("Command [$finalCommand] executed with Error: $e"); } // execute logger callback - _loggerHandlerFunction(context, finalCommand!, this._logger); + _loggerHandlerFunction(context, finalCommand!, _logger); // invoke after handler of command await _invokeAfterHandler(matchingCommand, context); // Invoke after handler for commander - if (this._afterHandlerFunction != null) { - this._afterHandlerFunction!(context); + if (_afterHandlerFunction != null) { + _afterHandlerFunction!(context); } } From 2b4fddf7257dccc35457f0c6207b35ffb0ec49a9 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 5 Nov 2021 22:26:59 +0100 Subject: [PATCH 08/25] Setup CI --- .github/workflows/deploy_docs.yml | 41 +++++++++++ .github/workflows/publish.yml | 26 +++++++ .github/workflows/unit_tests.yml | 113 ++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 .github/workflows/deploy_docs.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 0000000..037edc2 --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,41 @@ +name: deploy dev docs + +on: + push: + branches: + - dev + +jobs: + deploy-docs: + runs-on: ubuntu-latest + + steps: + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub_cache + key: ${{ runner.os }} + + - name: Setup Dart Action + uses: cedx/setup-dart@v2.3.0 + with: + release-channel: stable + + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install dependencies + run: dart pub get + + - name: Generate docs + run: dartdoc + + - name: Deploy nyxx dev docs + uses: easingthemes/ssh-deploy@v2.1.5 + env: + SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_SERVER_KEY }} + ARGS: "-rltDzvO" + SOURCE: "doc/api/" + REMOTE_HOST: ${{ secrets.DEPLOY_REMOTE_HOST }} + REMOTE_USER: ${{ secrets.DEPLOY_REMOTE_USER }} + TARGET: "${{ secrets.DEPLOY_REMOTE_TARGET }}/dartdocs/nyxx_commander/" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fa3ee14 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: publish + +on: + push: + branches: + - main + +jobs: + nyxx_publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: 'publish nyxx package to pub.dev' + id: publish + uses: k-paxian/dart-package-publisher@master + with: + skipTests: true + force: true + suppressBuildRunner: true + credentialJson: ${{ secrets.CREDENTIAL_JSON }} + - name: 'Commit release tag' + if: steps.publish.outputs.success + uses: hole19/git-tag-action@master + env: + TAG: ${{steps.publish.outputs.package}}-${{steps.publish.outputs.localVersion}} + GITHUB_TOKEN: ${{ secrets.TAG_RELEASE_TOKEN }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..1108528 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,113 @@ +name: unit tests + +on: + push: + branches-ignore: + - main + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + env: + TEST_TOKEN: ${{ secrets.TEST_TOKEN }} + steps: + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub_cache + key: ${{ runner.os }} + + - name: Setup Dart Action + uses: cedx/setup-dart@v2.3.0 + with: + release-channel: stable + + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install dependencies + run: dart pub get + + - name: Analyze project source + run: dart analyze + + format: + name: Format + runs-on: ubuntu-latest + env: + TEST_TOKEN: ${{ secrets.TEST_TOKEN }} + steps: + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub_cache + key: ${{ runner.os }} + + - name: Setup Dart Action + uses: cedx/setup-dart@v2.3.0 + with: + release-channel: stable + + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install dependencies + run: dart pub get + + - name: Format + run: dart format --set-exit-if-changed -l 160 ./lib + + tests: + needs: [ format, analyze ] + name: Tests + runs-on: ubuntu-latest + env: + TEST_TOKEN: ${{ secrets.TEST_TOKEN }} + steps: + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub_cache + key: ${{ runner.os }} + + - name: Setup Dart Action + uses: cedx/setup-dart@v2.3.0 + with: + release-channel: stable + + - name: Install lcov + run: sudo apt-get install -y lcov + + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install dependencies + run: dart pub get + + - name: Unit tests + run: dart run test --coverage="coverage" test/unit/** + + - name: Integration tests + run: dart run test --coverage="coverage" test/integration/** + + - name: Format coverage + run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib + + - name: Generate coverage + run: genhtml coverage/coverage.lcov -o coverage/coverage_gen + + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + + - name: Deploy code coverage + uses: easingthemes/ssh-deploy@v2.1.5 + env: + SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_SERVER_KEY }} + ARGS: '-rltDzvO --rsync-path="mkdir -p ${{ secrets.DEPLOY_REMOTE_TARGET }}/coverage/nyxx_commander/${{ steps.extract_branch.outputs.branch }}/ && rsync"' + SOURCE: "coverage/coverage_gen/" + REMOTE_HOST: ${{ secrets.DEPLOY_REMOTE_HOST }} + REMOTE_USER: ${{ secrets.DEPLOY_REMOTE_USER }} + TARGET: "${{ secrets.DEPLOY_REMOTE_TARGET }}/coverage/nyxx_commander/${{ steps.extract_branch.outputs.branch }}/" From cb47dddb61132b8c556840385b710b7a617ce864 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 5 Nov 2021 22:30:22 +0100 Subject: [PATCH 09/25] Fix styling --- lib/nyxx_commander.dart | 3 ++- lib/src/command_context.dart | 34 +++++++++++---------------- lib/src/command_handler.dart | 16 ++++--------- lib/src/commander.dart | 45 ++++++++++++++++++------------------ lib/src/utils.dart | 2 +- 5 files changed, 44 insertions(+), 56 deletions(-) diff --git a/lib/nyxx_commander.dart b/lib/nyxx_commander.dart index a77bd86..e680545 100644 --- a/lib/nyxx_commander.dart +++ b/lib/nyxx_commander.dart @@ -3,4 +3,5 @@ library nyxx_commander; export 'src/command_context.dart' show ICommandContext; export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler; export 'src/commander.dart' show Commander; -export 'src/utils.dart' show AfterHandlerFunction, CommandHandlerFunction, LoggerHandlerFunction,PassHandlerFunction, PrefixHandlerFunction, CommandExecutionError; +export 'src/utils.dart' + show AfterHandlerFunction, CommandHandlerFunction, LoggerHandlerFunction, PassHandlerFunction, PrefixHandlerFunction, CommandExecutionError; diff --git a/lib/src/command_context.dart b/lib/src/command_context.dart index a33f39f..6ae22b3 100644 --- a/lib/src/command_context.dart +++ b/lib/src/command_context.dart @@ -32,7 +32,7 @@ abstract class ICommandContext { String get commandMatcher; /// Creates inline reply for message - Future reply(MessageBuilder builder, {bool mention = false, bool reply = false }); + Future reply(MessageBuilder builder, {bool mention = false, bool reply = false}); /// Reply to message. It allows to send regular message, Embed or both. /// @@ -140,9 +140,7 @@ class CommandContext implements ICommandContext { /// Returns author as guild member @override - IMember? get member => message.member != null - ? message.member! - : null; + IMember? get member => message.member != null ? message.member! : null; /// Reference to client @override @@ -161,7 +159,7 @@ class CommandContext implements ICommandContext { /// Creates inline reply for message @override - Future reply(MessageBuilder builder, {bool mention = false, bool reply = false }) async { + Future reply(MessageBuilder builder, {bool mention = false, bool reply = false}) async { if (mention) { if (builder.allowedMentions != null) { builder.allowedMentions!.allow(reply: true); @@ -195,11 +193,9 @@ class CommandContext implements ICommandContext { /// } /// ``` @override - Future sendMessageTemp(Duration duration, MessageBuilder builder) => channel - .sendMessage(builder) - .then((msg) { - Timer(duration, () => msg.delete()); - return msg; + Future sendMessageTemp(Duration duration, MessageBuilder builder) => channel.sendMessage(builder).then((msg) { + Timer(duration, () => msg.delete()); + return msg; }); /// Replies to message after delay specified with [duration] @@ -209,13 +205,11 @@ class CommandContext implements ICommandContext { /// } /// ``` @override - Future sendMessageDelayed(Duration duration, MessageBuilder builder) => - Future.delayed(duration, () => channel.sendMessage(builder)); + Future sendMessageDelayed(Duration duration, MessageBuilder builder) => Future.delayed(duration, () => channel.sendMessage(builder)); /// Awaits for emoji under given [msg] @override - Future awaitEmoji(IMessage msg) async => - (await client.eventsWs.onMessageReactionAdded.where((event) => event.message == msg).first).emoji; + Future awaitEmoji(IMessage msg) async => (await client.eventsWs.onMessageReactionAdded.where((event) => event.message == msg).first).emoji; /// Collects emojis within given [duration]. Returns empty map if no reaction received /// @@ -227,7 +221,7 @@ class CommandContext implements ICommandContext { /// } /// ``` @override - Future> awaitEmojis(IMessage msg, Duration duration){ + Future> awaitEmojis(IMessage msg, Duration duration) { final collectedEmoji = {}; return Future>(() async { await for (final event in client.eventsWs.onMessageReactionAdded.where((evnt) => evnt.message != null && evnt.message!.id == msg.id)) { @@ -248,7 +242,6 @@ class CommandContext implements ICommandContext { }).timeout(duration, onTimeout: () => collectedEmoji); } - /// Waits for first [TypingEvent] and returns it. If timed out returns null. /// Can listen to specific user by specifying [user] @override @@ -264,7 +257,7 @@ class CommandContext implements ICommandContext { /// ``` @override Stream nextMessagesWhere(bool Function(IMessageReceivedEvent msg) predicate, {int limit = 1}) => - client.eventsWs.onMessageReceived.where((event) => event.message.channel.id == channel.id).where(predicate).take(limit); + client.eventsWs.onMessageReceived.where((event) => event.message.channel.id == channel.id).where(predicate).take(limit); /// Gets next [num] number of any messages sent within one context (same channel). /// @@ -275,8 +268,7 @@ class CommandContext implements ICommandContext { /// } /// ``` @override - Stream nextMessages(int num) => - client.eventsWs.onMessageReceived.where((event) => event.message.channel.id == channel.id).take(num); + Stream nextMessages(int num) => client.eventsWs.onMessageReceived.where((event) => event.message.channel.id == channel.id).take(num); /// Starts typing loop and ends when [callback] resolves. @override @@ -295,7 +287,7 @@ class CommandContext implements ICommandContext { Iterable getArguments() sync* { final matches = argumentsRegex.allMatches(message.content.toLowerCase().replaceFirst(commandMatcher.toLowerCase(), "")); - for(final match in matches) { + for (final match in matches) { final group1 = match.group(1); yield group1 ?? match.group(2)!; @@ -308,7 +300,7 @@ class CommandContext implements ICommandContext { @override Iterable getQuotedText() sync* { final matches = quotedTextRegex.allMatches(message.content.replaceFirst(commandMatcher, "")); - for(final match in matches) { + for (final match in matches) { yield match.group(1)!; } } diff --git a/lib/src/command_handler.dart b/lib/src/command_handler.dart index 4b3f5cd..14fa83b 100644 --- a/lib/src/command_handler.dart +++ b/lib/src/command_handler.dart @@ -15,7 +15,7 @@ abstract class CommandRegistrableAbstract implements ICommandRegistrable { /// Registers [CommandEntity] within context of this instance. Throws error if there is command with same name as provided. @override void registerCommandEntity(ICommandEntity entity) { - if (commandEntities.any((element) => element.isEntityName(entity.name) )) { + if (commandEntities.any((element) => element.isEntityName(entity.name))) { throw Exception("Command name should be unique! There is already command with name: ${entity.name}}"); } @@ -146,14 +146,12 @@ class CommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract CommandGroup({this.name = "", this.aliases = const [], this.defaultHandler, this.beforeHandler, this.afterHandler, this.parent}); /// Registers default command handler which will be executed if no subcommand is matched to message content - void registerDefaultCommand(CommandHandlerFunction commandHandler, - {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { + void registerDefaultCommand(CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { defaultHandler = BasicCommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); } /// Registers subcommand - void registerSubCommand(String name, CommandHandlerFunction commandHandler, - {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { + void registerSubCommand(String name, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { registerCommandEntity(BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); } @@ -169,13 +167,9 @@ abstract class ICommandHandler implements ICommandEntity { /// Handles command execution - requires to implement [name] field which /// returns name of command to match message content, and [commandHandler] callback /// which is fired when command matches message content. -abstract class CommandHandlerAbstract extends CommandEntityAbstract implements ICommandHandler { +abstract class CommandHandlerAbstract extends CommandEntityAbstract implements ICommandHandler {} -} - -abstract class IBasicCommandHandler implements ICommandHandler { - -} +abstract class IBasicCommandHandler implements ICommandHandler {} /// Basic implementation of command handler. Used internally in library. class BasicCommandHandler extends CommandHandlerAbstract implements IBasicCommandHandler { diff --git a/lib/src/commander.dart b/lib/src/commander.dart index 928bfcc..2324872 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -35,12 +35,12 @@ class Commander with CommandRegistrableAbstract { /// Allows to specify additional [beforeCommandHandler] executed before main command callback, /// and [afterCommandHandler] executed after main command callback. Commander(INyxxWebsocket client, - {String? prefix, - PrefixHandlerFunction? prefixHandler, - PassHandlerFunction? beforeCommandHandler, - AfterHandlerFunction? afterCommandHandler, - LoggerHandlerFunction? loggerHandlerFunction, - CommandExecutionError? commandExecutionError}) { + {String? prefix, + PrefixHandlerFunction? prefixHandler, + PassHandlerFunction? beforeCommandHandler, + AfterHandlerFunction? afterCommandHandler, + LoggerHandlerFunction? loggerHandlerFunction, + CommandExecutionError? commandExecutionError}) { if (!_hasRequiredIntents(client)) { _logger.shout("Commander cannot start without required intents (directMessages, guildMessages, guilds)"); exit(1); @@ -81,16 +81,17 @@ class Commander with CommandRegistrableAbstract { return; } - if(!event.message.content.startsWith(prefix)) { + if (!event.message.content.startsWith(prefix)) { return; } _logger.finer("Attempting to execute command from message: [${event.message.content}] from [${event.message.author.tag}]"); // Find matching command with given message content - final matchingCommand = CommandMatcher.findMatchingCommand(event.message.content.toLowerCase().replaceFirst(prefix, "").trim().split(" "), commandEntities) as ICommandHandler?; + final matchingCommand = + CommandMatcher.findMatchingCommand(event.message.content.toLowerCase().replaceFirst(prefix, "").trim().split(" "), commandEntities) as ICommandHandler?; - if(matchingCommand == null) { + if (matchingCommand == null) { return; } @@ -119,7 +120,7 @@ class Commander with CommandRegistrableAbstract { } // Invoke before handler for commander - if(_beforeCommandHandler != null && !(await _beforeCommandHandler!(context))) { + if (_beforeCommandHandler != null && !(await _beforeCommandHandler!(context))) { return; } @@ -127,13 +128,13 @@ class Commander with CommandRegistrableAbstract { try { await matchingCommand.commandHandler(context, event.message.content); } on Exception catch (e) { - if(_commandExecutionError != null) { + if (_commandExecutionError != null) { await _commandExecutionError!(context, e); } _logger.fine("Command [$finalCommand] executed with Exception: $e"); } on Error catch (e) { - if(_commandExecutionError != null) { + if (_commandExecutionError != null) { await _commandExecutionError!(context, e); } @@ -154,30 +155,30 @@ class Commander with CommandRegistrableAbstract { // Invokes command after handler and its parents Future _invokeAfterHandler(ICommandEntity? commandEntity, CommandContext context) async { - if(commandEntity == null) { + if (commandEntity == null) { return; } - if(commandEntity.afterHandler != null) { + if (commandEntity.afterHandler != null) { await commandEntity.afterHandler!(context); } - if(commandEntity.parent != null) { + if (commandEntity.parent != null) { await _invokeAfterHandler(commandEntity.parent, context); } } // Invokes command before handler and its parents. It will check for next before handlers if top handler returns true. Future _invokeBeforeHandler(ICommandEntity? commandEntity, CommandContext context) async { - if(commandEntity == null) { + if (commandEntity == null) { return true; } - if(commandEntity.beforeHandler == null) { + if (commandEntity.beforeHandler == null) { return _invokeBeforeHandler(commandEntity.parent, context); } - if(await commandEntity.beforeHandler!(context)) { + if (await commandEntity.beforeHandler!(context)) { return _invokeBeforeHandler(commandEntity.parent, context); } @@ -189,8 +190,8 @@ class Commander with CommandRegistrableAbstract { } bool _hasRequiredIntents(INyxxWebsocket client) => - PermissionsUtils.isApplied(client.intents, GatewayIntents.guildMessages) - || PermissionsUtils.isApplied(client.intents, GatewayIntents.directMessages) - || PermissionsUtils.isApplied(client.intents, GatewayIntents.guilds) - || PermissionsUtils.isApplied(client.intents, GatewayIntents.guilds); + PermissionsUtils.isApplied(client.intents, GatewayIntents.guildMessages) || + PermissionsUtils.isApplied(client.intents, GatewayIntents.directMessages) || + PermissionsUtils.isApplied(client.intents, GatewayIntents.guilds) || + PermissionsUtils.isApplied(client.intents, GatewayIntents.guilds); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index a092156..642d47a 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -30,7 +30,7 @@ class CommandMatcher { /// Matches [commands] from [messageParts]. Performs recursive lookup on available commands and it's children. static ICommandEntity? findMatchingCommand(Iterable messageParts, Iterable commands) { for (final entity in commands) { - if(entity is CommandGroup && entity.name == "") { + if (entity is CommandGroup && entity.name == "") { final e = findMatchingCommand(messageParts, entity.commandEntities); if (e != null) { From 7fb5fe2f30552757420f0dda54a53f823c8b926e Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 5 Nov 2021 22:33:32 +0100 Subject: [PATCH 10/25] Skip integration tests --- .github/workflows/unit_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 1108528..ce2de7e 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -90,6 +90,7 @@ jobs: - name: Integration tests run: dart run test --coverage="coverage" test/integration/** + continue-on-error: true - name: Format coverage run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib From d2b542afb68448e2a5f8405e843e92b395dc1a7e Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 6 Nov 2021 01:32:59 +0100 Subject: [PATCH 11/25] Remove prefix constructor parameter - only accept prefixHandler; Implement mention prefix handler; Add unit tests --- example/example.dart | 4 +-- example/per_guild_prefix.dart | 2 +- lib/nyxx_commander.dart | 2 +- lib/src/commander.dart | 16 ++---------- lib/src/utils.dart | 11 ++++++++ test/mocks/message.mock.dart | 5 ++++ test/mocks/nyxx_websocket.mock.dart | 7 ++++++ test/unit/prefix_handler.dart | 39 +++++++++++++++++++++++++++++ 8 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 test/mocks/nyxx_websocket.mock.dart create mode 100644 test/unit/prefix_handler.dart diff --git a/example/example.dart b/example/example.dart index e76bd37..cb9fdeb 100644 --- a/example/example.dart +++ b/example/example.dart @@ -6,8 +6,8 @@ void main() { final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); // Start commander with prefix `!` - Commander(bot, prefix: "!") - ..registerCommand("ping", (context, message) { // register command ping that will answer pong + Commander(bot, mentionPrefixHandler) + .registerCommand("ping", (context, message) { // register command ping that will answer pong context.reply(MessageBuilder.content("Pong")); }); } diff --git a/example/per_guild_prefix.dart b/example/per_guild_prefix.dart index 1668791..a4ac1b6 100644 --- a/example/per_guild_prefix.dart +++ b/example/per_guild_prefix.dart @@ -25,7 +25,7 @@ void main() { final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); // Start commander with prefix `!` - Commander(bot, prefixHandler: prefixHandler) // prefixHandler will handle deciding which guild can use which prefix + Commander(bot, prefixHandler) // prefixHandler will handle deciding which guild can use which prefix ..registerCommand("ping", (context, message) { // register command ping that will answer pong context.reply(MessageBuilder.content("Pong")); }) diff --git a/lib/nyxx_commander.dart b/lib/nyxx_commander.dart index e680545..fb268c1 100644 --- a/lib/nyxx_commander.dart +++ b/lib/nyxx_commander.dart @@ -4,4 +4,4 @@ export 'src/command_context.dart' show ICommandContext; export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler; export 'src/commander.dart' show Commander; export 'src/utils.dart' - show AfterHandlerFunction, CommandHandlerFunction, LoggerHandlerFunction, PassHandlerFunction, PrefixHandlerFunction, CommandExecutionError; + show AfterHandlerFunction, CommandHandlerFunction, LoggerHandlerFunction, PassHandlerFunction, PrefixHandlerFunction, CommandExecutionError, mentionPrefixHandler; diff --git a/lib/src/commander.dart b/lib/src/commander.dart index 2324872..7d227c4 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -35,9 +35,8 @@ class Commander with CommandRegistrableAbstract { /// Allows to specify additional [beforeCommandHandler] executed before main command callback, /// and [afterCommandHandler] executed after main command callback. Commander(INyxxWebsocket client, - {String? prefix, - PrefixHandlerFunction? prefixHandler, - PassHandlerFunction? beforeCommandHandler, + this._prefixHandler, + {PassHandlerFunction? beforeCommandHandler, AfterHandlerFunction? afterCommandHandler, LoggerHandlerFunction? loggerHandlerFunction, CommandExecutionError? commandExecutionError}) { @@ -46,17 +45,6 @@ class Commander with CommandRegistrableAbstract { exit(1); } - if (prefix == null && prefixHandler == null) { - _logger.shout("Commander cannot start without both prefix and prefixHandler"); - exit(1); - } - - if (prefix == null) { - _prefixHandler = prefixHandler!; - } else { - _prefixHandler = (_) => prefix; - } - _beforeCommandHandler = beforeCommandHandler; _afterHandlerFunction = afterCommandHandler; _commandExecutionError = commandExecutionError; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 642d47a..d7012e8 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -26,6 +26,17 @@ typedef LoggerHandlerFunction = FutureOr Function(ICommandContext context, /// Callback called when command executions returns with [Exception] or [Error] ([exception] variable could be either). typedef CommandExecutionError = FutureOr Function(ICommandContext context, dynamic exception); +String? mentionPrefixHandler(IMessage message) { + final mentionPrefixRegex = RegExp('<@(!?)(${message.client.appId})>'); + + final matches = mentionPrefixRegex.firstMatch(message.content); + if (matches != null) { + return matches.group(0); + } + + return null; +} + class CommandMatcher { /// Matches [commands] from [messageParts]. Performs recursive lookup on available commands and it's children. static ICommandEntity? findMatchingCommand(Iterable messageParts, Iterable commands) { diff --git a/test/mocks/message.mock.dart b/test/mocks/message.mock.dart index 7ec322a..813803d 100644 --- a/test/mocks/message.mock.dart +++ b/test/mocks/message.mock.dart @@ -1,9 +1,14 @@ import 'package:mockito/mockito.dart'; import 'package:nyxx/nyxx.dart'; +import 'nyxx_websocket.mock.dart'; + class MessageMock extends SnowflakeEntity with Fake implements IMessage { @override final String content; + @override + INyxx get client => NyxxWebsocketMock(); + MessageMock(this.content): super(Snowflake.fromNow()); } diff --git a/test/mocks/nyxx_websocket.mock.dart b/test/mocks/nyxx_websocket.mock.dart new file mode 100644 index 0000000..47548bc --- /dev/null +++ b/test/mocks/nyxx_websocket.mock.dart @@ -0,0 +1,7 @@ +import 'package:mockito/mockito.dart'; +import 'package:nyxx/nyxx.dart'; + +class NyxxWebsocketMock extends Fake implements INyxxWebsocket { + @override + Snowflake get appId => Snowflake(123); +} diff --git a/test/unit/prefix_handler.dart b/test/unit/prefix_handler.dart new file mode 100644 index 0000000..911bb1b --- /dev/null +++ b/test/unit/prefix_handler.dart @@ -0,0 +1,39 @@ +import 'package:nyxx_commander/nyxx_commander.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import '../mocks/message.mock.dart'; + +main() { + group("mentionPrefixHandler", () { + test("valid message", () { + final messageWithMention = MessageMock('<@!123> this is example command'); + + final result = mentionPrefixHandler(messageWithMention); + expect(result, isNotNull); + expect(result, equals("<@!123>")); + }); + + test("valid message mention at the end", () { + final messageWithMention = MessageMock('this is example command <@123>'); + + final result = mentionPrefixHandler(messageWithMention); + expect(result, isNotNull); + expect(result, equals("<@123>")); + }); + + test('invalid message no mention', () { + final messageWithMention = MessageMock('!some-other-stuff this is example command'); + + final result = mentionPrefixHandler(messageWithMention); + expect(result, isNotNull); + }); + + test('invalid message invalid app id', () { + final messageWithMention = MessageMock('<@!321> this is example command'); + + final result = mentionPrefixHandler(messageWithMention); + expect(result, isNotNull); + }); + }); +} From 0a63c1afc79696d21d41f02147ad64e0e5b5cc2d Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 6 Nov 2021 11:07:55 +0100 Subject: [PATCH 12/25] Fix style --- lib/nyxx_commander.dart | 9 ++++++++- lib/src/commander.dart | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/nyxx_commander.dart b/lib/nyxx_commander.dart index fb268c1..c14170f 100644 --- a/lib/nyxx_commander.dart +++ b/lib/nyxx_commander.dart @@ -4,4 +4,11 @@ export 'src/command_context.dart' show ICommandContext; export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler; export 'src/commander.dart' show Commander; export 'src/utils.dart' - show AfterHandlerFunction, CommandHandlerFunction, LoggerHandlerFunction, PassHandlerFunction, PrefixHandlerFunction, CommandExecutionError, mentionPrefixHandler; + show + AfterHandlerFunction, + CommandHandlerFunction, + LoggerHandlerFunction, + PassHandlerFunction, + PrefixHandlerFunction, + CommandExecutionError, + mentionPrefixHandler; diff --git a/lib/src/commander.dart b/lib/src/commander.dart index 7d227c4..acc8531 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -34,8 +34,7 @@ class Commander with CommandRegistrableAbstract { /// Either [prefix] or [prefixHandler] must be specified otherwise program will exit. /// Allows to specify additional [beforeCommandHandler] executed before main command callback, /// and [afterCommandHandler] executed after main command callback. - Commander(INyxxWebsocket client, - this._prefixHandler, + Commander(INyxxWebsocket client, this._prefixHandler, {PassHandlerFunction? beforeCommandHandler, AfterHandlerFunction? afterCommandHandler, LoggerHandlerFunction? loggerHandlerFunction, From d771b1e7bfea0bb17e8d8871ad7b6dd42a4e7a2f Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 6 Nov 2021 11:27:46 +0100 Subject: [PATCH 13/25] Fix prefix unit tests --- test/unit/prefix_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/prefix_handler.dart b/test/unit/prefix_handler.dart index 911bb1b..d798685 100644 --- a/test/unit/prefix_handler.dart +++ b/test/unit/prefix_handler.dart @@ -33,7 +33,7 @@ main() { final messageWithMention = MessageMock('<@!321> this is example command'); final result = mentionPrefixHandler(messageWithMention); - expect(result, isNotNull); + expect(result, isNull); }); }); } From fd74a0879930dd2a47faf9f706015de9dca8834f Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 6 Nov 2021 11:34:07 +0100 Subject: [PATCH 14/25] Fix prefix unit tests --- test/unit/prefix_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/prefix_handler.dart b/test/unit/prefix_handler.dart index d798685..6019af9 100644 --- a/test/unit/prefix_handler.dart +++ b/test/unit/prefix_handler.dart @@ -26,7 +26,7 @@ main() { final messageWithMention = MessageMock('!some-other-stuff this is example command'); final result = mentionPrefixHandler(messageWithMention); - expect(result, isNotNull); + expect(result, isNull); }); test('invalid message invalid app id', () { From 2d2e59690c560e4fb4050ec8bd13e0f7147a169c Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 6 Nov 2021 18:17:37 +0100 Subject: [PATCH 15/25] Export CommandHandler and CommandGroup --- lib/nyxx_commander.dart | 2 +- lib/src/command_handler.dart | 32 +++++++++++++++++++------------- lib/src/commander.dart | 4 ++-- lib/src/utils.dart | 4 ++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/nyxx_commander.dart b/lib/nyxx_commander.dart index c14170f..c027dbe 100644 --- a/lib/nyxx_commander.dart +++ b/lib/nyxx_commander.dart @@ -1,7 +1,7 @@ library nyxx_commander; export 'src/command_context.dart' show ICommandContext; -export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler; +export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler, CommandHandler, CommandGroup; export 'src/commander.dart' show Commander; export 'src/utils.dart' show diff --git a/lib/src/command_handler.dart b/lib/src/command_handler.dart index 14fa83b..98b0930 100644 --- a/lib/src/command_handler.dart +++ b/lib/src/command_handler.dart @@ -54,7 +54,7 @@ abstract class ICommandEntity { bool isEntityName(String str); } -/// Base object for [CommandHandlerAbstract] and [CommandGroup] +/// Base object for [CommandHandlerAbstract] and [BasicCommandGroup] abstract class CommandEntityAbstract implements ICommandEntity { /// Executed before executing command. /// Used to check if command can be executed in current context. @@ -103,7 +103,7 @@ abstract class CommandEntityAbstract implements ICommandEntity { } abstract class ICommandGroup implements ICommandEntity, ICommandRegistrable { - /// Default [CommandHandlerAbstract] for [CommandGroup] - it will be executed then no other command from group match + /// Default [CommandHandlerAbstract] for [BasicCommandGroup] - it will be executed then no other command from group match CommandHandlerAbstract? get defaultHandler; /// Registers default command handler which will be executed if no subcommand is matched to message content @@ -113,13 +113,13 @@ abstract class ICommandGroup implements ICommandEntity, ICommandRegistrable { void registerSubCommand(String name, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}); /// Registers command as implemented [CommandEntityAbstract] class - void registerCommandGroup(CommandGroup commandGroup); + void registerCommandGroup(BasicCommandGroup commandGroup); } /// Creates command group. Pass a [name] to crated command and commands added /// via [registerSubCommand] will be subcommands og that group // ignore: prefer_mixin -class CommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract { +class BasicCommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract { @override final List commandEntities = []; @@ -129,7 +129,7 @@ class CommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract @override final AfterHandlerFunction? afterHandler; - /// Default [CommandHandlerAbstract] for [CommandGroup] - it will be executed then no other command from group match + /// Default [CommandHandlerAbstract] for [BasicCommandGroup] - it will be executed then no other command from group match CommandHandlerAbstract? defaultHandler; @override @@ -139,24 +139,30 @@ class CommandGroup extends CommandEntityAbstract with CommandRegistrableAbstract final List aliases; @override - CommandGroup? parent; + BasicCommandGroup? parent; /// Creates command group. Pass a [name] to crated command and commands added /// via [registerSubCommand] will be subcommands og that group - CommandGroup({this.name = "", this.aliases = const [], this.defaultHandler, this.beforeHandler, this.afterHandler, this.parent}); + BasicCommandGroup({this.name = "", this.aliases = const [], this.defaultHandler, this.beforeHandler, this.afterHandler, this.parent}); /// Registers default command handler which will be executed if no subcommand is matched to message content void registerDefaultCommand(CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - defaultHandler = BasicCommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); + defaultHandler = CommandHandler("", commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this); } /// Registers subcommand void registerSubCommand(String name, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - registerCommandEntity(BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); + registerCommandEntity(CommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this)); } /// Registers command as implemented [CommandEntityAbstract] class - void registerCommandGroup(CommandGroup commandGroup) => registerCommandEntity(commandGroup..parent = this); + void registerCommandGroup(BasicCommandGroup commandGroup) => registerCommandEntity(commandGroup..parent = this); +} + +class CommandGroup extends BasicCommandGroup { + /// Creates command group. Pass a [name] to crated command and commands added + /// via [registerSubCommand] will be subcommands og that group + CommandGroup({String name = "", List aliases = const [], CommandHandlerAbstract? defaultHandler, PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler, CommandGroup? parent}); } abstract class ICommandHandler implements ICommandEntity { @@ -172,7 +178,7 @@ abstract class CommandHandlerAbstract extends CommandEntityAbstract implements I abstract class IBasicCommandHandler implements ICommandHandler {} /// Basic implementation of command handler. Used internally in library. -class BasicCommandHandler extends CommandHandlerAbstract implements IBasicCommandHandler { +class CommandHandler extends CommandHandlerAbstract implements IBasicCommandHandler { @override final PassHandlerFunction? beforeHandler; @@ -189,8 +195,8 @@ class BasicCommandHandler extends CommandHandlerAbstract implements IBasicComman final List aliases; @override - CommandGroup? parent; + BasicCommandGroup? parent; /// Basic implementation of command handler. Used internally in library. - BasicCommandHandler(this.name, this.commandHandler, {this.aliases = const [], this.beforeHandler, this.afterHandler, this.parent}); + CommandHandler(this.name, this.commandHandler, {this.aliases = const [], this.beforeHandler, this.afterHandler, this.parent}); } diff --git a/lib/src/commander.dart b/lib/src/commander.dart index acc8531..3709ca2 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -56,11 +56,11 @@ class Commander with CommandRegistrableAbstract { /// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { - registerCommandEntity(BasicCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler)); + registerCommandEntity(CommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler)); } /// Registers command as implemented [CommandEntity] class - void registerCommandGroup(CommandGroup commandGroup) => registerCommandEntity(commandGroup); + void registerCommandGroup(BasicCommandGroup commandGroup) => registerCommandEntity(commandGroup); Future _handleMessage(IMessageReceivedEvent event) async { final prefix = await _prefixHandler(event.message); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index d7012e8..c0439ae 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -41,7 +41,7 @@ class CommandMatcher { /// Matches [commands] from [messageParts]. Performs recursive lookup on available commands and it's children. static ICommandEntity? findMatchingCommand(Iterable messageParts, Iterable commands) { for (final entity in commands) { - if (entity is CommandGroup && entity.name == "") { + if (entity is BasicCommandGroup && entity.name == "") { final e = findMatchingCommand(messageParts, entity.commandEntities); if (e != null) { @@ -49,7 +49,7 @@ class CommandMatcher { } } - if (entity is CommandGroup && entity.isEntityName(messageParts.first)) { + if (entity is BasicCommandGroup && entity.isEntityName(messageParts.first)) { if (messageParts.length == 1 && entity.defaultHandler != null) { return entity.defaultHandler; } else if (messageParts.length == 1 && entity.defaultHandler == null) { From cabe640651866e9b9f9c77bdef63a2a3e2ef0c7f Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 6 Nov 2021 18:17:58 +0100 Subject: [PATCH 16/25] Fix formatting --- lib/src/command_handler.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/command_handler.dart b/lib/src/command_handler.dart index 98b0930..b243ee4 100644 --- a/lib/src/command_handler.dart +++ b/lib/src/command_handler.dart @@ -162,7 +162,13 @@ class BasicCommandGroup extends CommandEntityAbstract with CommandRegistrableAbs class CommandGroup extends BasicCommandGroup { /// Creates command group. Pass a [name] to crated command and commands added /// via [registerSubCommand] will be subcommands og that group - CommandGroup({String name = "", List aliases = const [], CommandHandlerAbstract? defaultHandler, PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler, CommandGroup? parent}); + CommandGroup( + {String name = "", + List aliases = const [], + CommandHandlerAbstract? defaultHandler, + PassHandlerFunction? beforeHandler, + AfterHandlerFunction? afterHandler, + CommandGroup? parent}); } abstract class ICommandHandler implements ICommandEntity { From 6028a7b336511eee6942212217bb884486de2a52 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 7 Nov 2021 11:10:33 +0100 Subject: [PATCH 17/25] Update github documents; Readme and contributing --- .github/ISSUE_TEMPLATE/bug_report.md | 29 ++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 28 ++++++++ CONTRIBUTING.md | 29 ++++++++ README.md | 83 +++++++---------------- 5 files changed, 132 insertions(+), 57 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..39c20a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Windows (with version), Linux (with kernel version)] + - Dart version [e.g. 2.14.0] + - Nyxx version [e.g. 3.0.0] + - nyxx_commander version [e.g. 3.0.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11fc491 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..b5ba64c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + +Use smart commits here to manipulate issues (eg. Fixes #issue) + +## Connected issues & potential other potential problems + +If changes are connected to other issues or are affecting code in other parts of framework +(e.g. in main package or any subpackage) make sure to link and describe where and why problem could be present + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +# Checklist: + +- [ ] Ran `dart analyze` or `make analyze` and fixed all issues +- [ ] Ran `dart format --set-exit-if-changed -l 160 ./lib` or `make format` and fixed all issues +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have checked my changes haven't lowered code coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e2c6919 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing +Nyxx is free and open-source project, and all contributions are welcome and highly appreciated. +However, please conform to the following guidelines when possible. + +## Branches + +Repo contains few main protected branches: +- `main` - for current stable version. Used for releasing new versions +- `dev` - for changes for next minor or patch version release +- `next` - for changes for next major version release + +## Development cycle + +All changes should be discussed beforehand either in issue or pull request on github +or in a discussion in our Discord channel with library regulars or other contributors. + +All issues marked with 'help-needed' badge are free to be picked up by any member of the community. + +### Pull Requests + +Pull requests should be descriptive about changes that are made. +If adding new functionality or modifying existing, documentation should be added/modified to reflect changes. + +## Coding style + +We attempt to conform [Effective Dart Coding Style](https://dart.dev/guides/language/effective-dart/style) where possible. +However, code style rules are not enforcement and code should be readable and easy to maintain. + +**One exception to rules above is line limit - we use 160 character line limit instead of 80 chars.** diff --git a/README.md b/README.md index 4335a81..41c2711 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # nyxx_commander -[![pub](https://img.shields.io/pub/v/nyxx_commander.svg)](https://pub.dartlang.org/packages/nyxx) -[![documentation](https://img.shields.io/badge/Documentation-nyxx.commander-yellow.svg)](https://www.dartdocs.org/documentation/nyxx.commander/latest/)
+[![Discord Shield](https://discordapp.com/api/guilds/846136758470443069/widget.png?style=shield)](https://discord.gg/nyxx) +[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx_commander) +[![documentation](https://img.shields.io/badge/Documentation-nyxx_interactions-yellow.svg)](https://www.dartdocs.org/documentation/nyxx_commander/latest/) Simple, robust framework for creating discord bots for Dart language. @@ -9,74 +10,48 @@ Simple, robust framework for creating discord bots for Dart language. ### Features -- **Slash commands support**
- Supports and provides easy API for creating and handling slash commands -- **Commands framework included**
- A fast way to create a bot with command support. Implementing the framework is simple - and everything is done automatically. -- **Cross Platform**
- Nyxx works on the command line, in the browser, and on mobile devices. -- **Fine Control**
- Nyxx allows you to control every outgoing HTTP request or WebSocket message. -- **Complete**
- Nyxx supports nearly all Discord API endpoints. - +- **commands and subcommands** +- **aliases support** +- **extensive support for middleware** +- **lightweight** ## Quick example -Basic usage: -```dart -void main() { - final bot = Nyxx("TOKEN", GatewayIntents.allUnprivileged); - - bot.onMessageReceived.listen((event) { - if (event.message.content == "!ping") { - event.message.channel.getFromCache()?.sendMessage(content: "Pong!"); - } - }); -} -``` - -Slash commands: -```dart -void main() { - final bot = Nyxx("<%TOKEN%>", GatewayIntents.allUnprivileged); - Interactions(bot) - ..registerSlashCommand( - SlashCommandBuilder("hi", "This is example slash command", []) - ..registerHandler((event) async { - await event.acknowledge(); - - await event.respond(MessageBuilder.content("Hello World!")); - }) - ); -} -``` - Commands: ```dart void main() { - final bot = Nyxx("TOKEN", GatewayIntents.allUnprivileged); + final bot = NyxxFactory.createNyxxWebsocket("", GatewayIntents.allUnprivileged); Commander(bot, prefix: "!!!") ..registerCommand("ping", (context, message) => context.reply(content: "Pong!")); } ``` -## More examples +## Other nyxx packages -Nyxx examples can be found [here](https://github.com/l7ssha/nyxx/tree/dev/nyxx/example). +- [nyxx](https://github.com/nyxx-discord/nyxx) +- [nyxx_interactions](https://github.com/nyxx-discord/nyxx_interactions) +- [nyxx_extensions](https://github.com/nyxx-discord/nyxx_extensions) +- [nyxx_lavalink](https://github.com/nyxx-discord/nyxx_lavalink) +- [nyxx_pagination](https://github.com/nyxx-discord/nyxx_pagination) -Commander examples can be found [here](https://github.com/l7ssha/nyxx/tree/dev/nyxx.commander/example) +## More examples -Slash commands (interactions) examples can be found [here](https://github.com/l7ssha/nyxx/tree/dev/nyxx.interactions/example) +Nyxx examples can be found [here](https://github.com/nyxx-discord/nyxx_commander/tree/dev/example). ### Example bots - [Running on Dart](https://github.com/l7ssha/running_on_dart) ## Documentation, help and examples -**Dartdoc documentation is hosted on [pub](https://www.dartdocs.org/documentation/nyxx/latest/). -This wiki just fills gap in docs with more descriptive guides and tutorials.** +**Dartdoc documentation for latest stable version is hosted on [pub](https://www.dartdocs.org/documentation/nyxx_commander/latest/)** + +#### [Docs and wiki](https://nyxx.l7ssha.xyz) +You can read docs and wiki articles for latest stable version on my website. This website also hosts docs for latest +dev changes to framework (`dev` branch) + +#### [Official nyxx discord server](https://discord.gg/nyxx) +If you need assistance in developing bot using nyxx you can join official nyxx discord guild. #### [Discord API docs](https://discordapp.com/developers/docs/intro) Discord API documentation features rich descriptions about all topics that nyxx covers. @@ -84,18 +59,12 @@ Discord API documentation features rich descriptions about all topics that nyxx #### [Discord API Guild](https://discord.gg/discord-api) The unofficial guild for Discord Bot developers. To get help with nyxx check `#dart_nyxx` channel. -#### [Dartdocs](https://www.dartdocs.org/documentation/nyxx/latest/) +#### [Dartdocs](https://www.dartdocs.org/documentation/nyxx_commander/latest/) The dartdocs page will always have the documentation for the latest release. -#### [Dev docs](https://nyxx.l7ssha.xyz) -You can read about upcoming changes in the library on my website. - -#### [Wiki](https://github.com/l7ssha/nyxx/wiki) -Wiki documentation are designed to match the latest Nyxx release. - ## Contributing to Nyxx -Read [contributing document](https://github.com/l7ssha/nyxx/blob/development/CONTRIBUTING.md) +Read [contributing document](https://github.com/l7ssha/nyxx_commander/blob/development/CONTRIBUTING.md) ## Credits From fb2532a35e1467b01bd43fc9a8cf5220fd43722d Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Tue, 9 Nov 2021 17:03:42 +0100 Subject: [PATCH 18/25] Fix CommandGroups bugs not passing parameters down the entity inheritance --- lib/src/command_handler.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/command_handler.dart b/lib/src/command_handler.dart index b243ee4..f4e49a3 100644 --- a/lib/src/command_handler.dart +++ b/lib/src/command_handler.dart @@ -168,7 +168,8 @@ class CommandGroup extends BasicCommandGroup { CommandHandlerAbstract? defaultHandler, PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler, - CommandGroup? parent}); + CommandGroup? parent}) + : super(name: name, aliases: aliases, defaultHandler: defaultHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: parent); } abstract class ICommandHandler implements ICommandEntity { From 9f3780dc1ef6f12d33291ad13982faa67622c6ff Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 11 Nov 2021 22:56:09 +0100 Subject: [PATCH 19/25] Fixup pub badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41c2711..0dac6a3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # nyxx_commander [![Discord Shield](https://discordapp.com/api/guilds/846136758470443069/widget.png?style=shield)](https://discord.gg/nyxx) -[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx_commander) +[![pub](https://img.shields.io/pub/v/nyxx_commander.svg)](https://pub.dartlang.org/packages/nyxx_commander) [![documentation](https://img.shields.io/badge/Documentation-nyxx_interactions-yellow.svg)](https://www.dartdocs.org/documentation/nyxx_commander/latest/) Simple, robust framework for creating discord bots for Dart language. From 66714b384da1d04d2cfab073b3b587ab6be1e682 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 11 Nov 2021 23:03:53 +0100 Subject: [PATCH 20/25] Do not export main class; Add create method to interface --- example/example.dart | 2 +- example/per_guild_prefix.dart | 2 +- lib/nyxx_commander.dart | 2 +- lib/src/commander.dart | 33 ++++++++++++++++++++++++++++----- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/example/example.dart b/example/example.dart index cb9fdeb..e3d3550 100644 --- a/example/example.dart +++ b/example/example.dart @@ -6,7 +6,7 @@ void main() { final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); // Start commander with prefix `!` - Commander(bot, mentionPrefixHandler) + ICommander.create(bot, mentionPrefixHandler) .registerCommand("ping", (context, message) { // register command ping that will answer pong context.reply(MessageBuilder.content("Pong")); }); diff --git a/example/per_guild_prefix.dart b/example/per_guild_prefix.dart index a4ac1b6..6ffb77e 100644 --- a/example/per_guild_prefix.dart +++ b/example/per_guild_prefix.dart @@ -25,7 +25,7 @@ void main() { final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); // Start commander with prefix `!` - Commander(bot, prefixHandler) // prefixHandler will handle deciding which guild can use which prefix + ICommander.create(bot, prefixHandler) // prefixHandler will handle deciding which guild can use which prefix ..registerCommand("ping", (context, message) { // register command ping that will answer pong context.reply(MessageBuilder.content("Pong")); }) diff --git a/lib/nyxx_commander.dart b/lib/nyxx_commander.dart index c027dbe..b1d1afe 100644 --- a/lib/nyxx_commander.dart +++ b/lib/nyxx_commander.dart @@ -2,7 +2,7 @@ library nyxx_commander; export 'src/command_context.dart' show ICommandContext; export 'src/command_handler.dart' show ICommandRegistrable, ICommandEntity, IBasicCommandHandler, ICommandGroup, ICommandHandler, CommandHandler, CommandGroup; -export 'src/commander.dart' show Commander; +export 'src/commander.dart' show ICommander; export 'src/utils.dart' show AfterHandlerFunction, diff --git a/lib/src/commander.dart b/lib/src/commander.dart index 3709ca2..63c46fa 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -8,6 +8,29 @@ import 'package:nyxx_commander/src/command_handler.dart'; import 'package:nyxx_commander/src/command_context.dart'; import 'package:nyxx_commander/src/utils.dart'; +abstract class ICommander implements CommandRegistrableAbstract { + /// Resolves prefix for given [message]. Returns null if there is no prefix for given [message] which + /// means command wouldn't execute in given context. + FutureOr getPrefixForMessage(IMessage message); + + /// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks + void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}); + + /// Registers command as implemented [CommandEntity] class + void registerCommandGroup(BasicCommandGroup commandGroup); + + static ICommander create(INyxxWebsocket client, PrefixHandlerFunction prefixHandler, + {PassHandlerFunction? beforeCommandHandler, + AfterHandlerFunction? afterCommandHandler, + LoggerHandlerFunction? loggerHandlerFunction, + CommandExecutionError? commandExecutionError}) => + Commander(client, prefixHandler, + beforeCommandHandler: beforeCommandHandler, + afterCommandHandler: afterCommandHandler, + loggerHandlerFunction: loggerHandlerFunction, + commandExecutionError: commandExecutionError); +} + /// Lightweight command framework. Doesn't use `dart:mirrors` and can be used in browser. /// While constructing specify prefix which is string with prefix or /// implement [PrefixHandlerFunction] for more fine control over where and in what conditions commands are executed. @@ -15,7 +38,7 @@ import 'package:nyxx_commander/src/utils.dart'; /// Allows to specify callbacks which are executed before and after command - also on per command basis. /// beforeCommandHandler callbacks are executed only command exists and is matched with message content. // ignore: prefer_mixin -class Commander with CommandRegistrableAbstract { +class Commander extends CommandRegistrableAbstract implements ICommander { late final PrefixHandlerFunction _prefixHandler; late final PassHandlerFunction? _beforeCommandHandler; late final AfterHandlerFunction? _afterHandlerFunction; @@ -27,10 +50,6 @@ class Commander with CommandRegistrableAbstract { final Logger _logger = Logger("Commander"); - /// Resolves prefix for given [message]. Returns null if there is no prefix for given [message] which - /// means command wouldn't execute in given context. - FutureOr getPrefixForMessage(IMessage message) => _prefixHandler(message); - /// Either [prefix] or [prefixHandler] must be specified otherwise program will exit. /// Allows to specify additional [beforeCommandHandler] executed before main command callback, /// and [afterCommandHandler] executed after main command callback. @@ -54,6 +73,10 @@ class Commander with CommandRegistrableAbstract { _logger.info("Commander ready!"); } + /// Resolves prefix for given [message]. Returns null if there is no prefix for given [message] which + /// means command wouldn't execute in given context. + FutureOr getPrefixForMessage(IMessage message) => _prefixHandler(message); + /// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { registerCommandEntity(CommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler)); From 294e945731b026e75e4cdc280ec90f4bf6b825f0 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 14 Nov 2021 00:10:35 +0100 Subject: [PATCH 21/25] Migrate to dart-lang/setup-dart@v1 action --- .github/workflows/deploy_docs.yml | 4 +--- .github/workflows/unit_tests.yml | 12 +++--------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 037edc2..883e8f8 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -17,9 +17,7 @@ jobs: key: ${{ runner.os }} - name: Setup Dart Action - uses: cedx/setup-dart@v2.3.0 - with: - release-channel: stable + uses: dart-lang/setup-dart@v1 - name: Checkout uses: actions/checkout@v2.3.4 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index ce2de7e..70ceb4a 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -19,9 +19,7 @@ jobs: key: ${{ runner.os }} - name: Setup Dart Action - uses: cedx/setup-dart@v2.3.0 - with: - release-channel: stable + uses: dart-lang/setup-dart@v1 - name: Checkout uses: actions/checkout@v2.3.4 @@ -45,9 +43,7 @@ jobs: key: ${{ runner.os }} - name: Setup Dart Action - uses: cedx/setup-dart@v2.3.0 - with: - release-channel: stable + uses: dart-lang/setup-dart@v1 - name: Checkout uses: actions/checkout@v2.3.4 @@ -72,9 +68,7 @@ jobs: key: ${{ runner.os }} - name: Setup Dart Action - uses: cedx/setup-dart@v2.3.0 - with: - release-channel: stable + uses: dart-lang/setup-dart@v1 - name: Install lcov run: sudo apt-get install -y lcov From b44696488c5d3c6f1e183f7c61419e4006fe7479 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 24 Nov 2021 20:45:22 +0100 Subject: [PATCH 22/25] Release: 3.0.0-dev.0 --- CHANGELOG.md | 12 ++++++++++++ example/example.dart | 6 +++++- example/per_guild_prefix.dart | 6 +++++- pubspec.yaml | 12 +++--------- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ce11f..5273c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 3.0.0-dev.0 +__24.11.2021__ + +- Implemented new interface-based entity model. + > All concrete implementations of entities are now hidden behind interfaces which exports only behavior which is + > intended for end developer usage. For example: User is now not exported and its interface `IUser` is available for developers. + > This change shouldn't have impact of end developers. +- Fix CommandGroups bugs not passing parameters down the entity tree + +Other changes are initial implementation of unit and integration tests to assure correct behavior of internal framework +processes. Also added `Makefile` with common commands that are run during development. + ## 2.0.0 _03.10.2021_ diff --git a/example/example.dart b/example/example.dart index e3d3550..e446add 100644 --- a/example/example.dart +++ b/example/example.dart @@ -3,7 +3,11 @@ import "package:nyxx_commander/nyxx_commander.dart"; void main() { // Start bot - final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); + final bot = NyxxFactory.createNyxxWebsocket("", GatewayIntents.allUnprivileged) + ..registerPlugin(Logging()) // Default logging plugin + ..registerPlugin(CliIntegration()) // Cli integration for nyxx allows stopping application via SIGTERM and SIGKILl + ..registerPlugin(IgnoreExceptions()) // Plugin that handles uncaught exceptions that may occur + ..connect(); // Start commander with prefix `!` ICommander.create(bot, mentionPrefixHandler) diff --git a/example/per_guild_prefix.dart b/example/per_guild_prefix.dart index 6ffb77e..6af7005 100644 --- a/example/per_guild_prefix.dart +++ b/example/per_guild_prefix.dart @@ -22,7 +22,11 @@ FutureOr prefixHandler(IMessage message) { void main() { // Start bot - final bot = NyxxFactory.createNyxxWebsocket("TOKEN", GatewayIntents.allUnprivileged); + final bot = NyxxFactory.createNyxxWebsocket("", GatewayIntents.allUnprivileged) + ..registerPlugin(Logging()) // Default logging plugin + ..registerPlugin(CliIntegration()) // Cli integration for nyxx allows stopping application via SIGTERM and SIGKILl + ..registerPlugin(IgnoreExceptions()) // Plugin that handles uncaught exceptions that may occur + ..connect(); // Start commander with prefix `!` ICommander.create(bot, prefixHandler) // prefixHandler will handle deciding which guild can use which prefix diff --git a/pubspec.yaml b/pubspec.yaml index 8886a5e..de70a10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,9 +10,9 @@ environment: sdk: '>=2.13.0 <3.0.0' dependencies: - http: "^0.13.3" - logging: "^1.0.1" - nyxx: "^2.0.0" + http: ^0.13.3 + logging: ^1.0.1 + nyxx: ^3.0.0-dev.0 dev_dependencies: build_runner: ^2.1.4 @@ -20,9 +20,3 @@ dev_dependencies: lints: ^1.0.1 mockito: ^5.0.16 test: ^1.19.0 - -dependency_overrides: - nyxx: - git: - url: 'https://github.com/nyxx-discord/nyxx.git' - ref: next From 84f6fdc9e274ca0f70cd1c36bbc1d611e4912fb8 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 28 Nov 2021 15:37:06 +0100 Subject: [PATCH 23/25] Update linter config; Fix lints --- analysis_options.yaml | 7 +++- lib/src/commander.dart | 3 ++ test/commander-test.dart | 86 ---------------------------------------- 3 files changed, 9 insertions(+), 87 deletions(-) delete mode 100644 test/commander-test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 1f18dab..53ec678 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,12 @@ include: package:lints/recommended.yaml +linter: + rules: + unrelated_type_equality_checks: false + implementation_imports: false + analyzer: - exclude: [build/**] + exclude: [build/**, example/**] language: strict-raw-types: true strong-mode: diff --git a/lib/src/commander.dart b/lib/src/commander.dart index 63c46fa..511ee3a 100644 --- a/lib/src/commander.dart +++ b/lib/src/commander.dart @@ -75,14 +75,17 @@ class Commander extends CommandRegistrableAbstract implements ICommander { /// Resolves prefix for given [message]. Returns null if there is no prefix for given [message] which /// means command wouldn't execute in given context. + @override FutureOr getPrefixForMessage(IMessage message) => _prefixHandler(message); /// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks + @override void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) { registerCommandEntity(CommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler)); } /// Registers command as implemented [CommandEntity] class + @override void registerCommandGroup(BasicCommandGroup commandGroup) => registerCommandEntity(commandGroup); Future _handleMessage(IMessageReceivedEvent event) async { diff --git a/test/commander-test.dart b/test/commander-test.dart deleted file mode 100644 index ac33f9f..0000000 --- a/test/commander-test.dart +++ /dev/null @@ -1,86 +0,0 @@ -import "dart:async"; -import "dart:io"; - -import "package:nyxx/nyxx.dart"; -import "package:nyxx_commander/nyxx_commander.dart"; - -void main() { - // final bot = Nyxx(Platform.environment["TEST_TOKEN"]!, GatewayIntents.allUnprivileged, ignoreExceptions: false); - // - // bot.onMessageReceived.listen((event) async { - // if (event.message.content == "Test 1") { - // event.message.delete(); // ignore: unawaited_futures - // } - // - // if (event.message.content == "Test 2") { - // event.message.delete(); // ignore: unawaited_futures - // } - // - // if (event.message.content == "Test 10") { - // event.message.delete(); // ignore: unawaited_futures - // } - // - // if (event.message.content == "Test 11") { - // await event.message.delete(); - // } - // - // if (event.message.content == "Test 12") { - // await event.message.delete(); - // - // await event.message.channel.getFromCache()?.sendMessage(MessageBuilder.content("Commander tests completed successfully!")); - // exit(0); - // } - // }); - // - // bot.onReady.listen((e) async { - // final channel = await bot.fetchChannel(Snowflake("846139169818017812")); - // - // await channel.sendMessage(MessageBuilder.content("Testing Commander")); - // - // final msg1 = await channel.sendMessage(MessageBuilder.content("test>test1")); - // msg1.delete(); // ignore: unawaited_futures - // - // final msg2 = await channel.sendMessage(MessageBuilder.content("test>test2 arg1")); - // msg2.delete(); // ignore: unawaited_futures - // - // final msg3 = await channel.sendMessage(MessageBuilder.content("test>test3")); - // msg3.delete(); // ignore: unawaited_futures - // - // final msg4 = await channel.sendMessage(MessageBuilder.content("test>test4")); - // msg4.delete(); // ignore: unawaited_futures - // - // final msg5 = await channel.sendMessage(MessageBuilder.content("test>test4 test5")); - // msg5.delete(); // ignore: unawaited_futures - // }); - // - // Commander(bot, prefix: "test>", beforeCommandHandler: (context) async { - // if (context.message.content.endsWith("test3")) { - // await context.channel.sendMessage(MessageBuilder.content("Test 10")); - // return true; - // } - // - // return true; - // }) - // ..registerCommand("test1", (context, message) async { - // await context.channel.sendMessage(MessageBuilder.content("Test 1")); - // }) - // ..registerCommand("test2", (context, message) async { - // final args = message.split(" "); - // - // if (args.length == 2 && args.last == "arg1") { - // await context.channel.sendMessage(MessageBuilder.content("Test 2")); - // } - // }) - // ..registerCommand("test3", (context, message) async { - // await context.message.delete(); - // }) - // ..registerCommandGroup(CommandGroup(name: "test4") - // ..registerDefaultCommand((context, message) => context.channel.sendMessage(MessageBuilder.content("Test 11"))) - // ..registerSubCommand("test5", (context, message) => context.channel.sendMessage(MessageBuilder.content("Test 12"))) - // ); - // - // Timer(const Duration(seconds: 60), () { - // print("Timed out waiting for messages"); - // exit(1); - // }); -} From 503d6653e4fde915d1697f0fadaf1264674d42be Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 16 Dec 2021 22:01:35 +0100 Subject: [PATCH 24/25] Update CI jobs --- .github/workflows/deploy_docs.yml | 22 +++++++++++----- .github/workflows/publish.yml | 13 ++++++++- .github/workflows/unit_tests.yml | 44 +++++++++++++++---------------- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 883e8f8..13c1c9d 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -4,30 +4,38 @@ on: push: branches: - dev + - next jobs: deploy-docs: runs-on: ubuntu-latest steps: - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.pub_cache - key: ${{ runner.os }} - - name: Setup Dart Action uses: dart-lang/setup-dart@v1 - name: Checkout uses: actions/checkout@v2.3.4 + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + - name: Install dependencies run: dart pub get - name: Generate docs run: dartdoc + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + - name: Deploy nyxx dev docs uses: easingthemes/ssh-deploy@v2.1.5 env: @@ -36,4 +44,4 @@ jobs: SOURCE: "doc/api/" REMOTE_HOST: ${{ secrets.DEPLOY_REMOTE_HOST }} REMOTE_USER: ${{ secrets.DEPLOY_REMOTE_USER }} - TARGET: "${{ secrets.DEPLOY_REMOTE_TARGET }}/dartdocs/nyxx_commander/" + TARGET: "${{ secrets.DEPLOY_REMOTE_TARGET }}/dartdocs/nyxx_commander/${{ steps.extract_branch.outputs.branch }}/" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fa3ee14..a20fe92 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,17 @@ jobs: nyxx_publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + - name: 'publish nyxx package to pub.dev' id: publish uses: k-paxian/dart-package-publisher@master @@ -18,6 +28,7 @@ jobs: force: true suppressBuildRunner: true credentialJson: ${{ secrets.CREDENTIAL_JSON }} + - name: 'Commit release tag' if: steps.publish.outputs.success uses: hole19/git-tag-action@master diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 70ceb4a..4a21e88 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -12,18 +12,19 @@ jobs: env: TEST_TOKEN: ${{ secrets.TEST_TOKEN }} steps: - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.pub_cache - key: ${{ runner.os }} - - name: Setup Dart Action uses: dart-lang/setup-dart@v1 - name: Checkout uses: actions/checkout@v2.3.4 + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- - name: Install dependencies run: dart pub get @@ -36,18 +37,19 @@ jobs: env: TEST_TOKEN: ${{ secrets.TEST_TOKEN }} steps: - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.pub_cache - key: ${{ runner.os }} - - name: Setup Dart Action uses: dart-lang/setup-dart@v1 - name: Checkout uses: actions/checkout@v2.3.4 + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- - name: Install dependencies run: dart pub get @@ -61,12 +63,6 @@ jobs: env: TEST_TOKEN: ${{ secrets.TEST_TOKEN }} steps: - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.pub_cache - key: ${{ runner.os }} - - name: Setup Dart Action uses: dart-lang/setup-dart@v1 @@ -76,16 +72,20 @@ jobs: - name: Checkout uses: actions/checkout@v2.3.4 + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + - name: Install dependencies run: dart pub get - name: Unit tests run: dart run test --coverage="coverage" test/unit/** - - name: Integration tests - run: dart run test --coverage="coverage" test/integration/** - continue-on-error: true - - name: Format coverage run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib From 353e92492700ba6166948cc8ec33dec793f355f3 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 19 Dec 2021 16:18:31 +0100 Subject: [PATCH 25/25] Release 3.0.0 --- CHANGELOG.md | 13 ++++++++++++- pubspec.yaml | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5273c79..7f376f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,21 @@ +## 3.0.0 + +- Implemented new interface-based entity model. + > All concrete implementations of entities are now hidden behind interfaces which exports only behavior which is + > intended for end developer usage. For example: User is now not exported and its interface `IUser` is available for developers. + > This change shouldn't have impact for end developers. +- Fix CommandGroups bugs not passing parameters down the entity tree + +Other changes are initial implementation of unit and integration tests to assure correct behavior of internal framework +processes. Also added `Makefile` with common commands that are run during development. + ## 3.0.0-dev.0 __24.11.2021__ - Implemented new interface-based entity model. > All concrete implementations of entities are now hidden behind interfaces which exports only behavior which is > intended for end developer usage. For example: User is now not exported and its interface `IUser` is available for developers. - > This change shouldn't have impact of end developers. + > This change shouldn't have impact for end developers. - Fix CommandGroups bugs not passing parameters down the entity tree Other changes are initial implementation of unit and integration tests to assure correct behavior of internal framework diff --git a/pubspec.yaml b/pubspec.yaml index de70a10..f9913fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx_commander -version: 3.0.0-dev.0 +version: 3.0.0 description: Nyxx Commander Module. Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx @@ -12,7 +12,7 @@ environment: dependencies: http: ^0.13.3 logging: ^1.0.1 - nyxx: ^3.0.0-dev.0 + nyxx: ^3.0.0 dev_dependencies: build_runner: ^2.1.4