diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..41bd5c6
--- /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_interactions 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/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
new file mode 100644
index 0000000..6cb92d2
--- /dev/null
+++ b/.github/workflows/deploy_docs.yml
@@ -0,0 +1,47 @@
+name: deploy dev docs
+
+on:
+ push:
+ branches:
+ - dev
+ - next
+
+jobs:
+ deploy-docs:
+ runs-on: ubuntu-latest
+
+ steps:
+ - 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:
+ 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_interactions/${{ steps.extract_branch.outputs.branch }}/"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..a20fe92
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,37 @@
+name: publish
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ nyxx_publish:
+ runs-on: ubuntu-latest
+ steps:
+ - 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
+ 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..e2521a3
--- /dev/null
+++ b/.github/workflows/unit_tests.yml
@@ -0,0 +1,110 @@
+name: unit tests
+
+on:
+ push:
+ branches-ignore:
+ - main
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ env:
+ TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
+ steps:
+ - 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: Analyze project source
+ run: dart analyze
+
+ format:
+ name: Format
+ runs-on: ubuntu-latest
+ env:
+ TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
+ steps:
+ - 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: 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: Setup Dart Action
+ uses: dart-lang/setup-dart@v1
+
+ - name: Install lcov
+ run: sudo apt-get install -y lcov
+
+ - 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: 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_interactions/${{ 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_interactions/${{ steps.extract_branch.outputs.branch }}/"
diff --git a/.gitignore b/.gitignore
index 11e6502..de985f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,36 +1,7 @@
-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
+**/coverage/**
.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/**
-old.*
+.dart_tool
+pubspec.lock
+.packages
+private_test.dart
+.vscode/
diff --git a/.packages b/.packages
deleted file mode 100644
index ebaf50b..0000000
--- a/.packages
+++ /dev/null
@@ -1,56 +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-10-04 16:59:03.827937.
-_fe_analyzer_shared:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-27.0.0/lib/
-analyzer:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/analyzer-2.4.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/
-charcode:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/
-cli_util:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/cli_util-0.3.4/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/
-file:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/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/
-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/
-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.0/lib/
-node_preamble:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/node_preamble-2.0.1/lib/
-nyxx:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/nyxx-2.0.2/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/
-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_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/
-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.18.2/lib/
-test_api:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.5/lib/
-test_core:file:///home/lusha/.pub-cache/hosted/pub.dartlang.org/test_core-0.4.5/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_interactions:lib/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fb3da3..f0b1f17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,55 @@
-## 2.0.2
-_02.11.2021_
+## 3.0.0
+__19.12.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.
+- Improved handling autocomplete
+ > Autocomplete can be now registered in CommandOptionBuilder. This allows registering multiple autocomplete handler for options
+ > with same names.
+- Fixed bugs with registering commands and command permissions. This feature should now work flawlessly.
+- Fix critical bug preventing commands from being registered
+- Fix #10
+
+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.2
+__10.12.2021__
+
+- Fix #10
+
+## 3.0.0-dev.1
+__04.12.2021__
+
+- Fix critical bug preventing commands from being registered
+
+## 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.
+- Improved handling autocomplete
+ > Autocomplete can be now registered in CommandOptionBuilder. This allows registering multiple autocomplete handler for options
+ > with same names.
+- Fixed bugs with registering commands and command permissions. This feature should now work flawlessly.
+
+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.3
+_03.11.2021_
- allow handlers on different nesting layers, closes nyxx#233 (664fd7cdab23ccbf037e4d29ead92178de7e7660) @abitofevrything
+## 2.0.2
+_15.10.2021_
+
+- Move to Apache 2 license
+
## 2.0.1
_03.10.2021_
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/Makefile b/Makefile
new file mode 100644
index 0000000..50bc561
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+.PHONY: help
+help: ## Shows this
+ @fgrep -h "##" $(MAKEFILE_LIST) | sed -e 's/\(\:.*\#\#\)/\:\ /' | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
+
+.PHONY: app-check
+app-check: format-check generate-coverage ## Run basic format checks and then generate code coverage
+
+.PHONY: format-check
+format-check: format analyze ## Check basic format
+
+.PHONY: generate-coverage
+generate-coverage: unit-tests coverage-format coverage-gen-html ## Run all test and generate html code coverage
+
+.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/README.md b/README.md
index 139a4fa..3e80ceb 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
# nyxx_interactions
-[![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/)
+[![Discord Shield](https://discordapp.com/api/guilds/846136758470443069/widget.png?style=shield)](https://discord.gg/nyxx)
+[![pub](https://img.shields.io/pub/v/nyxx_interactions.svg)](https://pub.dartlang.org/packages/nyxx_interactions)
+[![documentation](https://img.shields.io/badge/Documentation-nyxx_interactions-yellow.svg)](https://www.dartdocs.org/documentation/nyxx_interactions/latest/)
Simple, robust framework for creating discord bots for Dart language.
@@ -14,69 +12,55 @@ Simple, robust framework for creating discord bots for Dart language.
- **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.
+- **Buttons and dropdowns**
+- **Autocomplete**
+- **Context menus**
## 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.sendMessage(MessageBuilder.content("Pong!"));
- }
- });
-}
-```
-
Slash commands:
```dart
void main() {
- final bot = Nyxx("<%TOKEN%>", GatewayIntents.allUnprivileged);
- final interactions = Interactions(bot);
-
- interactions
- ..registerHandler("test", "This is test comamnd", [], handler: (event) async {
+ 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();
+
+ IInteractions.create(WebsocketInteractionBackend(bot))
+ ..registerHandler("test", "This is test command", [], handler: (event) async {
await event.reply(MessageBuilder.content("This is example message result"));
- });
+ })
+ ..syncOnReady();
}
```
-Commands:
-```dart
-void main() {
- final bot = Nyxx("TOKEN", GatewayIntents.allUnprivileged);
+## Other nyxx packages
- Commander(bot, prefix: "!!!")
- ..registerCommand("ping", (context, message) => context.reply(MessageBuilder.content("Pong!")));
-}
-```
+- [nyxx](https://github.com/nyxx-discord/nyxx)
+- [nyxx_commander](https://github.com/nyxx-discord/nyxx_commander)
+- [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)
## More examples
-Nyxx examples can be found [here](https://github.com/l7ssha/nyxx/tree/dev/nyxx/example).
-
-Commander examples can be found [here](https://github.com/l7ssha/nyxx/tree/dev/nyxx_commander/example)
-
-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_interactions/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_interactions/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 +68,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_interactions/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_interactions/blob/development/CONTRIBUTING.md)
## Credits
diff --git a/analysis_options.yaml b/analysis_options.yaml
index eec1199..53ec678 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,105 +1,13 @@
+include: package:lints/recommended.yaml
+
+linter:
+ rules:
+ unrelated_type_equality_checks: false
+ implementation_imports: false
+
analyzer:
+ exclude: [build/**, example/**]
+ 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
- - 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/example/basic.dart b/example/basic.dart
index 2fee54e..62b1bdf 100644
--- a/example/basic.dart
+++ b/example/basic.dart
@@ -1,7 +1,7 @@
import "dart:math";
import "package:nyxx/nyxx.dart";
-import "package:nyxx_interactions/interactions.dart";
+import "package:nyxx_interactions/nyxx_interactions.dart";
// Creates instance of slash command builder with name, description and sub options.
// Its used to synchronise commands with discord and also to be able to respond to them.
@@ -35,8 +35,13 @@ final subCommandFlipGame = CommandOptionBuilder(CommandOptionType.subCommand, "c
});
void main() {
- final bot = Nyxx("", GatewayIntents.allUnprivileged);
- Interactions(bot)
+ 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();
+
+ IInteractions.create(WebsocketInteractionBackend(bot))
..registerSlashCommand(singleCommand) // Register created before slash command
..syncOnReady(); // This is needed if you want to sync commands on bot startup.
}
diff --git a/example/buttons-and-dropdowns.dart b/example/buttons_and_dropdowns.dart
similarity index 82%
rename from example/buttons-and-dropdowns.dart
rename to example/buttons_and_dropdowns.dart
index 8ace1a5..4865a2e 100644
--- a/example/buttons-and-dropdowns.dart
+++ b/example/buttons_and_dropdowns.dart
@@ -1,5 +1,5 @@
import "package:nyxx/nyxx.dart";
-import "package:nyxx_interactions/interactions.dart";
+import "package:nyxx_interactions/nyxx_interactions.dart";
final singleCommand = SlashCommandBuilder("help", "This is example help command", [])
..registerHandler((event) async {
@@ -34,7 +34,7 @@ final singleCommand = SlashCommandBuilder("help", "This is example help command"
// slash command it needs to acknowledged and/or responded.
// If you know that command handler would take more that 3 second to complete
// you would need to acknowledge and then respond later with proper result.
-Future buttonHandler(ButtonInteractionEvent event) async {
+Future buttonHandler(IButtonInteractionEvent event) async {
await event.acknowledge(); // ack the interaction so we can send response later
// Send followup to button click with id of button
@@ -46,7 +46,7 @@ Future buttonHandler(ButtonInteractionEvent event) async {
// Handling multiselect events is no different from handling button.
// Only thing that changes is type of function argument -- it now passes information
// about values selected with multiselect
-Future multiselectHandlerHandler(MultiselectInteractionEvent event) async {
+Future multiselectHandlerHandler(IMultiselectInteractionEvent event) async {
await event.acknowledge(); // ack the interaction so we can send response later
// Send followup to button click with id of button
@@ -56,8 +56,13 @@ Future multiselectHandlerHandler(MultiselectInteractionEvent event) async
}
void main() {
- final bot = Nyxx("", GatewayIntents.allUnprivileged);
- Interactions(bot)
+ 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();
+
+ IInteractions.create(WebsocketInteractionBackend(bot))
..registerSlashCommand(singleCommand) // Register created before slash command
..registerButtonHandler("thisisid", buttonHandler) // register handler for button with id: thisisid
..registerMultiselectHandler("customId", multiselectHandlerHandler) // register handler for multiselect with id: customId
diff --git a/example/example.dart b/example/example.dart
index f8d706a..cec1ae9 100644
--- a/example/example.dart
+++ b/example/example.dart
@@ -1,9 +1,14 @@
import "package:nyxx/nyxx.dart";
-import "package:nyxx_interactions/interactions.dart";
+import "package:nyxx_interactions/nyxx_interactions.dart";
void main() {
- final bot = Nyxx("", GatewayIntents.allUnprivileged);
- Interactions(bot)
+ 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();
+
+ IInteractions.create(WebsocketInteractionBackend(bot))
..registerSlashCommand(
SlashCommandBuilder("itest", "This is test command", [
CommandOptionBuilder(CommandOptionType.subCommand, "subtest", "This is sub test")
diff --git a/example/private-test.dart b/example/private-test.dart
deleted file mode 100644
index c00f808..0000000
--- a/example/private-test.dart
+++ /dev/null
@@ -1,98 +0,0 @@
-import "dart:io";
-
-import "package:nyxx_interactions/interactions.dart";
-import "package:nyxx/nyxx.dart";
-
-void main() {
- final bot = Nyxx(Platform.environment["BOT_TOKEN"]!, GatewayIntents.allUnprivileged);
-
- Interactions(bot)
- // ..registerSlashCommand(SlashCommandBuilder("test", "This is command group test", [
- // CommandOptionBuilder(CommandOptionType.string, "group1", "this is subcommand group", autoComplete: true)
- // ], guild: 302360552993456135.toSnowflake()))
- ..registerSlashCommand(SlashCommandBuilder("testfiles", "This is command group test", [], guild: 302360552993456135.toSnowflake())
- ..registerHandler((p0) async {
- final builder = ComponentMessageBuilder()
- ..content = "this is content"
- ..addAttachment(AttachmentBuilder.path("/home/lusha/Pictures/dragraceundergambling.jpg"))
- ..addComponentRow(ComponentRowBuilder()
- ..addComponent(LinkButtonBuilder("This is discord link", "discord://-/channels/@me/"))
- );
- await p0.respond(builder, hidden: true);
- await p0.sendFollowup(builder);
- })
- )
- // ..registerAutocompleteHandler("group1", (event) => event.respond([ArgChoiceBuilder("test", "test")]))
- ..syncOnReady();
-
-// Interactions(bot)
-// ..registerSlashCommand(
-// SlashCommandBuilder("itest", "This is test command", [
-// CommandOptionBuilder(
-// CommandOptionType.subCommand, "testi", "This is testi", options: [CommandOptionBuilder(CommandOptionType.user, "user", "this is user")])
-// ..registerHandler((event) {
-// event.acknowledge();
-// event.respond(MessageBuilder.content("This is respond"));
-// })
-// // CommandOptionBuilder(
-// // CommandOptionType.subCommand, "testii", "This is testi")
-// // ..registerHandler((event) async {
-// // await event.acknowledge(hidden: true);
-// //
-// // await event.respond(MessageBuilder.content("This is respond1"));
-// // })
-// ], guild: 302360552993456135.toSnowflake())
-// )..registerSlashCommand(
-// SlashCommandBuilder(
-// "buttontest", "This is command for testing buttons", [],
-// guild: 302360552993456135.toSnowflake())
-// ..registerHandler((event) async {
-// await event.acknowledge();
-//
-// final select = MultiselectBuilder("testid", [
-// MultiselectOptionBuilder("Example label", "123")
-// ..emoji = UnicodeEmoji("💕"),
-// MultiselectOptionBuilder("Example label 2", "1233"),
-// ]);
-//
-// await event.respond(ComponentMessageBuilder()
-// ..content = "Buttons"
-// ..components = [
-// [
-// ButtonBuilder(
-// "Disappearing button", "testowyu", ComponentStyle.danger)
-// ],
-// [select]
-// ]);
-// })
-// )
-// ..registerButtonHandler("testowyu", (event) async {
-// // await event.respond(ButtonMessageBuilder()..buttons = []..content = "new content");
-// await event.acknowledge();
-// await event.respond(ComponentMessageBuilder()
-// ..content = "This is test if it works"
-// ..components = []);
-// })
-// ..registerMultiselectHandler("testid", (event) async {
-// await event.respond(MessageBuilder.content("Responded"));
-// })
-// ..syncOnReady();
-// // ..registerHandler("reply", "This is test command",
-// // [CommandOptionBuilder(CommandOptionType.subCommand, "test1", "This is description"), CommandOptionBuilder(CommandOptionType.subCommand, "test2", "this is test")], guild: 302360552993456135.toSnowflake(),
-// // handler: (event) async {
-// // await event.acknowledge();
-// //
-// // final subCommand = event.subCommand;
-// //
-// // if (subCommand == null) {
-// // return;
-// // }
-// //
-// // Future.delayed(const Duration(seconds: 10), () async {
-// // await event.respond(content: subCommand.name, hidden: true);
-// //
-// // await event.sendFollowup(content: "Test followup");
-// // });
-// // });
-// // }
-}
diff --git a/lib/interactions.dart b/lib/interactions.dart
deleted file mode 100644
index 316a49d..0000000
--- a/lib/interactions.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-library nyxx_interactions;
-
-import "dart:async";
-import "dart:collection";
-import "dart:convert";
-import "dart:io";
-
-import "package:crypto/crypto.dart";
-import "package:logging/logging.dart";
-import "package:nyxx/nyxx.dart";
-
-// Root
-part "src/Interactions.dart";
-// Builders
-part "src/builders/ArgChoiceBuilder.dart";
-part "src/builders/CommandOptionBuilder.dart";
-part "src/builders/CommandPermissionBuilder.dart";
-part "src/builders/ComponentBuilder.dart";
-part "src/builders/SlashCommandBuilder.dart";
-// Events
-part "src/events/InteractionEvent.dart";
-part "src/exceptions/AlreadyResponded.dart";
-// Exceptions
-part "src/exceptions/InteractionExpired.dart";
-part "src/exceptions/ResponseRequired.dart";
-// Internal
-part "src/internal/_EventController.dart";
-part "src/internal/InteractionEndpoints.dart";
-// Sync
-part "src/internal/sync/ICommandsSync.dart";
-part "src/internal/sync/LockFileCommandSync.dart";
-part "src/internal/sync/ManualCommandSync.dart";
-// Utils
-part "src/internal/utils.dart";
-// Command Args
-part "src/models/ArgChoice.dart";
-part "src/models/CommandOption.dart";
-part "src/models/Interaction.dart";
-part "src/models/InteractionDataResolved.dart";
-part "src/models/InteractionOption.dart";
-// Models
-part "src/models/SlashCommand.dart";
-part "src/models/SlashCommandType.dart";
-
-/// Typedef of api response
-typedef RawApiMap = Map;
diff --git a/lib/nyxx_interactions.dart b/lib/nyxx_interactions.dart
new file mode 100644
index 0000000..2e9e89a
--- /dev/null
+++ b/lib/nyxx_interactions.dart
@@ -0,0 +1,41 @@
+library nyxx_interactions;
+
+export 'src/builders/arg_choice_builder.dart' show ArgChoiceBuilder;
+export 'src/builders/command_option_builder.dart' show CommandOptionBuilder;
+export 'src/builders/command_permission_builder.dart' show CommandPermissionBuilderAbstract, RoleCommandPermissionBuilder, UserCommandPermissionBuilder;
+export 'src/builders/component_builder.dart'
+ show ComponentMessageBuilder, ComponentRowBuilder, LinkButtonBuilder, ButtonBuilder, MultiselectBuilder, MultiselectOptionBuilder;
+export 'src/builders/slash_command_builder.dart' show SlashCommandBuilder;
+export 'src/events/interaction_event.dart'
+ show
+ IInteractionEventWithAcknowledge,
+ IInteractionEvent,
+ IAutocompleteInteractionEvent,
+ IButtonInteractionEvent,
+ IComponentInteractionEvent,
+ IMultiselectInteractionEvent,
+ InteractionEventAbstract,
+ InteractionEventWithAcknowledge,
+ ISlashCommandInteractionEvent;
+export 'src/exceptions/already_responded.dart' show AlreadyRespondedError;
+export 'src/exceptions/interaction_expired.dart' show InteractionExpiredError;
+export 'src/exceptions/response_required.dart' show ResponseRequiredError;
+export 'src/internal/sync/commands_sync.dart' show ICommandsSync;
+export 'src/internal/sync/lock_file_command_sync.dart' show LockFileCommandSync;
+export 'src/internal/sync/manual_command_sync.dart' show ManualCommandSync;
+export 'src/internal/event_controller.dart' show IEventController;
+export 'src/internal/interaction_endpoints.dart' show IInteractionsEndpoints;
+export 'src/internal/utils.dart' show slashCommandNameRegex;
+export 'src/models/arg_choice.dart' show IArgChoice;
+export 'src/models/command_option.dart' show ICommandOption, CommandOptionType;
+export 'src/models/interaction.dart' show IComponentInteraction, IInteraction, IButtonInteraction, IMultiselectInteraction, ISlashCommandInteraction;
+export 'src/models/interaction_data_resolved.dart' show IInteractionDataResolved, IPartialChannel;
+export 'src/models/interaction_option.dart' show IInteractionOption;
+export 'src/models/slash_command.dart' show ISlashCommand;
+export 'src/models/slash_command_type.dart' show SlashCommandType;
+
+export 'src/interactions.dart' show IInteractions;
+export 'src/typedefs.dart' show AutocompleteInteractionHandler, ButtonInteractionHandler, MultiselectInteractionHandler, SlashCommandHandler;
+
+export 'src/backend/interaction_backend.dart' show InteractionBackend;
+export 'src/backend/nyxx_backend.dart' show WebsocketInteractionBackend;
diff --git a/lib/src/Interactions.dart b/lib/src/Interactions.dart
deleted file mode 100644
index f405c3e..0000000
--- a/lib/src/Interactions.dart
+++ /dev/null
@@ -1,284 +0,0 @@
-part of nyxx_interactions;
-
-/// Function that will handle execution of slash command interaction event
-typedef SlashCommandHandler = FutureOr Function(SlashCommandInteractionEvent);
-
-/// Function that will handle execution of button interaction event
-typedef ButtonInteractionHandler = FutureOr Function(ButtonInteractionEvent);
-
-/// Function that will handle execution of dropdown event
-typedef MultiselectInteractionHandler = FutureOr Function(MultiselectInteractionEvent);
-
-/// Function that will handle execution of button interaction event
-typedef AutocompleteInteractionHandler = FutureOr Function(AutocompleteInteractionEvent);
-
-/// Interaction extension for Nyxx. Allows use of: Slash Commands.
-class Interactions {
- static const _interactionCreateCommand = "INTERACTION_CREATE";
-
- late final _EventController _events;
- final Logger _logger = Logger("Interactions");
-
- /// Reference to client
- final Nyxx _client;
-
- final _commandBuilders = [];
- final _commands = [];
- final _commandHandlers = {};
- final _buttonHandlers = {};
- final _autocompleteHandlers = {};
- final _multiselectHandlers = {};
-
- /// Commands registered by bot
- Iterable get commands => UnmodifiableListView(this._commands);
-
- /// Emitted when a slash command is sent.
- late final Stream onSlashCommand;
-
- /// Emitted when a button interaction is received.
- late final Stream onButtonEvent;
-
- /// Emitted when a dropdown interaction is received.
- late final Stream onMultiselectEvent;
-
- /// Emitted when a slash command is created by the user.
- late final Stream onSlashCommandCreated;
-
- /// Emitted when a slash command is created by the user.
- late final Stream onAutocompleteEvent;
-
- /// All interaction endpoints that can be accessed.
- late final IInteractionsEndpoints interactionsEndpoints;
-
- /// Create new instance of the interactions class.
- Interactions(this._client) {
- _events = _EventController(this);
- _client.options.dispatchRawShardEvent = true;
- this.interactionsEndpoints = _InteractionsEndpoints(_client);
-
- _logger.info("Interactions ready");
-
- _client.onReady.listen((event) async {
- _client.shardManager.rawEvent.listen((event) {
- if (event.rawData["op"] == OPCodes.dispatch && event.rawData["t"] == _interactionCreateCommand) {
- this._logger.fine("Received interaction event: [${event.rawData}]");
-
- final type = event.rawData["d"]["type"] as int;
-
- switch (type) {
- case 2:
- _events.onSlashCommand.add(SlashCommandInteractionEvent._new(this, event.rawData["d"] as RawApiMap));
- break;
- case 3:
- final componentType = event.rawData["d"]["data"]["component_type"] as int;
-
- switch (componentType) {
- case 2:
- _events.onButtonEvent
- .add(ButtonInteractionEvent._new(this, event.rawData["d"] as Map));
- break;
- case 3:
- _events.onMultiselectEvent
- .add(MultiselectInteractionEvent._new(this, event.rawData["d"] as Map));
- break;
- default:
- this
- ._logger
- .warning("Unknown componentType type: [$componentType]; Payload: ${jsonEncode(event.rawData)}");
- }
- break;
- case 4:
- _events.onAutocompleteEvent
- .add(AutocompleteInteractionEvent._new(this, event.rawData["d"] as Map));
- break;
- default:
- this._logger.warning("Unknown interaction type: [$type]; Payload: ${jsonEncode(event.rawData)}");
- }
- }
- });
- });
- }
-
- /// Syncs commands builders with discord after client is ready.
- void syncOnReady({ICommandsSync syncRule = const ManualCommandSync()}) {
- this._client.onReady.listen((_) async {
- await this.sync(syncRule: syncRule);
- });
- }
-
- /// Syncs command builders with discord immediately.
- /// Warning: Client could not be ready at the function execution.
- /// Use [syncOnReady] for proper behavior
- Future sync({ICommandsSync syncRule = const ManualCommandSync()}) async {
- if (!await syncRule.shouldSync(this._commandBuilders)) {
- return;
- }
-
- final commandPartition = _partition(this._commandBuilders, (element) => element.guild == null);
- final globalCommands = commandPartition.first;
- final groupedGuildCommands = _groupSlashCommandBuilders(commandPartition.last);
-
- final globalCommandsResponse = await this.interactionsEndpoints
- .bulkOverrideGlobalCommands(this._client.app.id, globalCommands)
- .toList();
-
- _extractCommandIds(globalCommandsResponse);
- this._registerCommandHandlers(globalCommandsResponse, globalCommands);
-
- for(final entry in groupedGuildCommands.entries) {
- final response = await this.interactionsEndpoints
- .bulkOverrideGuildCommands(this._client.app.id, entry.key, entry.value)
- .toList();
-
- _extractCommandIds(response);
- this._registerCommandHandlers(response, entry.value);
- await this.interactionsEndpoints.bulkOverrideGuildCommandsPermissions(this._client.app.id, entry.key, entry.value);
- }
-
- this._commandBuilders.clear(); // Cleanup after registering command since we don't need this anymore
- this._logger.info("Finished bulk overriding slash commands and permissions");
-
- if (this._commands.isNotEmpty) {
- this.onSlashCommand.listen((event) async {
- final commandHash = _determineInteractionCommandHandler(event.interaction);
-
- this._logger.info("Executing command with hash [$commandHash]");
- if (this._commandHandlers.containsKey(commandHash)) {
- await this._commandHandlers[commandHash]!(event);
- }
- });
-
- this._logger.info("Finished registering ${this._commandHandlers.length} commands!");
- }
-
- if (this._buttonHandlers.isNotEmpty) {
- this.onButtonEvent.listen((event) {
- if (this._buttonHandlers.containsKey(event.interaction.customId)) {
- this._logger.info("Executing button with id [${event.interaction.customId}]");
- this._buttonHandlers[event.interaction.customId]!(event);
- } else {
- this._logger.warning("Received event for unknown button: ${event.interaction.customId}");
- }
- });
- }
-
- if (this._multiselectHandlers.isNotEmpty) {
- this.onMultiselectEvent.listen((event) {
- if (this._multiselectHandlers.containsKey(event.interaction.customId)) {
- this._logger.info("Executing multiselect with id [${event.interaction.customId}]");
- this._multiselectHandlers[event.interaction.customId]!(event);
- } else {
- this._logger.warning("Received event for unknown dropdown: ${event.interaction.customId}");
- }
- });
- }
-
- if (this._autocompleteHandlers.isNotEmpty) {
- this.onAutocompleteEvent.listen((event) {
- final name = event.focusedOption.name;
-
- if (this._autocompleteHandlers.containsKey(name)) {
- this._logger.info("Executing autocomplete with id [$name]");
- this._autocompleteHandlers[name]!(event);
- } else {
- this._logger.warning("Received event for unknown dropdown: $name");
- }
- });
- }
- }
-
- /// Registers callback for button event for given [id]
- void registerAutocompleteHandler(String id, AutocompleteInteractionHandler handler) => this._autocompleteHandlers[id] = handler;
-
- /// Registers callback for button event for given [id]
- void registerButtonHandler(String id, ButtonInteractionHandler handler) => this._buttonHandlers[id] = handler;
-
- /// Register callback for dropdown event for given [id]
- void registerMultiselectHandler(String id, MultiselectInteractionHandler handler) =>
- this._multiselectHandlers[id] = handler;
-
- /// Allows to register new [SlashCommandBuilder]
- void registerSlashCommand(SlashCommandBuilder slashCommandBuilder) => this._commandBuilders.add(slashCommandBuilder);
-
- /// Register callback for slash command event for given [id]
- void registerSlashCommandHandler(String id, SlashCommandHandler handler) =>
- this._commandHandlers[id] = handler;
-
- /// Deletes global command
- Future deleteGlobalCommand(Snowflake commandId) =>
- this.interactionsEndpoints.deleteGlobalCommand(this._client.app.id, commandId);
-
- /// Deletes guild command
- Future deleteGuildCommand(Snowflake commandId, Snowflake guildId) =>
- this.interactionsEndpoints.deleteGuildCommand(this._client.app.id, commandId, guildId);
-
- /// Fetches all global bots command
- Stream fetchGlobalCommands() =>
- this.interactionsEndpoints.fetchGlobalCommands(this._client.app.id);
-
- /// Fetches all guild commands for given guild
- Stream fetchGuildCommands(Snowflake guildId) =>
- this.interactionsEndpoints.fetchGuildCommands(this._client.app.id, guildId);
-
- void _extractCommandIds(List commands) {
- for (final slashCommand in commands) {
- this._commandBuilders
- .firstWhere((element) => element.name == slashCommand.name && element.guild == slashCommand.guild?.id)
- ._setId(slashCommand.id);
- }
- }
-
- void _registerCommandHandlers(List registeredSlashCommands, Iterable builders) {
- for(final registeredCommand in registeredSlashCommands) {
- final matchingBuilder = builders.firstWhere((element) => element.name.toLowerCase() == registeredCommand.name);
- this._assignCommandToHandler(matchingBuilder, registeredCommand);
-
- this._commands.add(registeredCommand);
- }
- }
-
- void _assignCommandToHandler(SlashCommandBuilder builder, SlashCommand command) {
- final commandHashPrefix = "${command.id}|${command.name}";
-
- var allowRootHandler = true;
-
- final subCommands = builder.options.where((element) => element.type == CommandOptionType.subCommand);
- if (subCommands.isNotEmpty) {
- for (final subCommand in subCommands) {
- if (subCommand._handler == null) {
- continue;
- }
-
- this._commandHandlers["$commandHashPrefix|${subCommand.name}"] = subCommand._handler!;
- }
-
- allowRootHandler = false;
- }
-
- final subCommandGroups = builder.options.where((element) => element.type == CommandOptionType.subCommandGroup);
- if (subCommandGroups.isNotEmpty) {
- for (final subCommandGroup in subCommandGroups) {
- final subCommands =
- subCommandGroup.options?.where((element) => element.type == CommandOptionType.subCommand) ?? [];
-
- for (final subCommand in subCommands) {
- if (subCommand._handler == null) {
- continue;
- }
-
- this._commandHandlers["$commandHashPrefix|${subCommandGroup.name}|${subCommand.name}"] = subCommand._handler!;
- }
- }
-
- allowRootHandler = false;
- }
-
- if (!allowRootHandler) {
- return;
- }
-
- if (builder._handler != null) {
- this._commandHandlers[commandHashPrefix] = builder._handler!;
- }
- }
-}
diff --git a/lib/src/backend/interaction_backend.dart b/lib/src/backend/interaction_backend.dart
new file mode 100644
index 0000000..6fd7c47
--- /dev/null
+++ b/lib/src/backend/interaction_backend.dart
@@ -0,0 +1,16 @@
+import 'dart:async';
+
+import 'package:nyxx/nyxx.dart';
+
+typedef ApiData = Map;
+
+abstract class InteractionBackend {
+ INyxx get client;
+
+ void setup();
+
+ Stream getStream();
+ StreamController getStreamController();
+
+ Stream getReadyStream();
+}
diff --git a/lib/src/backend/nyxx_backend.dart b/lib/src/backend/nyxx_backend.dart
new file mode 100644
index 0000000..db9b7e8
--- /dev/null
+++ b/lib/src/backend/nyxx_backend.dart
@@ -0,0 +1,35 @@
+import 'dart:async';
+
+import 'package:nyxx/nyxx.dart';
+
+import 'interaction_backend.dart';
+
+class WebsocketInteractionBackend implements InteractionBackend {
+ @override
+ INyxxWebsocket client;
+
+ late final StreamController _streamController;
+ late final Stream _stream;
+
+ WebsocketInteractionBackend(this.client);
+
+ @override
+ void setup() {
+ _streamController = StreamController.broadcast();
+ _stream = _streamController.stream;
+
+ client.options.dispatchRawShardEvent = true;
+ getReadyStream().listen((_) {
+ client.shardManager.rawEvent.map((event) => event.rawData).pipe(_streamController);
+ });
+ }
+
+ @override
+ Stream getStream() => _stream;
+
+ @override
+ StreamController getStreamController() => _streamController;
+
+ @override
+ Stream getReadyStream() => client.eventsWs.onReady;
+}
diff --git a/lib/src/builders/ArgChoiceBuilder.dart b/lib/src/builders/arg_choice_builder.dart
similarity index 83%
rename from lib/src/builders/ArgChoiceBuilder.dart
rename to lib/src/builders/arg_choice_builder.dart
index a872a22..15fc058 100644
--- a/lib/src/builders/ArgChoiceBuilder.dart
+++ b/lib/src/builders/arg_choice_builder.dart
@@ -1,4 +1,4 @@
-part of nyxx_interactions;
+import 'package:nyxx/nyxx.dart';
/// A specified choice for a slash command argument.
class ArgChoiceBuilder extends Builder {
@@ -16,5 +16,6 @@ class ArgChoiceBuilder extends Builder {
}
}
- RawApiMap build() => { "name": this.name, "value": this.value };
+ @override
+ RawApiMap build() => {"name": name, "value": value};
}
diff --git a/lib/src/builders/CommandOptionBuilder.dart b/lib/src/builders/command_option_builder.dart
similarity index 58%
rename from lib/src/builders/CommandOptionBuilder.dart
rename to lib/src/builders/command_option_builder.dart
index 250b25e..bf2f23a 100644
--- a/lib/src/builders/CommandOptionBuilder.dart
+++ b/lib/src/builders/command_option_builder.dart
@@ -1,4 +1,8 @@
-part of nyxx_interactions;
+import 'package:nyxx/nyxx.dart';
+
+import 'package:nyxx_interactions/src/builders/arg_choice_builder.dart';
+import 'package:nyxx_interactions/src/models/command_option.dart';
+import 'package:nyxx_interactions/src/typedefs.dart';
/// An argument for a [SlashCommandBuilder].
class CommandOptionBuilder extends Builder {
@@ -37,34 +41,44 @@ class CommandOptionBuilder extends Builder {
List? channelTypes;
/// Set to true if option should be autocompleted
- bool? autoComplete;
+ bool autoComplete;
+
+ SlashCommandHandler? handler;
- SlashCommandHandler? _handler;
+ AutocompleteInteractionHandler? autocompleteHandler;
/// Used to create an argument for a [SlashCommandBuilder].
CommandOptionBuilder(this.type, this.name, this.description,
- {this.defaultArg = false, this.required = false, this.choices, this.options,
- this.channelTypes, this.autoComplete});
+ {this.defaultArg = false, this.required = false, this.choices, this.options, this.channelTypes, this.autoComplete = false});
/// Registers handler for subcommand
void registerHandler(SlashCommandHandler handler) {
- if (this.type != CommandOptionType.subCommand) {
+ if (type != CommandOptionType.subCommand) {
throw StateError("Cannot register handler for command option with type other that subcommand");
}
- this._handler = handler;
+ this.handler = handler;
+ }
+
+ void registerAutocompleteHandler(AutocompleteInteractionHandler handler) {
+ if (choices != null || options != null) {
+ throw ArgumentError("Autocomplete cannot be set if choices or options are present");
+ }
+
+ autocompleteHandler = handler;
+ autoComplete = true;
}
+ @override
RawApiMap build() => {
- "type": this.type.value,
- "name": this.name,
- "description": this.description,
- "default": this.defaultArg,
- "required": this.required,
- if (this.choices != null) "choices": this.choices!.map((e) => e.build()).toList(),
- if (this.options != null) "options": this.options!.map((e) => e.build()).toList(),
- if (this.channelTypes != null && this.type == CommandOptionType.channel)
- "channel_types": channelTypes!.map((e) => e.value).toList(),
- if (this.autoComplete != null) "autocomplete": this.autoComplete,
- };
+ "type": type.value,
+ "name": name,
+ "description": description,
+ "default": defaultArg,
+ "required": required,
+ if (choices != null) "choices": choices!.map((e) => e.build()).toList(),
+ if (options != null) "options": options!.map((e) => e.build()).toList(),
+ if (channelTypes != null && type == CommandOptionType.channel) "channel_types": channelTypes!.map((e) => e.value).toList(),
+ "autocomplete": autoComplete,
+ };
}
diff --git a/lib/src/builders/CommandPermissionBuilder.dart b/lib/src/builders/command_permission_builder.dart
similarity index 52%
rename from lib/src/builders/CommandPermissionBuilder.dart
rename to lib/src/builders/command_permission_builder.dart
index 91c59dd..ee514a1 100644
--- a/lib/src/builders/CommandPermissionBuilder.dart
+++ b/lib/src/builders/command_permission_builder.dart
@@ -1,8 +1,8 @@
-part of nyxx_interactions;
+import 'package:nyxx/nyxx.dart';
/// Used to define permissions for a particular command.
-abstract class ICommandPermissionBuilder extends Builder {
- late final int _type;
+abstract class CommandPermissionBuilderAbstract extends Builder {
+ int get type;
/// The ID of the Role or User to give permissions too
final Snowflake id;
@@ -10,37 +10,35 @@ abstract class ICommandPermissionBuilder extends Builder {
/// Does the role have permission to use the command
final bool hasPermission;
- ICommandPermissionBuilder(this.id, {this.hasPermission = true});
+ CommandPermissionBuilderAbstract(this.id, {this.hasPermission = true});
/// A permission for a single user that can be used in [SlashCommandBuilder]
- factory ICommandPermissionBuilder.user(Snowflake id, {bool hasPermission = true}) =>
- UserCommandPermissionBuilder(id, hasPermission: hasPermission);
+ factory CommandPermissionBuilderAbstract.user(Snowflake id, {bool hasPermission = true}) => UserCommandPermissionBuilder(id, hasPermission: hasPermission);
/// A permission for a single role that can be used in [SlashCommandBuilder]
- factory ICommandPermissionBuilder.role(Snowflake id, {bool hasPermission = true}) =>
- RoleCommandPermissionBuilder(id, hasPermission: hasPermission);
+ factory CommandPermissionBuilderAbstract.role(Snowflake id, {bool hasPermission = true}) => RoleCommandPermissionBuilder(id, hasPermission: hasPermission);
}
/// A permission for a single role that can be used in [SlashCommandBuilder]
-class RoleCommandPermissionBuilder extends ICommandPermissionBuilder {
+class RoleCommandPermissionBuilder extends CommandPermissionBuilderAbstract {
@override
- late final int _type = 1;
+ late final int type = 1;
/// A permission for a single role that can be used in [SlashCommandBuilder]
RoleCommandPermissionBuilder(Snowflake id, {bool hasPermission = true}) : super(id, hasPermission: hasPermission);
@override
- RawApiMap build() => {"id": this.id.toString(), "type": this._type, "permission": this.hasPermission};
+ RawApiMap build() => {"id": id.toString(), "type": type, "permission": hasPermission};
}
/// A permission for a single user that can be used in [SlashCommandBuilder]
-class UserCommandPermissionBuilder extends ICommandPermissionBuilder {
+class UserCommandPermissionBuilder extends CommandPermissionBuilderAbstract {
@override
- late final int _type = 2;
+ late final int type = 2;
/// A permission for a single user that can be used in [SlashCommandBuilder]
UserCommandPermissionBuilder(Snowflake id, {bool hasPermission = true}) : super(id, hasPermission: hasPermission);
@override
- RawApiMap build() => {"id": this.id.toString(), "type": this._type, "permission": this.hasPermission};
+ RawApiMap build() => {"id": id.toString(), "type": type, "permission": hasPermission};
}
diff --git a/lib/src/builders/ComponentBuilder.dart b/lib/src/builders/component_builder.dart
similarity index 52%
rename from lib/src/builders/ComponentBuilder.dart
rename to lib/src/builders/component_builder.dart
index 5565ac6..3819324 100644
--- a/lib/src/builders/ComponentBuilder.dart
+++ b/lib/src/builders/component_builder.dart
@@ -1,14 +1,14 @@
-part of nyxx_interactions;
+import 'package:nyxx/nyxx.dart';
/// Allows to create components
-abstract class IComponentBuilder extends Builder {
+abstract class ComponentBuilderAbstract extends Builder {
/// Type of component
ComponentType get type;
@override
Map build() => {
- "type": this.type.value,
- };
+ "type": type.value,
+ };
}
/// Allows to create multi select option for [MultiselectBuilder]
@@ -33,20 +33,21 @@ class MultiselectOptionBuilder extends Builder {
@override
RawApiMap build() => {
- "label": this.label,
- "value": this.value,
- "default": this.isDefault,
- if (this.emoji != null) "emoji": {
- if (this.emoji is IGuildEmoji) "id": (this.emoji as IGuildEmoji).id,
- if (this.emoji is UnicodeEmoji) "name": (this.emoji as UnicodeEmoji).code,
- if (this.emoji is GuildEmoji) "animated": (this.emoji as GuildEmoji).animated,
- },
- if (description != null) "description": this.description,
- };
+ "label": label,
+ "value": value,
+ "default": isDefault,
+ if (emoji != null)
+ "emoji": {
+ if (emoji is IBaseGuildEmoji) "id": (emoji as IBaseGuildEmoji).id.toString(),
+ if (emoji is UnicodeEmoji) "name": (emoji as UnicodeEmoji).code,
+ if (emoji is IGuildEmoji) "animated": (emoji as IGuildEmoji).animated,
+ },
+ if (description != null) "description": description,
+ };
}
/// Allows to create multi select interactive components.
-class MultiselectBuilder extends IComponentBuilder {
+class MultiselectBuilder extends ComponentBuilderAbstract {
@override
ComponentType get type => ComponentType.select;
@@ -69,7 +70,7 @@ class MultiselectBuilder extends IComponentBuilder {
/// Creates instance of [MultiselectBuilder]
MultiselectBuilder(this.customId, [Iterable? options]) {
- if (this.customId.length > 100) {
+ if (customId.length > 100) {
throw ArgumentError("Custom Id for Select cannot have more than 100 characters");
}
@@ -79,24 +80,21 @@ class MultiselectBuilder extends IComponentBuilder {
}
/// Adds option to dropdown
- void addOption(MultiselectOptionBuilder builder) => this.options.add(builder);
+ void addOption(MultiselectOptionBuilder builder) => options.add(builder);
@override
Map build() => {
- ...super.build(),
- "custom_id": this.customId,
- "options": [
- for (final optionBuilder in this.options)
- optionBuilder.build()
- ],
- if (placeholder != null) "placeholder": this.placeholder,
- if (minValues != null) "min_values": this.minValues,
- if (maxValues != null) "max_values": this.maxValues,
- };
+ ...super.build(),
+ "custom_id": customId,
+ "options": [for (final optionBuilder in options) optionBuilder.build()],
+ if (placeholder != null) "placeholder": placeholder,
+ if (minValues != null) "min_values": minValues,
+ if (maxValues != null) "max_values": maxValues,
+ };
}
/// Allows to build button. Generic interface for all types of buttons
-abstract class IButtonBuilder extends IComponentBuilder {
+abstract class ButtonBuilderAbstract extends ComponentBuilderAbstract {
@override
ComponentType get type => ComponentType.button;
@@ -112,120 +110,99 @@ abstract class IButtonBuilder extends IComponentBuilder {
/// Additional emoji for button
IEmoji? emoji;
- /// Creates instance of [IButtonBuilder]
- IButtonBuilder(this.label, this.style, {this.disabled = false, this.emoji}) {
- if (this.label.length > 80) {
+ /// Creates instance of [ButtonBuilderAbstract]
+ ButtonBuilderAbstract(this.label, this.style, {this.disabled = false, this.emoji}) {
+ if (label.length > 80) {
throw ArgumentError("Label for Button cannot have more than 80 characters");
}
}
@override
Map build() => {
- ...super.build(),
- "label": this.label,
- "style": this.style.value,
- if (this.disabled) "disabled": true,
- if (this.emoji != null) "emoji": {
- if (this.emoji is IGuildEmoji) "id": (this.emoji as IGuildEmoji).id,
- if (this.emoji is UnicodeEmoji) "name": (this.emoji as UnicodeEmoji).code,
- if (this.emoji is GuildEmoji) "animated": (this.emoji as GuildEmoji).animated,
- }
- };
+ ...super.build(),
+ "label": label,
+ "style": style.value,
+ if (disabled) "disabled": true,
+ if (emoji != null)
+ "emoji": {
+ if (emoji is IGuildEmoji) "id": (emoji as IGuildEmoji).id,
+ if (emoji is UnicodeEmoji) "name": (emoji as UnicodeEmoji).code,
+ if (emoji is IGuildEmoji) "animated": (emoji as IGuildEmoji).animated,
+ }
+ };
}
/// Allows to create a button with link
-class LinkButtonBuilder extends IButtonBuilder {
+class LinkButtonBuilder extends ButtonBuilderAbstract {
/// Url where his button should redirect
final String url;
/// Creates instance of [LinkButtonBuilder]
- LinkButtonBuilder(
- String label,
- this.url,
- {bool disabled = false,
- IEmoji? emoji
- }): super(label, ComponentStyle.link, disabled: disabled, emoji: emoji
- ) {
- if (this.url.length > 512) {
+ LinkButtonBuilder(String label, this.url, {bool disabled = false, IEmoji? emoji}) : super(label, ComponentStyle.link, disabled: disabled, emoji: emoji) {
+ if (url.length > 512) {
throw ArgumentError("Url for button cannot have more than 512 characters");
}
}
@override
- RawApiMap build() => {
- ...super.build(),
- "url": url
- };
+ RawApiMap build() => {...super.build(), "url": url};
}
/// Button which will generate event when clicked.
-class ButtonBuilder extends IButtonBuilder {
+class ButtonBuilder extends ButtonBuilderAbstract {
/// Id with optional additional metadata for button.
String customId;
/// Creates instance of [ButtonBuilder]
- ButtonBuilder(
- String label,
- this.customId,
- ComponentStyle style,
- {bool disabled = false,
- IEmoji? emoji
- }) : super(label, style, disabled: disabled, emoji: emoji
- ) {
- if (this.label.length > 100) {
+ ButtonBuilder(String label, this.customId, ComponentStyle style, {bool disabled = false, IEmoji? emoji})
+ : super(label, style, disabled: disabled, emoji: emoji) {
+ if (customId.length > 100) {
throw ArgumentError("customId for button cannot have more than 100 characters");
}
}
@override
- RawApiMap build() => {
- ...super.build(),
- "custom_id": customId
- };
+ RawApiMap build() => {...super.build(), "custom_id": customId};
}
/// Helper builder to provide fluid api for building component rows
class ComponentRowBuilder {
- final List _components = [];
+ final List _components = [];
/// Adds component to row
- void addComponent(IComponentBuilder componentBuilder) => this._components.add(componentBuilder);
+ void addComponent(ComponentBuilderAbstract componentBuilder) => _components.add(componentBuilder);
}
/// Extended [MessageBuilder] with support for buttons
class ComponentMessageBuilder extends MessageBuilder {
/// Set of buttons to attach to message. Message can only have 5 rows with 5 buttons each.
- List>? components;
+ List>? componentRows;
/// Allows to add
void addComponentRow(ComponentRowBuilder componentRowBuilder) {
- if (this.components == null) {
- this.components = [];
- }
+ componentRows ??= [];
if (componentRowBuilder._components.length > 5 || componentRowBuilder._components.isEmpty) {
throw ArgumentError("Component row cannot be empty or have more than 5 components");
}
- if (this.components!.length == 5) {
- throw ArgumentError("There cannot be more that 5 rows of components");
+ if (componentRows!.length == 5) {
+ throw ArgumentError("Maximum number of component rows is 5");
}
- this.components!.add(componentRowBuilder._components);
+ componentRows!.add(componentRowBuilder._components);
}
@override
- RawApiMap build(INyxx client) => {
- ...super.build(client),
- if (this.components != null) "components": [
- for (final row in this.components!)
- {
- "type": ComponentType.row.value,
+ RawApiMap build([AllowedMentions? defaultAllowedMentions]) => {
+ ...super.build(allowedMentions),
+ if (componentRows != null)
"components": [
- for (final component in row)
- component.build()
+ for (final row in componentRows!)
+ {
+ "type": ComponentType.row.value,
+ "components": [for (final component in row) component.build()]
+ }
]
- }
- ]
- };
+ };
}
diff --git a/lib/src/builders/SlashCommandBuilder.dart b/lib/src/builders/slash_command_builder.dart
similarity index 54%
rename from lib/src/builders/SlashCommandBuilder.dart
rename to lib/src/builders/slash_command_builder.dart
index 46bdc73..915cdb9 100644
--- a/lib/src/builders/SlashCommandBuilder.dart
+++ b/lib/src/builders/slash_command_builder.dart
@@ -1,4 +1,13 @@
-part of nyxx_interactions;
+import 'package:nyxx/nyxx.dart';
+
+import 'package:nyxx_interactions/src/builders/command_option_builder.dart';
+import 'package:nyxx_interactions/src/builders/command_permission_builder.dart';
+
+import 'package:nyxx_interactions/src/models/slash_command_type.dart';
+import 'package:nyxx_interactions/src/models/command_option.dart';
+import 'package:nyxx_interactions/src/interactions.dart';
+import 'package:nyxx_interactions/src/internal/utils.dart';
+import 'package:nyxx_interactions/src/typedefs.dart';
/// A slash command, can only be instantiated through a method on [Interactions]
class SlashCommandBuilder extends Builder {
@@ -21,58 +30,57 @@ class SlashCommandBuilder extends Builder {
List options;
/// Permission overrides for the command
- List? permissions;
+ List? permissions;
/// Target of slash command if different that SlashCommandTarget.chat - slash command will
/// become context menu in appropriate context
SlashCommandType type;
/// Handler for SlashCommandBuilder
- SlashCommandHandler? _handler;
+ SlashCommandHandler? handler;
/// A slash command, can only be instantiated through a method on [Interactions]
SlashCommandBuilder(this.name, this.description, this.options,
{this.defaultPermissions = true, this.permissions, this.guild, this.type = SlashCommandType.chat}) {
- if (!slashCommandNameRegex.hasMatch(this.name)) {
+ if (!slashCommandNameRegex.hasMatch(name)) {
throw ArgumentError("Command name has to match regex: ${slashCommandNameRegex.pattern}");
}
- if (this.description == null && this.type == SlashCommandType.chat) {
+ if (description == null && type == SlashCommandType.chat) {
throw ArgumentError("Normal slash command needs to have description");
}
- if (this.description != null && this.type != SlashCommandType.chat) {
+ if (description != null && type != SlashCommandType.chat) {
throw ArgumentError("Context menus cannot have description");
}
}
@override
RawApiMap build() => {
- "name": this.name,
- if (this.type == SlashCommandType.chat) "description": this.description,
- "default_permission": this.defaultPermissions,
- if (this.options.isNotEmpty) "options": this.options.map((e) => e.build()).toList(),
- "type": this.type.value,
+ "name": name,
+ if (type == SlashCommandType.chat) "description": description,
+ "default_permission": defaultPermissions,
+ if (options.isNotEmpty) "options": options.map((e) => e.build()).toList(),
+ "type": type.value,
};
- void _setId(Snowflake id) => this._id = id;
+ void setId(Snowflake id) => _id = id;
+
+ Snowflake get id => _id;
/// Register a permission
- void addPermission(ICommandPermissionBuilder permission) {
- if (this.permissions == null) {
- this.permissions = [];
- }
- this.permissions!.add(permission);
+ void addPermission(CommandPermissionBuilderAbstract permission) {
+ permissions ??= [];
+
+ permissions!.add(permission);
}
/// Registers handler for command. Note command cannot have handler if there are options present
void registerHandler(SlashCommandHandler handler) {
- if (this.options.any((element) =>
- element.type == CommandOptionType.subCommand || element.type == CommandOptionType.subCommandGroup)) {
- throw new ArgumentError(
- "Cannot register handler for slash command if command have subcommand or subcommandgroup");
+ if (options.any((element) => element.type == CommandOptionType.subCommand || element.type == CommandOptionType.subCommandGroup)) {
+ throw ArgumentError("Cannot register handler for slash command if command have subcommand or subcommandgroup");
}
- this._handler = handler;
+ this.handler = handler;
}
}
diff --git a/lib/src/events/InteractionEvent.dart b/lib/src/events/InteractionEvent.dart
deleted file mode 100644
index 0013888..0000000
--- a/lib/src/events/InteractionEvent.dart
+++ /dev/null
@@ -1,209 +0,0 @@
-part of nyxx_interactions;
-
-abstract class InteractionEvent {
- /// Reference to [Nyxx]
- Nyxx get client => interactions._client;
-
- /// Reference to [Interactions]
- late final Interactions interactions;
-
- /// The interaction data, includes the args, name, guild, channel, etc.
- T get interaction;
-
- /// The DateTime the interaction was received by the Nyxx Client.
- final DateTime receivedAt = DateTime.now();
-
- final Logger _logger = Logger("Interaction Event");
-
- InteractionEvent._new(this.interactions);
-}
-
-class AutocompleteInteractionEvent extends InteractionEvent {
- @override
- late final SlashCommandInteraction interaction;
-
- AutocompleteInteractionEvent._new(Interactions interactions, RawApiMap raw): super._new(interactions) {
- this.interaction = SlashCommandInteraction._new(client, raw);
- }
-
- /// Returns focused option of autocomplete
- InteractionOption get focusedOption => _extractArgs(this.interaction.options)
- .firstWhere((element) => element.isFocused);
-
- /// Responds to interaction
- Future respond(List builders) async {
- if (DateTime.now().difference(this.receivedAt).inSeconds > 3) {
- throw new InteractionExpiredError._3secs();
- }
-
- return this.interactions.interactionsEndpoints.respondToAutocomplete(this.interaction.id, this.interaction.token, builders);
- }
-}
-
-abstract class InteractionEventWithAcknowledge extends InteractionEvent {
- /// If the Client has sent a response to the Discord API. Once the API was received a response you cannot send another.
- bool _hasAcked = false;
-
- /// Opcode for acknowledging interaction
- int get _acknowledgeOpCode;
-
- /// Opcode for responding to interaction
- int get _respondOpcode;
-
- InteractionEventWithAcknowledge._new(Interactions interactions): super._new(interactions);
-
- /// Create a followup message for an Interaction
- Future sendFollowup(MessageBuilder builder) async {
- if (!_hasAcked) {
- return Future.error(ResponseRequiredError());
- }
- this._logger.fine("Sending followup for for interaction: ${this.interaction.id}");
-
- return this.interactions.interactionsEndpoints.sendFollowup(
- this.interaction.token,
- this.client.app.id,
- builder
- );
- }
-
- /// Edits followup message
- Future editFollowup(Snowflake messageId, MessageBuilder builder) =>
- this.interactions.interactionsEndpoints.editFollowup(this.interaction.token, this.client.app.id, messageId, builder);
-
- /// Deletes followup message with given id
- Future deleteFollowup(Snowflake messageId) =>
- this.interactions.interactionsEndpoints.deleteFollowup(this.interaction.token, this.client.app.id, messageId);
-
- /// Deletes original response
- Future deleteOriginalResponse() =>
- this.interactions.interactionsEndpoints.deleteOriginalResponse(this.interaction.token, this.client.app.id, this.interaction.id.toString());
-
- /// Fetch followup message
- Future fetchFollowup(Snowflake messageId) async =>
- this.interactions.interactionsEndpoints.fetchFollowup(this.interaction.token, this.client.app.id, messageId);
-
- /// Used to acknowledge a Interaction but not send any response yet.
- /// Once this is sent you can then only send ChannelMessages.
- /// You can also set showSource to also print out the command the user entered.
- Future acknowledge({bool hidden = false}) async {
- if (_hasAcked) {
- return Future.error(AlreadyRespondedError());
- }
-
- if (DateTime.now().isAfter(this.receivedAt.add(const Duration(seconds: 3)))) {
- return Future.error(InteractionExpiredError._3secs());
- }
-
- await this.interactions.interactionsEndpoints.acknowledge(
- this.interaction.token,
- this.interaction.id.toString(),
- hidden,
- this._acknowledgeOpCode
- );
-
- this._logger.fine("Sending acknowledge for for interaction: ${this.interaction.id}");
-
- _hasAcked = true;
- }
-
- /// Used to acknowledge a Interaction and send a response.
- /// Once this is sent you can then only send ChannelMessages.
- Future respond(MessageBuilder builder, {bool hidden = false}) async {
- final now = DateTime.now();
- if (_hasAcked && now.isAfter(this.receivedAt.add(const Duration(minutes: 15)))) {
- return Future.error(InteractionExpiredError._15mins());
- } else if (now.isAfter(this.receivedAt.add(const Duration(seconds: 3)))) {
- return Future.error(InteractionExpiredError._3secs());
- }
-
- this._logger.fine("Sending respond for for interaction: ${this.interaction.id}");
- if (_hasAcked) {
- await this.interactions.interactionsEndpoints.respondEditOriginal(
- this.interaction.token,
- this.client.app.id,
- builder,
- hidden
- );
- } else {
- if (!builder.canBeUsedAsNewMessage()) {
- return Future.error(ArgumentError("Cannot sent message when MessageBuilder doesn't have set either content, embed or files"));
- }
-
- await this.interactions.interactionsEndpoints.respondCreateResponse(
- this.interaction.token,
- this.interaction.id.toString(),
- builder,
- hidden,
- _respondOpcode
- );
- }
-
- _hasAcked = true;
- }
-
- /// Returns [Message] object of original interaction response
- Future getOriginalResponse() async =>
- this.interactions.interactionsEndpoints.fetchOriginalResponse(this.interaction.token, this.client.app.id, this.interaction.id.toString());
-
- /// Edits original message response
- Future editOriginalResponse(MessageBuilder builder) =>
- this.interactions.interactionsEndpoints.editOriginalResponse(this.interaction.token, this.client.app.id, builder);
-}
-
-/// Event for slash commands
-class SlashCommandInteractionEvent extends InteractionEventWithAcknowledge {
- /// Interaction data for slash command
- @override
- late final SlashCommandInteraction interaction;
-
- @override
- int get _acknowledgeOpCode => 5;
-
- @override
- int get _respondOpcode => 4;
-
- /// Returns args of interaction
- List get args => UnmodifiableListView(_extractArgs(this.interaction.options));
-
- /// Searches for arg with [name] in this interaction
- InteractionOption getArg(String name) => args.firstWhere((element) => element.name == name);
-
- SlashCommandInteractionEvent._new(Interactions interactions, RawApiMap raw) : super._new(interactions) {
- this.interaction = SlashCommandInteraction._new(client, raw);
- }
-}
-
-/// Generic event for component interactions
-abstract class ComponentInteractionEvent extends InteractionEventWithAcknowledge {
- /// Interaction data for slash command
- @override
- late final T interaction;
-
- @override
- int get _acknowledgeOpCode => 6;
-
- @override
- int get _respondOpcode => 7;
-
- ComponentInteractionEvent._new(Interactions interactions, RawApiMap raw) : super._new(interactions);
-}
-
-/// Interaction event for button events
-class ButtonInteractionEvent extends ComponentInteractionEvent {
- @override
- late final ButtonInteraction interaction;
-
- ButtonInteractionEvent._new(Interactions interactions, RawApiMap raw) : super._new(interactions, raw) {
- this.interaction = ButtonInteraction._new(client, raw);
- }
-}
-
-/// Interaction event for dropdown events
-class MultiselectInteractionEvent extends ComponentInteractionEvent {
- @override
- late final MultiselectInteraction interaction;
-
- MultiselectInteractionEvent._new(Interactions interactions, RawApiMap raw) : super._new(interactions, raw) {
- this.interaction = MultiselectInteraction._new(client, raw);
- }
-}
diff --git a/lib/src/events/interaction_event.dart b/lib/src/events/interaction_event.dart
new file mode 100644
index 0000000..5407ec7
--- /dev/null
+++ b/lib/src/events/interaction_event.dart
@@ -0,0 +1,290 @@
+import 'dart:collection';
+
+import 'package:logging/logging.dart';
+import 'package:nyxx_interactions/src/models/interaction.dart';
+import 'package:nyxx_interactions/src/interactions.dart';
+import 'package:nyxx_interactions/src/internal/utils.dart';
+import 'package:nyxx_interactions/src/models/interaction_option.dart';
+import 'package:nyxx_interactions/src/builders/arg_choice_builder.dart';
+import 'package:nyxx_interactions/src/exceptions/interaction_expired.dart';
+import 'package:nyxx_interactions/src/exceptions/response_required.dart';
+import 'package:nyxx_interactions/src/exceptions/already_responded.dart';
+import 'package:nyxx/nyxx.dart';
+
+abstract class IInteractionEvent {
+ /// Reference to [Nyxx]
+ INyxx get client;
+
+ /// Reference to [Interactions]
+ Interactions get interactions;
+
+ /// The interaction data, includes the args, name, guild, channel, etc.
+ T get interaction;
+
+ /// The DateTime the interaction was received by the Nyxx Client.
+ DateTime get receivedAt;
+}
+
+abstract class InteractionEventAbstract implements IInteractionEvent {
+ /// Reference to [Nyxx]
+ @override
+ INyxx get client => interactions.client;
+
+ /// Reference to [Interactions]
+ @override
+ late final Interactions interactions;
+
+ /// The interaction data, includes the args, name, guild, channel, etc.
+ @override
+ T get interaction;
+
+ /// The DateTime the interaction was received by the Nyxx Client.
+ @override
+ final DateTime receivedAt = DateTime.now();
+
+ final Logger logger = Logger("Interaction Event");
+
+ InteractionEventAbstract(this.interactions);
+}
+
+abstract class IAutocompleteInteractionEvent implements InteractionEventAbstract {
+ @override
+ late final ISlashCommandInteraction interaction;
+
+ /// Returns focused option of autocomplete
+ IInteractionOption get focusedOption;
+
+ /// List of autocomplete options
+ late final Iterable options;
+
+ /// Responds to interaction
+ Future respond(List builders);
+}
+
+class AutocompleteInteractionEvent extends InteractionEventAbstract implements IAutocompleteInteractionEvent {
+ @override
+ late final ISlashCommandInteraction interaction;
+
+ @override
+ IInteractionOption get focusedOption => options.firstWhere((element) => element.isFocused);
+
+ @override
+ late final Iterable options;
+
+ AutocompleteInteractionEvent(Interactions interactions, RawApiMap raw) : super(interactions) {
+ interaction = SlashCommandInteraction(client, raw);
+ options = extractArgs(interaction.options);
+ }
+
+ @override
+ Future respond(List builders) async {
+ if (DateTime.now().difference(receivedAt).inSeconds > 3) {
+ throw InteractionExpiredError.threeSecs();
+ }
+
+ return interactions.interactionsEndpoints.respondToAutocomplete(interaction.id, interaction.token, builders);
+ }
+}
+
+abstract class IInteractionEventWithAcknowledge implements IInteractionEvent {
+ /// Create a followup message for an Interaction
+ Future sendFollowup(MessageBuilder builder, {bool hidden = false});
+
+ /// Edits followup message
+ Future editFollowup(Snowflake messageId, MessageBuilder builder);
+
+ /// Deletes followup message with given id
+ Future deleteFollowup(Snowflake messageId);
+
+ /// Deletes original response
+ Future deleteOriginalResponse();
+
+ /// Fetch followup message
+ Future fetchFollowup(Snowflake messageId);
+
+ /// Used to acknowledge a Interaction but not send any response yet.
+ /// Once this is sent you can then only send ChannelMessages.
+ /// You can also set showSource to also print out the command the user entered.
+ Future acknowledge({bool hidden = false});
+
+ /// Used to acknowledge a Interaction and send a response.
+ /// Once this is sent you can then only send ChannelMessages.
+ Future respond(MessageBuilder builder, {bool hidden = false});
+
+ /// Returns [Message] object of original interaction response
+ Future getOriginalResponse();
+
+ /// Edits original message response
+ Future editOriginalResponse(MessageBuilder builder);
+}
+
+abstract class InteractionEventWithAcknowledge extends InteractionEventAbstract implements IInteractionEventWithAcknowledge {
+ /// If the Client has sent a response to the Discord API. Once the API was received a response you cannot send another.
+ bool _hasAcked = false;
+
+ /// Opcode for acknowledging interaction
+ int get _acknowledgeOpCode;
+
+ /// Opcode for responding to interaction
+ int get _respondOpcode;
+
+ InteractionEventWithAcknowledge(Interactions interactions) : super(interactions);
+
+ /// Create a followup message for an Interaction
+ @override
+ Future sendFollowup(MessageBuilder builder, {bool hidden = false}) async {
+ if (!_hasAcked) {
+ return Future.error(ResponseRequiredError());
+ }
+ logger.fine("Sending followup for for interaction: ${interaction.id}");
+
+ return interactions.interactionsEndpoints.sendFollowup(interaction.token, client.appId, builder, hidden: hidden);
+ }
+
+ /// Edits followup message
+ @override
+ Future editFollowup(Snowflake messageId, MessageBuilder builder) =>
+ interactions.interactionsEndpoints.editFollowup(interaction.token, client.appId, messageId, builder);
+
+ /// Deletes followup message with given id
+ @override
+ Future deleteFollowup(Snowflake messageId) => interactions.interactionsEndpoints.deleteFollowup(interaction.token, client.appId, messageId);
+
+ /// Deletes original response
+ @override
+ Future deleteOriginalResponse() =>
+ interactions.interactionsEndpoints.deleteOriginalResponse(interaction.token, client.appId, interaction.id.toString());
+
+ /// Fetch followup message
+ @override
+ Future fetchFollowup(Snowflake messageId) async => interactions.interactionsEndpoints.fetchFollowup(interaction.token, client.appId, messageId);
+
+ /// Used to acknowledge a Interaction but not send any response yet.
+ /// Once this is sent you can then only send ChannelMessages.
+ /// You can also set showSource to also print out the command the user entered.
+ @override
+ Future acknowledge({bool hidden = false}) async {
+ if (_hasAcked) {
+ return Future.error(AlreadyRespondedError());
+ }
+
+ if (DateTime.now().isAfter(receivedAt.add(const Duration(seconds: 3)))) {
+ return Future.error(InteractionExpiredError.threeSecs());
+ }
+
+ await interactions.interactionsEndpoints.acknowledge(interaction.token, interaction.id.toString(), hidden, _acknowledgeOpCode);
+
+ logger.fine("Sending acknowledge for for interaction: ${interaction.id}");
+
+ _hasAcked = true;
+ }
+
+ /// Used to acknowledge a Interaction and send a response.
+ /// Once this is sent you can then only send ChannelMessages.
+ @override
+ Future respond(MessageBuilder builder, {bool hidden = false}) async {
+ final now = DateTime.now();
+ if (_hasAcked && now.isAfter(receivedAt.add(const Duration(minutes: 15)))) {
+ return Future.error(InteractionExpiredError.fifteenMins());
+ } else if (now.isAfter(receivedAt.add(const Duration(seconds: 3)))) {
+ return Future.error(InteractionExpiredError.threeSecs());
+ }
+
+ logger.fine("Sending respond for for interaction: ${interaction.id}");
+ if (_hasAcked) {
+ await interactions.interactionsEndpoints.respondEditOriginal(interaction.token, client.appId, builder, hidden);
+ } else {
+ if (!builder.canBeUsedAsNewMessage()) {
+ return Future.error(ArgumentError("Cannot sent message when MessageBuilder doesn't have set either content, embed or files"));
+ }
+
+ await interactions.interactionsEndpoints.respondCreateResponse(interaction.token, interaction.id.toString(), builder, hidden, _respondOpcode);
+ }
+
+ _hasAcked = true;
+ }
+
+ /// Returns [Message] object of original interaction response
+ @override
+ Future getOriginalResponse() async =>
+ interactions.interactionsEndpoints.fetchOriginalResponse(interaction.token, client.appId, interaction.id.toString());
+
+ /// Edits original message response
+ @override
+ Future editOriginalResponse(MessageBuilder builder) =>
+ interactions.interactionsEndpoints.editOriginalResponse(interaction.token, client.appId, builder);
+}
+
+abstract class ISlashCommandInteractionEvent implements InteractionEventWithAcknowledge {
+ /// Returns args of interaction
+ List get args;
+
+ /// Searches for arg with [name] in this interaction
+ IInteractionOption getArg(String name);
+}
+
+/// Event for slash commands
+class SlashCommandInteractionEvent extends InteractionEventWithAcknowledge implements ISlashCommandInteractionEvent {
+ /// Interaction data for slash command
+ @override
+ late final SlashCommandInteraction interaction;
+
+ @override
+ int get _acknowledgeOpCode => 5;
+
+ @override
+ int get _respondOpcode => 4;
+
+ /// Returns args of interaction
+ @override
+ List get args => UnmodifiableListView(extractArgs(interaction.options));
+
+ /// Searches for arg with [name] in this interaction
+ @override
+ IInteractionOption getArg(String name) => args.firstWhere((element) => element.name == name);
+
+ SlashCommandInteractionEvent(Interactions interactions, RawApiMap raw) : super(interactions) {
+ interaction = SlashCommandInteraction(client, raw);
+ }
+}
+
+abstract class IComponentInteractionEvent implements IInteractionEventWithAcknowledge {}
+
+/// Generic event for component interactions
+abstract class ComponentInteractionEvent extends InteractionEventWithAcknowledge implements IComponentInteractionEvent {
+ /// Interaction data for slash command
+ @override
+ T get interaction;
+
+ @override
+ int get _acknowledgeOpCode => 6;
+
+ @override
+ int get _respondOpcode => 7;
+
+ ComponentInteractionEvent(Interactions interactions, RawApiMap raw) : super(interactions);
+}
+
+abstract class IButtonInteractionEvent implements IComponentInteractionEvent {}
+
+/// Interaction event for button events
+class ButtonInteractionEvent extends ComponentInteractionEvent implements IButtonInteractionEvent {
+ @override
+ late final IButtonInteraction interaction;
+
+ ButtonInteractionEvent(Interactions interactions, RawApiMap raw) : super(interactions, raw) {
+ interaction = ButtonInteraction(client, raw);
+ }
+}
+
+abstract class IMultiselectInteractionEvent implements ComponentInteractionEvent {}
+
+/// Interaction event for dropdown events
+class MultiselectInteractionEvent extends ComponentInteractionEvent implements IMultiselectInteractionEvent {
+ @override
+ late final IMultiselectInteraction interaction;
+
+ MultiselectInteractionEvent(Interactions interactions, RawApiMap raw) : super(interactions, raw) {
+ interaction = MultiselectInteraction(client, raw);
+ }
+}
diff --git a/lib/src/exceptions/InteractionExpired.dart b/lib/src/exceptions/InteractionExpired.dart
deleted file mode 100644
index 0d34a9c..0000000
--- a/lib/src/exceptions/InteractionExpired.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-part of nyxx_interactions;
-
-/// Thrown when 15 minutes has passed since an interaction was called.
-class InteractionExpiredError implements Error {
- late final String _timeFrameString;
-
- InteractionExpiredError._15mins() {
- this._timeFrameString = "15mins";
- }
-
- InteractionExpiredError._3secs() {
- this._timeFrameString = "3secs";
- }
-
- /// Returns a string representation of this object.
- @override
- String toString() => "InteractionExpiredError: Interaction tokens are only valid for $_timeFrameString. It has been over $_timeFrameString and the token is now invalid.";
-
- @override
- StackTrace? get stackTrace => StackTrace.empty;
-}
diff --git a/lib/src/exceptions/AlreadyResponded.dart b/lib/src/exceptions/already_responded.dart
similarity index 93%
rename from lib/src/exceptions/AlreadyResponded.dart
rename to lib/src/exceptions/already_responded.dart
index 9e84b48..58edec6 100644
--- a/lib/src/exceptions/AlreadyResponded.dart
+++ b/lib/src/exceptions/already_responded.dart
@@ -1,5 +1,3 @@
-part of nyxx_interactions;
-
/// Thrown when you have already responded to an interaction
class AlreadyRespondedError implements Error {
/// Returns a string representation of this object.
diff --git a/lib/src/exceptions/interaction_expired.dart b/lib/src/exceptions/interaction_expired.dart
new file mode 100644
index 0000000..4a838e4
--- /dev/null
+++ b/lib/src/exceptions/interaction_expired.dart
@@ -0,0 +1,20 @@
+/// Thrown when 15 minutes has passed since an interaction was called.
+class InteractionExpiredError implements Error {
+ late final String _timeFrameString;
+
+ InteractionExpiredError.fifteenMins() {
+ _timeFrameString = "15mins";
+ }
+
+ InteractionExpiredError.threeSecs() {
+ _timeFrameString = "3secs";
+ }
+
+ /// Returns a string representation of this object.
+ @override
+ String toString() =>
+ "InteractionExpiredError: Interaction tokens are only valid for $_timeFrameString. It has been over $_timeFrameString and the token is now invalid.";
+
+ @override
+ StackTrace? get stackTrace => StackTrace.empty;
+}
diff --git a/lib/src/exceptions/ResponseRequired.dart b/lib/src/exceptions/response_required.dart
similarity index 92%
rename from lib/src/exceptions/ResponseRequired.dart
rename to lib/src/exceptions/response_required.dart
index ff12c92..45c99fe 100644
--- a/lib/src/exceptions/ResponseRequired.dart
+++ b/lib/src/exceptions/response_required.dart
@@ -1,5 +1,3 @@
-part of nyxx_interactions;
-
/// Thrown when you haven't sent a response yet
class ResponseRequiredError implements Error {
/// Returns a string representation of this object.
diff --git a/lib/src/interactions.dart b/lib/src/interactions.dart
new file mode 100644
index 0000000..0183048
--- /dev/null
+++ b/lib/src/interactions.dart
@@ -0,0 +1,358 @@
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:logging/logging.dart';
+import 'package:nyxx/nyxx.dart';
+import 'package:nyxx_interactions/src/backend/interaction_backend.dart';
+
+import 'package:nyxx_interactions/src/builders/slash_command_builder.dart';
+import 'package:nyxx_interactions/src/internal/interaction_endpoints.dart';
+import 'package:nyxx_interactions/src/internal/event_controller.dart';
+import 'package:nyxx_interactions/src/models/slash_command.dart';
+import 'package:nyxx_interactions/src/internal/sync/commands_sync.dart';
+import 'package:nyxx_interactions/src/internal/sync/manual_command_sync.dart';
+import 'package:nyxx_interactions/src/internal/utils.dart';
+import 'package:nyxx_interactions/src/models/command_option.dart';
+import 'package:nyxx_interactions/src/typedefs.dart';
+import 'package:nyxx_interactions/src/events/interaction_event.dart';
+import 'package:nyxx_interactions/src/builders/command_option_builder.dart';
+
+abstract class IInteractions {
+ IEventController get events;
+
+ /// Reference to client
+ INyxx get client;
+
+ InteractionBackend get backend;
+
+ /// Commands registered by bot
+ Iterable get commands;
+
+ /// All interaction endpoints that can be accessed.
+ IInteractionsEndpoints get interactionsEndpoints;
+
+ /// Syncs commands builders with discord after client is ready.
+ void syncOnReady({ICommandsSync syncRule = const ManualCommandSync()});
+
+ /// Syncs command builders with discord immediately.
+ /// Warning: Client could not be ready at the function execution.
+ /// Use [syncOnReady] for proper behavior
+ Future sync({ICommandsSync syncRule = const ManualCommandSync()});
+
+ /// Registers callback for button event for given [id]
+ void registerButtonHandler(String id, ButtonInteractionHandler handler);
+
+ /// Register callback for dropdown event for given [id]
+ void registerMultiselectHandler(String id, MultiselectInteractionHandler handler);
+
+ /// Allows to register new [SlashCommandBuilder]
+ void registerSlashCommand(SlashCommandBuilder slashCommandBuilder);
+
+ /// Register callback for slash command event for given [id]
+ void registerSlashCommandHandler(String id, SlashCommandHandler handler);
+
+ /// Deletes global command
+ Future deleteGlobalCommand(Snowflake commandId);
+
+ /// Deletes all global commands
+ Future deleteGlobalCommands();
+
+ /// Deletes guild command
+ Future deleteGuildCommand(Snowflake commandId, Snowflake guildId);
+
+ /// Deletes all guild commands for the specified guilds
+ Future deleteGuildCommands(List guildIds);
+
+ /// Fetches all global bots command
+ Stream fetchGlobalCommands();
+
+ /// Fetches all guild commands for given guild
+ Stream fetchGuildCommands(Snowflake guildId);
+
+ static IInteractions create(InteractionBackend backend) => Interactions(backend);
+}
+
+/// Interaction extension for Nyxx. Allows use of: Slash Commands.
+class Interactions implements IInteractions {
+ static const _interactionCreateCommand = "INTERACTION_CREATE";
+
+ final Logger _logger = Logger("Interactions");
+ final _commandBuilders = [];
+ final _commands = [];
+ final _commandHandlers = {};
+ final _buttonHandlers = {};
+ final _autocompleteHandlers = {};
+ final _multiselectHandlers = {};
+
+ @override
+ late final IEventController events;
+
+ /// Reference to client
+ @override
+ INyxx get client => backend.client;
+
+ @override
+ final InteractionBackend backend;
+
+ /// Commands registered by bot
+ @override
+ Iterable get commands => UnmodifiableListView(_commands);
+
+ /// All interaction endpoints that can be accessed.
+ @override
+ late final IInteractionsEndpoints interactionsEndpoints;
+
+ /// Create new instance of the interactions class.
+ Interactions(this.backend) {
+ events = EventController();
+ interactionsEndpoints = InteractionsEndpoints(client);
+
+ backend.setup();
+
+ _logger.info("Interactions ready");
+
+ backend.getStream().listen((rawData) {
+ if (rawData["op"] == 0 && rawData["t"] == _interactionCreateCommand) {
+ _logger.fine("Received interaction event: [$rawData]");
+
+ final type = rawData["d"]["type"] as int;
+
+ switch (type) {
+ case 2:
+ (events as EventController).onSlashCommandController.add(SlashCommandInteractionEvent(this, rawData["d"] as RawApiMap));
+ break;
+ case 3:
+ final componentType = rawData["d"]["data"]["component_type"] as int;
+
+ switch (componentType) {
+ case 2:
+ (events as EventController).onButtonEventController.add(ButtonInteractionEvent(this, rawData["d"] as Map));
+ break;
+ case 3:
+ (events as EventController).onMultiselectEventController.add(MultiselectInteractionEvent(this, rawData["d"] as Map));
+ break;
+ default:
+ _logger.warning("Unknown componentType type: [$componentType]; Payload: ${jsonEncode(rawData)}");
+ }
+ break;
+ case 4:
+ (events as EventController).onAutocompleteEventController.add(AutocompleteInteractionEvent(this, rawData["d"] as Map));
+ break;
+ default:
+ _logger.warning("Unknown interaction type: [$type]; Payload: ${jsonEncode(rawData)}");
+ }
+ }
+ });
+ }
+
+ /// Syncs commands builders with discord after client is ready.
+ @override
+ void syncOnReady({ICommandsSync syncRule = const ManualCommandSync()}) {
+ backend.getReadyStream().listen((_) async {
+ await sync(syncRule: syncRule);
+ });
+ }
+
+ /// Syncs command builders with discord immediately.
+ /// Warning: Client could not be ready at the function execution.
+ /// Use [syncOnReady] for proper behavior
+ @override
+ Future sync({ICommandsSync syncRule = const ManualCommandSync()}) async {
+ final shouldSync = await syncRule.shouldSync(_commandBuilders);
+
+ final commandPartition = partition(_commandBuilders, (element) => element.guild == null);
+ final globalCommands = commandPartition.first;
+ final groupedGuildCommands = groupSlashCommandBuilders(commandPartition.last);
+
+ if (shouldSync) {
+ final globalCommandsResponse = await interactionsEndpoints.bulkOverrideGlobalCommands(client.appId, globalCommands).toList();
+ _extractCommandIds(globalCommandsResponse);
+
+ for (final command in globalCommandsResponse) {
+ _commands.add(command);
+ }
+ }
+
+ for (final globalCommandBuilder in globalCommands) {
+ _assignCommandToHandler(globalCommandBuilder);
+ }
+
+ for (final entry in groupedGuildCommands.entries) {
+ if (shouldSync) {
+ final response = await interactionsEndpoints.bulkOverrideGuildCommands(client.appId, entry.key, entry.value).toList();
+ _extractCommandIds(response);
+
+ for (final command in response) {
+ _commands.add(command);
+ }
+ }
+
+ for (final globalCommandBuilder in entry.value) {
+ _assignCommandToHandler(globalCommandBuilder);
+ }
+
+ await interactionsEndpoints.bulkOverrideGuildCommandsPermissions(client.appId, entry.key, entry.value);
+ }
+
+ if (_commandHandlers.isNotEmpty) {
+ events.onSlashCommand.listen((event) async {
+ final commandHash = determineInteractionCommandHandler(event.interaction);
+
+ _logger.info("Executing command with hash [$commandHash]");
+ if (_commandHandlers.containsKey(commandHash)) {
+ await _commandHandlers[commandHash]!(event);
+ }
+ });
+
+ _logger.info("Finished registering ${_commandHandlers.length} commands!");
+ }
+
+ if (_buttonHandlers.isNotEmpty) {
+ events.onButtonEvent.listen((event) {
+ if (_buttonHandlers.containsKey(event.interaction.customId)) {
+ _logger.info("Executing button with id [${event.interaction.customId}]");
+ _buttonHandlers[event.interaction.customId]!(event);
+ } else {
+ _logger.warning("Received event for unknown button: ${event.interaction.customId}");
+ }
+ });
+ }
+
+ if (_multiselectHandlers.isNotEmpty) {
+ events.onMultiselectEvent.listen((event) {
+ if (_multiselectHandlers.containsKey(event.interaction.customId)) {
+ _logger.info("Executing multiselect with id [${event.interaction.customId}]");
+ _multiselectHandlers[event.interaction.customId]!(event);
+ } else {
+ _logger.warning("Received event for unknown dropdown: ${event.interaction.customId}");
+ }
+ });
+ }
+
+ if (_autocompleteHandlers.isNotEmpty) {
+ events.onAutocompleteEvent.listen((event) {
+ final name = event.focusedOption.name;
+ final commandHash = determineInteractionCommandHandler(event.interaction);
+ final autocompleteHash = "$commandHash$name";
+
+ if (_autocompleteHandlers.containsKey(autocompleteHash)) {
+ _logger.info("Executing autocomplete with id [$autocompleteHash]");
+ _autocompleteHandlers[autocompleteHash]!(event);
+ } else {
+ _logger.warning("Received event for unknown dropdown: $autocompleteHash");
+ }
+ });
+ }
+
+ _commandBuilders.clear(); // Cleanup after registering command since we don't need this anymore
+ _logger.info("Finished registering commands");
+ }
+
+ /// Registers callback for button event for given [id]
+ @override
+ void registerButtonHandler(String id, ButtonInteractionHandler handler) => _buttonHandlers[id] = handler;
+
+ /// Register callback for dropdown event for given [id]
+ @override
+ void registerMultiselectHandler(String id, MultiselectInteractionHandler handler) => _multiselectHandlers[id] = handler;
+
+ /// Allows to register new [SlashCommandBuilder]
+ @override
+ void registerSlashCommand(SlashCommandBuilder slashCommandBuilder) => _commandBuilders.add(slashCommandBuilder);
+
+ /// Register callback for slash command event for given [id]
+ @override
+ void registerSlashCommandHandler(String id, SlashCommandHandler handler) => _commandHandlers[id] = handler;
+
+ /// Deletes global command
+ @override
+ Future deleteGlobalCommand(Snowflake commandId) => interactionsEndpoints.deleteGlobalCommand(client.appId, commandId);
+
+ /// Deletes all global commands
+ @override
+ Future deleteGlobalCommands() async => interactionsEndpoints.bulkOverrideGlobalCommands(client.appId, []);
+
+ /// Deletes guild command
+ @override
+ Future deleteGuildCommand(Snowflake commandId, Snowflake guildId) => interactionsEndpoints.deleteGuildCommand(client.appId, commandId, guildId);
+
+ /// Deletes all guild commands for the specified guilds
+ @override
+ Future deleteGuildCommands(List guildIds) async {
+ await Future.wait(guildIds.map((guildId) => interactionsEndpoints.bulkOverrideGuildCommands(client.appId, guildId, []).toList()));
+ }
+
+ /// Fetches all global bots command
+ @override
+ Stream fetchGlobalCommands() => interactionsEndpoints.fetchGlobalCommands(client.appId);
+
+ /// Fetches all guild commands for given guild
+ @override
+ Stream fetchGuildCommands(Snowflake guildId) => interactionsEndpoints.fetchGuildCommands(client.appId, guildId);
+
+ void _extractCommandIds(List commands) {
+ for (final slashCommand in commands) {
+ _commandBuilders.firstWhere((element) => element.name == slashCommand.name && element.guild == slashCommand.guild?.id).setId(slashCommand.id);
+ }
+ }
+
+ void _assignAutoCompleteHandler(String commandHash, Iterable options) {
+ for (final option in options) {
+ if (!option.autoComplete || option.autocompleteHandler == null) {
+ continue;
+ }
+
+ _autocompleteHandlers['$commandHash${option.name}'] = option.autocompleteHandler!;
+ }
+ }
+
+ void _assignCommandToHandler(SlashCommandBuilder builder) {
+ final commandHashPrefix = builder.name;
+
+ var allowRootHandler = true;
+
+ final subCommands = builder.options.where((element) => element.type == CommandOptionType.subCommand);
+ if (subCommands.isNotEmpty) {
+ for (final subCommand in subCommands) {
+ if (subCommand.handler == null) {
+ continue;
+ }
+
+ final hash = "$commandHashPrefix|${subCommand.name}";
+ _commandHandlers[hash] = subCommand.handler!;
+ _assignAutoCompleteHandler(hash, subCommand.options ?? []);
+ }
+
+ allowRootHandler = false;
+ }
+
+ final subCommandGroups = builder.options.where((element) => element.type == CommandOptionType.subCommandGroup);
+ if (subCommandGroups.isNotEmpty) {
+ for (final subCommandGroup in subCommandGroups) {
+ final subCommands = subCommandGroup.options?.where((element) => element.type == CommandOptionType.subCommand) ?? [];
+
+ for (final subCommand in subCommands) {
+ if (subCommand.handler == null) {
+ continue;
+ }
+
+ final hash = "$commandHashPrefix|${subCommandGroup.name}|${subCommand.name}";
+ _commandHandlers[hash] = subCommand.handler!;
+ _assignAutoCompleteHandler(hash, subCommand.options ?? []);
+ }
+ }
+
+ allowRootHandler = false;
+ }
+
+ if (!allowRootHandler) {
+ return;
+ }
+
+ if (builder.handler != null) {
+ _commandHandlers[commandHashPrefix] = builder.handler!;
+ _assignAutoCompleteHandler(commandHashPrefix, builder.options);
+ }
+ }
+}
diff --git a/lib/src/internal/InteractionEndpoints.dart b/lib/src/internal/InteractionEndpoints.dart
deleted file mode 100644
index 97c2bc9..0000000
--- a/lib/src/internal/InteractionEndpoints.dart
+++ /dev/null
@@ -1,423 +0,0 @@
-part of nyxx_interactions;
-
-abstract class IInteractionsEndpoints {
- /// Sends followup for interaction with given [token]. Message will be created with [builder]
- Future sendFollowup(String token, Snowflake applicationId, MessageBuilder builder);
-
- /// Fetches followup messagge from API
- Future fetchFollowup(String token, Snowflake applicationId, Snowflake messageId);
-
- /// Acknowledges interaction that response can be sent within next 15 mins.
- /// Response will be ephemeral if [hidden] is set to true. To response to different interaction types
- /// (slash command, button...) [opCode] is used.
- Future acknowledge(String token, String interactionId, bool hidden, int opCode);
-
- /// Response to interaction by editing original response. Used when interaction was acked before.
- Future respondEditOriginal(String token, Snowflake applicationId, MessageBuilder builder, bool hidden);
-
- /// Response to interaction by creating response. Used when interaction wasn't acked before.
- Future respondCreateResponse(String token, String interactionId, MessageBuilder builder, bool hidden, int respondOpCode);
-
- /// Fetch original interaction response.
- Future fetchOriginalResponse(String token, Snowflake applicationId, String interactionId);
-
- /// Edits original interaction response using [builder]
- Future editOriginalResponse(String token, Snowflake applicationId, MessageBuilder builder);
-
- /// Deletes original interaction response
- Future deleteOriginalResponse(String token, Snowflake applicationId, String interactionId);
-
- /// Deletes followup message with given id
- Future deleteFollowup(String token, Snowflake applicationId, Snowflake messageId);
-
- /// Edits followup message with given [messageId]
- Future editFollowup(String token, Snowflake applicationId, Snowflake messageId, MessageBuilder builder);
-
- /// Fetches global commands of application
- Stream fetchGlobalCommands(Snowflake applicationId);
-
- /// Fetches global command with given [commandId]
- Future fetchGlobalCommand(Snowflake applicationId, Snowflake commandId);
-
- /// Edits global command with given [commandId] using [builder]
- Future editGlobalCommand(Snowflake applicationId, Snowflake commandId, SlashCommandBuilder builder);
-
- /// Deletes global command with given [commandId]
- Future deleteGlobalCommand(Snowflake applicationId, Snowflake commandId);
-
- /// Bulk overrides global commands. To delete all apps global commands pass empty list to [builders]
- Stream bulkOverrideGlobalCommands(Snowflake applicationId, Iterable builders);
-
- /// Fetches all commands for given [guildId]
- Stream fetchGuildCommands(Snowflake applicationId, Snowflake guildId);
-
- /// Fetches single guild command with given [commandId]
- Future fetchGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId);
-
- /// Edits single guild command with given [commandId]
- Future editGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId, SlashCommandBuilder builder);
-
- /// Deletes guild command with given commandId]
- Future deleteGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId);
-
- /// Bulk overrides global commands. To delete all apps global commands pass empty list to [builders]
- Stream bulkOverrideGuildCommands(Snowflake applicationId, Snowflake guildId, Iterable builders);
-
- /// Overrides permissions for guild commands
- Future bulkOverrideGuildCommandsPermissions(Snowflake applicationId, Snowflake guildId, Iterable builders);
-
- /// Responds to autocomplete interaction
- Future respondToAutocomplete(Snowflake interactionId, String token, List builders);
-}
-
-class _InteractionsEndpoints implements IInteractionsEndpoints {
- final Nyxx _client;
-
- _InteractionsEndpoints(this._client);
-
- @override
- Future acknowledge(String token, String interactionId, bool hidden, int opCode) async {
- final url = "/interactions/$interactionId/$token/callback";
- final response = await this._client.httpEndpoints.sendRawRequest(url, "POST", body: {
- "type": opCode,
- "data": {
- if (hidden) "flags": 1 << 6,
- }
- });
-
- if (response is HttpResponseError) {
- return Future.error(response);
- }
- }
-
- @override
- Future deleteFollowup(String token, Snowflake applicationId, Snowflake messageId) =>
- this._client.httpEndpoints.sendRawRequest(
- "webhooks/$applicationId/$token/messages/$messageId",
- "DELETE"
- );
-
- @override
- Future deleteOriginalResponse(String token, Snowflake applicationId, String interactionId) async {
- final url = "/webhooks/$applicationId/$token/messages/@original";
- const method = "DELETE";
-
- final response = await this._client.httpEndpoints.sendRawRequest(url, method);
- if (response is HttpResponseError) {
- return Future.error(response);
- }
- }
-
- @override
- Future editFollowup(String token, Snowflake applicationId, Snowflake messageId, MessageBuilder builder) async {
- final url = "/webhooks/$applicationId/$token/messages/$messageId";
- final body = BuilderUtility.buildWithClient(builder, _client);
-
- final response = await this._client.httpEndpoints.sendRawRequest(url, "PATCH", body: body);
- if (response is HttpResponseError) {
- return Future.error(response);
- }
-
- return EntityUtility.createMessage(this._client, (response as HttpResponseSuccess).jsonBody as RawApiMap);
- }
-
- @override
- Future editOriginalResponse(String token, Snowflake applicationId, MessageBuilder builder) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/webhooks/$applicationId/$token/messages/@original",
- "PATCH",
- body: builder.build(this._client)
- );
-
- if (response is HttpResponseError) {
- return Future.error(response);
- }
-
- return EntityUtility.createMessage(this._client, (response as HttpResponseSuccess).jsonBody as RawApiMap);
- }
-
- @override
- Future fetchOriginalResponse(String token, Snowflake applicationId, String interactionId) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/webhooks/$applicationId/$token/messages/@original",
- "GET"
- );
-
- if (response is HttpResponseError) {
- return Future.error(response);
- }
-
- return EntityUtility.createMessage(this._client, (response as HttpResponseSuccess).jsonBody as RawApiMap);
- }
-
- @override
- Future respondEditOriginal(String token, Snowflake applicationId, MessageBuilder builder, bool hidden) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/webhooks/$applicationId/$token/messages/@original",
- "PATCH",
- body: {
- if (hidden) "flags": 1 << 6,
- ...BuilderUtility.buildWithClient(builder, _client)
- },
- files: builder.files ?? []
- );
-
- if (response is HttpResponseError) {
- return Future.error(response);
- }
- }
-
- @override
- Future respondCreateResponse(String token, String interactionId, MessageBuilder builder, bool hidden, int respondOpCode) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/interactions/${interactionId.toString()}/$token/callback",
- "POST",
- body: {
- "type": respondOpCode,
- "data": {
- if (hidden) "flags": 1 << 6,
- ...BuilderUtility.buildWithClient(builder, _client)
- },
- },
- files: builder.files ?? []
- );
-
- if (response is HttpResponseError) {
- return Future.error(response);
- }
- }
-
- @override
- Future sendFollowup(String token, Snowflake applicationId, MessageBuilder builder) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/webhooks/$applicationId/$token",
- "POST",
- body: BuilderUtility.buildWithClient(builder, _client),
- files: builder.files ?? []
- );
-
- if (response is HttpResponseError) {
- return Future.error(response);
- }
-
- return EntityUtility.createMessage(this._client, (response as HttpResponseSuccess).jsonBody as RawApiMap);
- }
-
- @override
- Stream bulkOverrideGlobalCommands(Snowflake applicationId, Iterable builders) async* {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/commands",
- "PUT",
- body: [
- for(final builder in builders)
- builder.build()
- ]
- );
-
- if (response is HttpResponseError) {
- yield* Stream.error(response);
- }
-
- for (final rawRes in (response as HttpResponseSuccess).jsonBody as List) {
- yield SlashCommand._new(rawRes as RawApiMap, this._client);
- }
- }
-
- @override
- Stream bulkOverrideGuildCommands(Snowflake applicationId, Snowflake guildId, Iterable builders) async* {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/${this._client.app.id}/guilds/$guildId/commands",
- "PUT",
- body: [
- for(final builder in builders)
- builder.build()
- ]
- );
-
- if (response is HttpResponseError) {
- yield* Stream.error(response);
- }
-
- for (final rawRes in (response as HttpResponseSuccess).jsonBody as List) {
- yield SlashCommand._new(rawRes as RawApiMap, this._client);
- }
- }
-
- @override
- Future deleteGlobalCommand(Snowflake applicationId, Snowflake commandId) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/commands/$commandId",
- "DELETE"
- );
-
- if (response is HttpResponseSuccess) {
- return Future.error(response);
- }
- }
-
- @override
- Future deleteGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/guilds/$guildId/commands/$commandId",
- "DELETE"
- );
-
- if (response is HttpResponseSuccess) {
- return Future.error(response);
- }
- }
-
- @override
- Future editGlobalCommand(Snowflake applicationId, Snowflake commandId, SlashCommandBuilder builder) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/commands/$commandId",
- "PATCH",
- body: builder.build()
- );
-
- if (response is HttpResponseSuccess) {
- return Future.error(response);
- }
-
- return SlashCommand._new((response as HttpResponseSuccess).jsonBody as RawApiMap, _client);
- }
-
- @override
- Future editGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId, SlashCommandBuilder builder) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/guilds/$guildId/commands/$commandId",
- "GET",
- body: builder.build()
- );
-
- if (response is HttpResponseSuccess) {
- return Future.error(response);
- }
-
- return SlashCommand._new((response as HttpResponseSuccess).jsonBody as RawApiMap, _client);
- }
-
- @override
- Future fetchGlobalCommand(Snowflake applicationId, Snowflake commandId) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/commands/$commandId",
- "GET"
- );
-
- if (response is HttpResponseSuccess) {
- return Future.error(response);
- }
-
- return SlashCommand._new((response as HttpResponseSuccess).jsonBody as RawApiMap, _client);
- }
-
- @override
- Stream fetchGlobalCommands(Snowflake applicationId) async* {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/commands",
- "GET"
- );
-
- if (response is HttpResponseError) {
- yield* Stream.error(response);
- }
-
- for (final commandSlash in (response as HttpResponseSuccess).jsonBody as List) {
- yield SlashCommand._new(commandSlash as RawApiMap, _client);
- }
- }
-
- @override
- Future fetchGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId) async {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/guilds/$guildId/commands/$commandId",
- "GET"
- );
-
- if (response is HttpResponseSuccess) {
- return Future.error(response);
- }
-
- return SlashCommand._new((response as HttpResponseSuccess).jsonBody as RawApiMap, _client);
- }
-
- @override
- Stream fetchGuildCommands(Snowflake applicationId, Snowflake guildId) async* {
- final response = await this._client.httpEndpoints.sendRawRequest(
- "/applications/$applicationId/guilds/$guildId/commands",
- "GET"
- );
-
- if (response is HttpResponseError) {
- yield* Stream.error(response);
- }
-
- for (final commandSlash in (response as HttpResponseSuccess).jsonBody as List) {
- yield SlashCommand._new(commandSlash as RawApiMap, _client);
- }
- }
-
- @override
- Future bulkOverrideGlobalCommandsPermissions(Snowflake applicationId, Iterable builders) async {
- final globalBody = builders
- .where((builder) => builder.permissions != null && builder.permissions!.isNotEmpty)
- .map((builder) => {
- "id": builder._id.toString(),
- "permissions": [for (final permsBuilder in builder.permissions!) permsBuilder.build()]
- })
- .toList();
-
- await this._client.httpEndpoints
- .sendRawRequest("/applications/$applicationId/commands/permissions", "PUT", body: globalBody);
- }
-
- @override
- Future bulkOverrideGuildCommandsPermissions(Snowflake applicationId, Snowflake guildId, Iterable builders) async {
- final guildBody = builders
- .where((b) => b.permissions != null && b.permissions!.isNotEmpty)
- .map((builder) => {
- "id": builder._id.toString(),
- "permissions": [for (final permsBuilder in builder.permissions!) permsBuilder.build()]
- })
- .toList();
-
- await this._client.httpEndpoints
- .sendRawRequest("/applications/$applicationId/guilds/$guildId/commands/permissions", "PUT", body: guildBody);
-
- }
-
- @override
- Future fetchFollowup(String token, Snowflake applicationId, Snowflake messageId) async {
- final result = await this._client.httpEndpoints.sendRawRequest(
- "/webhooks/$applicationId/$token/messages/${messageId.toString()}",
- "GET"
- );
-
- if (result is HttpResponseError) {
- return Future.error(result);
- }
-
- return EntityUtility.createMessage(_client, (result as HttpResponseSuccess).jsonBody as RawApiMap);
- }
-
- @override
- Future respondToAutocomplete(Snowflake interactionId, String token, List builders) async {
- final result = await this._client.httpEndpoints.sendRawRequest(
- "/interactions/${interactionId.toString()}/$token/callback",
- "POST",
- body: {
- "type": 8,
- "data": {
- "choices": [
- for (final builder in builders)
- builder.build()
- ]
- }
- }
- );
-
- if (result is HttpResponseError) {
- return Future.error(result);
- }
- }
-}
diff --git a/lib/src/internal/_EventController.dart b/lib/src/internal/_EventController.dart
deleted file mode 100644
index 0b6c2b1..0000000
--- a/lib/src/internal/_EventController.dart
+++ /dev/null
@@ -1,41 +0,0 @@
-part of nyxx_interactions;
-
-class _EventController implements Disposable {
- /// Emitted when a a slash command is sent.
- late final StreamController onSlashCommand;
-
- /// Emitted when a a slash command is sent.
- late final StreamController onSlashCommandCreated;
-
- /// Emitted when button event is sent
- late final StreamController onButtonEvent;
-
- /// Emitted when dropdown event is sent
- late final StreamController onMultiselectEvent;
-
- /// Emitted when autocomplete interaction event is sent
- late final StreamController onAutocompleteEvent;
-
- _EventController(Interactions _client) {
- this.onSlashCommand = StreamController.broadcast();
- _client.onSlashCommand = this.onSlashCommand.stream;
-
- this.onSlashCommandCreated = StreamController.broadcast();
- _client.onSlashCommandCreated = this.onSlashCommandCreated.stream;
-
- this.onButtonEvent = StreamController.broadcast();
- _client.onButtonEvent = this.onButtonEvent.stream;
-
- this.onMultiselectEvent = StreamController.broadcast();
- _client.onMultiselectEvent = this.onMultiselectEvent.stream;
-
- this.onAutocompleteEvent = StreamController.broadcast();
- _client.onAutocompleteEvent = this.onAutocompleteEvent.stream;
- }
-
- @override
- Future dispose() async {
- await this.onSlashCommand.close();
- await this.onSlashCommandCreated.close();
- }
-}
diff --git a/lib/src/internal/event_controller.dart b/lib/src/internal/event_controller.dart
new file mode 100644
index 0000000..9b37adb
--- /dev/null
+++ b/lib/src/internal/event_controller.dart
@@ -0,0 +1,87 @@
+import 'dart:async';
+
+import 'package:nyxx/nyxx.dart';
+
+import 'package:nyxx_interactions/src/events/interaction_event.dart';
+import 'package:nyxx_interactions/src/models/slash_command.dart';
+
+abstract class IEventController implements Disposable {
+ /// Emitted when a slash command is sent.
+ Stream get onSlashCommand;
+
+ /// Emitted when a button interaction is received.
+ Stream get onButtonEvent;
+
+ /// Emitted when a dropdown interaction is received.
+ Stream get onMultiselectEvent;
+
+ /// Emitted when a slash command is created by the user.
+ Stream get onSlashCommandCreated;
+
+ /// Emitted when a slash command is created by the user.
+ Stream get onAutocompleteEvent;
+}
+
+class EventController implements IEventController {
+ /// Emitted when a slash command is sent.
+ @override
+ late final Stream onSlashCommand;
+
+ /// Emitted when a button interaction is received.
+ @override
+ late final Stream onButtonEvent;
+
+ /// Emitted when a dropdown interaction is received.
+ @override
+ late final Stream onMultiselectEvent;
+
+ /// Emitted when a slash command is created by the user.
+ @override
+ late final Stream onSlashCommandCreated;
+
+ /// Emitted when a slash command is created by the user.
+ @override
+ late final Stream onAutocompleteEvent;
+
+ /// Emitted when a a slash command is sent.
+ late final StreamController onSlashCommandController;
+
+ /// Emitted when a a slash command is sent.
+ late final StreamController onSlashCommandCreatedController;
+
+ /// Emitted when button event is sent
+ late final StreamController onButtonEventController;
+
+ /// Emitted when dropdown event is sent
+ late final StreamController onMultiselectEventController;
+
+ /// Emitted when autocomplete interaction event is sent
+ late final StreamController onAutocompleteEventController;
+
+ /// Creates na instance of [EventController]
+ EventController() {
+ onSlashCommandController = StreamController.broadcast();
+ onSlashCommand = onSlashCommandController.stream;
+
+ onSlashCommandCreatedController = StreamController.broadcast();
+ onSlashCommandCreated = onSlashCommandCreatedController.stream;
+
+ onButtonEventController = StreamController.broadcast();
+ onButtonEvent = onButtonEventController.stream;
+
+ onMultiselectEventController = StreamController.broadcast();
+ onMultiselectEvent = onMultiselectEventController.stream;
+
+ onAutocompleteEventController = StreamController.broadcast();
+ onAutocompleteEvent = onAutocompleteEventController.stream;
+ }
+
+ @override
+ Future dispose() async {
+ await onSlashCommandController.close();
+ await onSlashCommandCreatedController.close();
+ await onButtonEventController.close();
+ await onMultiselectEventController.close();
+ await onAutocompleteEventController.close();
+ }
+}
diff --git a/lib/src/internal/interaction_endpoints.dart b/lib/src/internal/interaction_endpoints.dart
new file mode 100644
index 0000000..6fdf65a
--- /dev/null
+++ b/lib/src/internal/interaction_endpoints.dart
@@ -0,0 +1,354 @@
+import 'package:nyxx/nyxx.dart';
+import 'package:nyxx/src/core/message/message.dart';
+
+import 'package:nyxx_interactions/src/models/slash_command.dart';
+import 'package:nyxx_interactions/src/builders/slash_command_builder.dart';
+import 'package:nyxx_interactions/src/builders/arg_choice_builder.dart';
+
+abstract class IInteractionsEndpoints {
+ /// Sends followup for interaction with given [token]. IMessage will be created with [builder]
+ Future sendFollowup(String token, Snowflake applicationId, MessageBuilder builder, {bool hidden = false});
+
+ /// Fetches followup message from API
+ Future fetchFollowup(String token, Snowflake applicationId, Snowflake messageId);
+
+ /// Acknowledges interaction that response can be sent within next 15 mins.
+ /// Response will be ephemeral if [hidden] is set to true. To response to different interaction types
+ /// (slash command, button...) [opCode] is used.
+ Future acknowledge(String token, String interactionId, bool hidden, int opCode);
+
+ /// Response to interaction by editing original response. Used when interaction was acked before.
+ Future respondEditOriginal(String token, Snowflake applicationId, MessageBuilder builder, bool hidden);
+
+ /// Response to interaction by creating response. Used when interaction wasn't acked before.
+ Future respondCreateResponse(String token, String interactionId, MessageBuilder builder, bool hidden, int respondOpCode);
+
+ /// Fetch original interaction response.
+ Future fetchOriginalResponse(String token, Snowflake applicationId, String interactionId);
+
+ /// Edits original interaction response using [builder]
+ Future editOriginalResponse(String token, Snowflake applicationId, MessageBuilder builder);
+
+ /// Deletes original interaction response
+ Future deleteOriginalResponse(String token, Snowflake applicationId, String interactionId);
+
+ /// Deletes followup IMessage with given id
+ Future deleteFollowup(String token, Snowflake applicationId, Snowflake messageId);
+
+ /// Edits followup IMessage with given [messageId]
+ Future editFollowup(String token, Snowflake applicationId, Snowflake messageId, MessageBuilder builder);
+
+ /// Fetches global commands of application
+ Stream fetchGlobalCommands(Snowflake applicationId);
+
+ /// Fetches global command with given [commandId]
+ Future fetchGlobalCommand(Snowflake applicationId, Snowflake commandId);
+
+ /// Edits global command with given [commandId] using [builder]
+ Future editGlobalCommand(Snowflake applicationId, Snowflake commandId, SlashCommandBuilder builder);
+
+ /// Deletes global command with given [commandId]
+ Future