Compare commits

163 Commits

Author SHA1 Message Date
a3c5eb62ac Merge pull request 'add option ranges' (#20) from feat/option-ranges into dev
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Publish / Gradle-Publish (push) Successful in 43s
Test / Gradle-Test (push) Successful in 47s
Reviewed-on: #20
2025-03-22 18:42:28 +00:00
996f854ff7 feat(command): add wrapper implementations of option ranges
All checks were successful
github-mirror / push-github (push) Successful in 1m40s
Build / Gradle-Build (push) Successful in 33s
Test / Gradle-Test (push) Successful in 47s
2025-03-22 12:33:21 +01:00
d2eec8b07c feat(command): add option ranges to options 2025-03-22 12:31:25 +01:00
caa2ee7089 refactor(command): move both choices vars to different annotation
All checks were successful
github-mirror / push-github (push) Successful in 1m36s
Build / Gradle-Build (push) Successful in 31s
Publish / Gradle-Publish (push) Successful in 36s
Test / Gradle-Test (push) Successful in 47s
2025-03-18 09:33:53 +01:00
2e5979e6e4 Merge pull request 'migrate to slf4j' (#18) from migrate/slf4j into dev
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 1m45s
Publish / Gradle-Publish (push) Successful in 1m32s
Test / Gradle-Test (push) Successful in 1m38s
Reviewed-on: #18
2025-03-17 19:34:26 +00:00
ab1eb74e85 fix(logging) implement getQualifiedCallerName
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 35s
Test / Gradle-Test (push) Successful in 48s
2025-03-17 14:36:43 +01:00
a5737b9eaa feat(logging): add Fallback logging
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 31s
Test / Gradle-Test (push) Successful in 47s
2025-03-17 14:13:21 +01:00
faca21724c fix(logging): fix wrong getClass method. 2025-03-17 14:13:00 +01:00
4c5e28b679 feat(logging): replace log4j dependency with slf4j and replace imports
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 38s
Test / Gradle-Test (push) Successful in 51s
2025-03-17 10:52:24 +01:00
33f355e6ea Merge pull request 'improve wrapper and general structure' (#16) from improve/structure into dev
All checks were successful
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 36s
Publish / Gradle-Publish (push) Successful in 32s
Test / Gradle-Test (push) Successful in 48s
Reviewed-on: #16
No new features
2025-03-17 08:26:43 +00:00
d32ac62b4a fix(autocomplete): fix autocomplete using component context objects
All checks were successful
github-mirror / push-github (push) Successful in 1m46s
Build / Gradle-Build (push) Successful in 31s
Test / Gradle-Test (push) Successful in 59s
2025-03-17 08:37:11 +01:00
e7c35d9308 refactor(core): remove redundant distinct method and its uses
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Successful in 46s
2025-03-17 00:16:26 +01:00
d4a91f3251 fix(core): fix wrong equality method and refactor a bit 2025-03-17 00:15:10 +01:00
bce4ce7812 fix(wrapper): add condition for CommandRegisterer if api is null 2025-03-16 23:35:39 +01:00
bae077654e refactor(Discord4J): implement CommandRegisterer refactor
Some checks failed
github-mirror / push-github (push) Successful in 1m44s
Build / Gradle-Build (push) Successful in 55s
Test / Gradle-Test (push) Failing after 41s
2025-03-16 17:16:26 +01:00
203498de68 refactor(javacord): implement CommandRegisterer refactor 2025-03-16 17:07:14 +01:00
24df1731da refactor(command): add CommandRegisterer 2025-03-16 17:06:37 +01:00
e3fc10a1ce fix(util): ObjectListAggregator syntax 2025-03-16 02:48:24 +01:00
78cacb7eb6 feat(util): add ObjectListAggregator 2025-03-16 02:47:16 +01:00
7287d44645 refactor(util): add multiple key support to ObjectAggregator 2025-03-16 02:46:55 +01:00
630c8ddee5 feat(register): add convenience getter 2025-03-16 01:49:24 +01:00
4e27e6ce56 feat(struct): introduce ObjectAggregator 2025-03-16 01:36:23 +01:00
432cf78a2e add IdentifierProvider and wrapper implementations
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 33s
Test / Gradle-Test (push) Successful in 48s
2025-03-05 21:44:12 +01:00
f940f48566 move CommandConverter to wrapper package
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 31s
Test / Gradle-Test (push) Successful in 46s
2025-03-04 11:16:44 +01:00
f4ee258eb1 remove missing parent() call in SlashCommandDefinition#getSubCommands()
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 53s
Test / Gradle-Test (push) Successful in 52s
2025-03-03 23:40:06 +01:00
56b668851b add CommandConverter logic and wrapper implementations
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 37s
Test / Gradle-Test (push) Successful in 48s
2025-03-03 23:32:25 +01:00
823402e0cd fix base getSubCommands method 2025-03-03 23:32:07 +01:00
91b1df8d5b replace ExecutableCommandDefinition with InteractionIdentifiers
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 42s
Test / Gradle-Test (push) Successful in 45s
2025-03-03 12:42:04 +01:00
76d7e9ef4f Merge branch 'dev' into improve/structure
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 1m20s
Test / Gradle-Test (push) Successful in 49s
2025-03-01 22:59:40 +00:00
236c584da8 move Choice classes
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 10s
Test / Gradle-Test (push) Successful in 20s
2025-02-20 09:10:28 +01:00
f6db113deb add CommandConverter stub
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 10s
Test / Gradle-Test (push) Successful in 21s
2025-02-20 00:41:15 +01:00
9058629af5 remove redundant getFullSlashCommand
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 11s
Test / Gradle-Test (push) Successful in 21s
2025-02-19 21:50:54 +01:00
8f14b0feb9 split up getSubCommands 2025-02-19 21:49:12 +01:00
3e02e7b85b Merge pull request 'Migrate to Mockito' (#15) from migrate/mockito into dev
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 11s
Publish / Gradle-Publish (push) Successful in 11s
Test / Gradle-Test (push) Successful in 22s
Reviewed-on: #15
2025-02-19 19:22:39 +00:00
ba560cb909 remove deprecated manual mocks
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 10s
Test / Gradle-Test (push) Successful in 21s
2025-02-19 15:34:14 +01:00
6cdca411c6 migrate SlashCommandTest 2025-02-19 15:33:21 +01:00
3a39b268c1 migrate InteractionCheckTest 2025-02-19 14:39:48 +01:00
ca0ab13316 migrate ButtonTest to mockito
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 12s
Test / Gradle-Test (push) Successful in 21s
2025-02-19 14:10:45 +01:00
ded81983e3 migrate AutocompleteTest and add mockito ofc 2025-02-19 14:00:01 +01:00
92704ca230 add correct null handling for api objects 2025-02-19 13:56:45 +01:00
5d8f737481 Merge pull request 'add Discord4J Wrapper' (#14) from wrapper/d4j into dev
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 10s
Publish / Gradle-Publish (push) Successful in 11s
Test / Gradle-Test (push) Successful in 21s
Reviewed-on: #14
2025-02-19 12:43:38 +00:00
8943d6d4a4 add tests
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 18s
Test / Gradle-Test (push) Successful in 21s
2025-02-19 10:50:30 +01:00
842fcfe5ac add PermissionCheck 2025-02-19 10:50:19 +01:00
858fab5e32 fix: Message ContextObject returning Optional 2025-02-19 09:02:52 +01:00
20471fefea fix: logging causing NullPointerException 2025-02-19 09:01:42 +01:00
8d27ec28db fix: remove Test Line, allow api to be null, fix getCommandDefinition to also allow ChatInputAutoCompleteEvent 2025-02-18 11:21:32 +01:00
f81602f5db restructure and add discord4j test stub
Some checks failed
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 9s
Test / Gradle-Test (push) Failing after 10s
2025-02-17 23:59:08 +01:00
dbad157058 adjust actions to add new wrapper
Some checks failed
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 9s
Test / Gradle-Test (push) Failing after 13s
2025-02-17 20:24:40 +01:00
ad19ed6ada add First prototype of Discord4J wrapper
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 21s
Test / Gradle-Test (push) Successful in 14s
2025-02-17 20:13:31 +01:00
ca822909e3 fix: consistency 2025-02-17 19:55:03 +01:00
caeaec1926 add: fromValue method to SlashCommandOptionType 2025-02-17 19:45:18 +01:00
f4dbdc302d fix: improve consistency 2025-02-17 19:44:52 +01:00
fd87431d51 Merge remote-tracking branch 'origin/dev' into wrapper/d4j 2025-02-17 13:06:27 +01:00
3b65784770 Merge pull request 'Add Choices and Autocomplete' (#8) from feat/choices into dev
All checks were successful
github-mirror / push-github (push) Successful in 30s
Build / Gradle-Build (push) Successful in 14s
Publish / Gradle-Publish (push) Successful in 10s
Test / Gradle-Test (push) Successful in 14s
Reviewed-on: #8
Double choices missing because PR will not get merged since Javacord seized development
2025-02-17 10:14:37 +00:00
b4af922ac2 change: getInteractionType check changed from class based to context based check
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 16s
Test / Gradle-Test (push) Successful in 14s
2025-02-17 09:15:54 +01:00
1146fa1cc6 Merge branch 'dev' into feat/choices
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 34s
Test / Gradle-Test (push) Successful in 41s
2024-12-20 19:30:35 +01:00
9d88ca902d fix NoSuchElement case on command with no options
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 35s
Publish / Gradle-Publish (push) Successful in 36s
Test / Gradle-Test (push) Successful in 43s
2024-12-20 19:27:36 +01:00
e9b5610f3c Merge pull request 'add Logging to various locations' (#11) from feat/logging into dev
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 37s
Publish / Gradle-Publish (push) Successful in 33s
Test / Gradle-Test (push) Successful in 40s
Reviewed-on: #11
2024-12-20 17:51:24 +00:00
8e872e7ada Merge branch 'dev' into feat/logging
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Successful in 40s
2024-12-20 17:05:18 +01:00
43c5946227 Merge branch 'dev' into feat/choices
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Successful in 40s
2024-12-20 17:04:13 +01:00
71c910ab93 fix equality check of ExecutableSlashCommand
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Publish / Gradle-Publish (push) Successful in 32s
Test / Gradle-Test (push) Successful in 39s
2024-12-20 17:03:35 +01:00
c5a7f3665e Merge branch 'dev' into feat/choices
All checks were successful
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Successful in 40s
2024-12-20 14:56:35 +01:00
3baf1eda9a log the result of an InteractionCheck
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Successful in 38s
2024-12-20 00:44:48 +01:00
628abb4239 fix order and wording
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Successful in 42s
2024-12-20 00:32:20 +01:00
9a89544a9e Merge branch 'dev' into feat/logging
All checks were successful
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 31s
Test / Gradle-Test (push) Successful in 41s
2024-12-19 22:11:21 +01:00
455fc4955f fix ExecutableSlashCommandDefinition toString method with all various checks
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 33s
Publish / Gradle-Publish (push) Successful in 33s
Test / Gradle-Test (push) Successful in 39s
2024-12-19 22:11:06 +01:00
b72c55fc6e Merge branch 'dev' into feat/logging
Some checks failed
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Failing after 39s
2024-12-19 21:23:48 +01:00
3691434f66 fix toString method of ExecutableSlashCommandDefinition and added subCommandGroup display
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Publish / Gradle-Publish (push) Successful in 33s
Test / Gradle-Test (push) Successful in 39s
2024-12-19 21:23:24 +01:00
74d55d81ca add remaining logging in Javacord wrapper and exceptions
Some checks failed
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 32s
Test / Gradle-Test (push) Failing after 41s
2024-12-19 21:12:36 +01:00
404f221ccf added Logging for InteractionChecks
Some checks failed
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 33s
Test / Gradle-Test (push) Failing after 40s
2024-12-19 21:07:42 +01:00
7249c99b69 add Logging to Parsers 2024-12-19 20:38:04 +01:00
b764972eba make use of new helper method in InteractionMethod 2024-12-19 20:15:22 +01:00
83ee4b1efa add method name helper method 2024-12-19 20:04:53 +01:00
6b86e9ff87 add initialization Logging 2024-12-19 19:41:49 +01:00
7a40aebd6d add Interactionmethod logging 2024-12-19 19:41:37 +01:00
76ab779ab2 fix position of Logging in InteractionRegistry
All checks were successful
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 35s
Test / Gradle-Test (push) Successful in 47s
2024-12-19 18:36:51 +01:00
3d19fae6b8 fix null issue in InteractionRegistry
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 33s
Test / Gradle-Test (push) Successful in 44s
2024-12-19 18:34:00 +01:00
1ecbc563a6 add Logging in InteractionRegistry
Some checks failed
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 34s
Test / Gradle-Test (push) Failing after 44s
2024-12-19 18:07:33 +01:00
bf0022775d add helper LoggerUtil method 2024-12-19 13:30:36 +01:00
a17f5e826f add LoggerUtil to decide which Logger to get
All checks were successful
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 35s
Test / Gradle-Test (push) Successful in 39s
2024-12-19 12:28:45 +01:00
3de0f32074 change to just javacord api dependency 2024-12-19 12:28:27 +01:00
aaf4f3297a add autocomplete option toggle, add double value for non javacord wrappers, rename OptionChoices to match with discords naming and general cleanup
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 31s
Test / Gradle-Test (push) Successful in 39s
2024-12-16 13:03:11 +01:00
445190db89 added Tests for choices 2024-12-16 12:49:51 +01:00
9d3a6b8b85 add Autocomplete Test
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 33s
Test / Gradle-Test (push) Successful in 39s
2024-12-15 23:15:37 +01:00
1cb6cd0e05 clean up code and switch to request instead of getting from cache 2024-12-15 23:15:29 +01:00
a5e1230fc6 fix issues with ExecutableSlashCommandDefinition equals check 2024-12-15 23:13:57 +01:00
432db43bf5 add remaining parts of AutocompleteInteraction
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 33s
Test / Gradle-Test (push) Successful in 40s
2024-12-15 15:09:09 +01:00
7a2c15d877 create seperate class for ContextObjectProviders and renamed some context parameters from parameter to context.
First parts of AutocompleteInteraction added
2024-12-15 15:08:34 +01:00
f32c7045a1 remove unnecessary getClass call fixing test
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 36s
Test / Gradle-Test (push) Successful in 40s
2024-12-14 12:32:03 +01:00
69b27e4554 add prototype choices implementation
Some checks failed
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 50s
Test / Gradle-Test (push) Failing after 41s
2024-12-13 10:50:30 +01:00
d8504a7cde Merge pull request 'add Checks system' (#6) from feat/checks into dev
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 11s
Publish / Gradle-Publish (push) Successful in 11s
Test / Gradle-Test (push) Successful in 16s
Reviewed-on: #6
2024-12-05 07:51:36 +00:00
bef34ee548 fix null issue
All checks were successful
github-mirror / push-github (push) Successful in 3s
Build / Gradle-Build (push) Successful in 12s
Test / Gradle-Test (push) Successful in 16s
2024-12-03 20:20:57 +01:00
29bb7e667e add PermissionCHeck test
Some checks failed
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 11s
Test / Gradle-Test (push) Failing after 16s
2024-12-02 21:21:00 +01:00
94da2a0e3c change logic to always give objects the highest value 2024-12-02 21:20:49 +01:00
7f47130461 add missing annotation annotations 2024-12-02 21:20:26 +01:00
83a3efd4b8 add canRun check which i forgor 2024-12-02 21:19:58 +01:00
aefd8a51a0 change wrong annotation class usage 2024-12-02 21:19:42 +01:00
4332592dfa add Javacord PermissionCheck
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 12s
Test / Gradle-Test (push) Successful in 17s
2024-12-02 13:16:28 +01:00
c363ab9744 add ability to return false to cancel execution in pre Checks and remove return type on post checks
All checks were successful
github-mirror / push-github (push) Successful in 7s
Build / Gradle-Build (push) Successful in 14s
Test / Gradle-Test (push) Successful in 21s
2024-12-01 13:06:42 +01:00
33392b02fb add InteractionCheck test
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 13s
Test / Gradle-Test (push) Successful in 15s
2024-11-29 21:41:46 +01:00
b7333c2e5e fix problem with multiple method overrides in generic types. 2024-11-29 21:41:18 +01:00
239e921e6f change Annotation#getClass to Annotation#annotationType because it was not working as expected 2024-11-29 21:37:13 +01:00
6eb7fb723f add geantyref to fix and simplify generic Type parsing.
Also switch to java 23 to avoid conflicts and issues.
2024-11-29 21:36:02 +01:00
659218682e add context Object to check methods and create the ability to have a specific method for each type of context or one for all by using the superclass and casting yourself
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 12s
Test / Gradle-Test (push) Successful in 17s
2024-11-29 18:17:33 +01:00
019ba8f552 add getMostSpecificMethod method to simplify Check method parsing.
This Method searches the The method that has the best matching parameters with the fewest inheritance levels as possible. Left sided priority
2024-11-29 18:15:47 +01:00
f89ae5e425 - add prototype Interactioncheck impementation.
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 12s
Test / Gradle-Test (push) Successful in 16s
- refactor dependency injection to have all widely used dependencies in the Marinara class.
2024-11-28 10:32:48 +01:00
582e0f0bae implement AnnotationParser system
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 13s
Test / Gradle-Test (push) Successful in 16s
2024-11-24 00:02:19 +01:00
0ea330d48b move to seperate files because gitea does not have expressions yet
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 9s
Publish / Gradle-Publish (push) Successful in 10s
Test / Gradle-Test (push) Successful in 13s
2024-11-20 12:01:47 +01:00
c241f6b1fe enable dev branch publishing
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 17s
Publish / Gradle-Publish (push) Successful in 6s
Test / Gradle-Test (push) Successful in 13s
2024-11-20 11:52:41 +01:00
4ed5482b63 Merge pull request 'add package publishing and CI/CD using Gitea actions' (#5) from feat/ci-cd into master
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 1m32s
Test / Gradle-Test (push) Successful in 21s
Reviewed-on: #5
2024-11-16 12:41:24 +00:00
4d20dd65f2 upload test results instead of non working publishing
All checks were successful
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 1m9s
Test / Gradle-Test (push) Successful in 1m15s
2024-11-15 11:46:58 +01:00
2ab30a2bc1 switch to comma seperation
Some checks failed
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 1m9s
Test / Gradle-Test (push) Failing after 1m19s
2024-11-14 14:32:55 +01:00
420408fbdf switch to java-junit reporter
Some checks failed
github-mirror / push-github (push) Successful in 4s
Build / Gradle-Build (push) Successful in 1m8s
Test / Gradle-Test (push) Failing after 1m17s
2024-11-14 14:11:57 +01:00
df10c54767 fix variable usage
Some checks failed
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 2m1s
Test / Gradle-Test (push) Failing after 1m16s
2024-11-14 13:54:12 +01:00
18d85edcf2 fix formatting
Some checks are pending
Test / Gradle-Test (push) Waiting to run
github-mirror / push-github (push) Successful in 5s
Build / Gradle-Build (push) Successful in 1m11s
2024-11-14 13:05:36 +01:00
b371ea8520 add setup gradle step
All checks were successful
github-mirror / push-github (push) Successful in 6s
Build / Gradle-Build (push) Successful in 2m2s
2024-11-14 12:45:43 +01:00
b28406b16b update artifact directory structure
All checks were successful
github-mirror / push-github (push) Successful in 6s
Build / Gradle-Build (push) Successful in 1m6s
2024-11-14 12:38:31 +01:00
f31292c5b7 switch artifact action to gitea one
All checks were successful
github-mirror / push-github (push) Successful in 11s
Build / Gradle Build (push) Successful in 7m57s
2024-11-14 12:23:41 +01:00
de83137886 add first version of CI with build, test and publish
Some checks failed
github-mirror / push-github (push) Successful in 4s
Build / Gradle Build (push) Failing after 7m19s
2024-11-14 11:53:08 +01:00
fd3933df25 add publications
All checks were successful
github-mirror / push-github (push) Successful in 4s
2024-11-13 10:22:25 +01:00
c00b6033cc rename package in wrapper-javacord
All checks were successful
github-mirror / push-github (push) Successful in 4s
2024-11-13 10:08:55 +01:00
186fa10684 Merge pull request 'Add Button Interaction Implementation' (#4) from feat/buttons into master
All checks were successful
github-mirror / push-github (push) Successful in 4s
Reviewed-on: #4
2024-11-08 13:41:21 +00:00
7d378e6451 set fetch depth 0
All checks were successful
github-mirror / push-github (push) Successful in 4s
2024-11-07 12:42:09 +01:00
f835cfa500 change runs-on
Some checks failed
github-mirror / push-github (push) Failing after 4s
2024-11-07 12:22:41 +01:00
cb8b799000 add github mirror action
Some checks are pending
github-mirror / push-github (push) Waiting to run
2024-11-07 12:21:14 +01:00
fcc99597e6 remove unused imports 2024-11-07 11:51:43 +01:00
d457e57614 Add Button Execution Test 2024-11-07 11:51:02 +01:00
8a7348eebc fix class names and add Button Interaction type case 2024-11-07 11:50:52 +01:00
480c1ec81f add Button Interaction Method case to factory method 2024-11-07 11:50:09 +01:00
3d2bca4548 fix off by one error 2024-11-07 11:49:45 +01:00
5689fef866 remove unused import 2024-11-07 11:49:30 +01:00
7d9e14eb25 add retention and target and change the annotation parameter to "value" for easier usage 2024-11-06 14:43:49 +01:00
7fa402ac38 ignore bin directories 2024-11-06 14:43:11 +01:00
eaff502892 remove unused imports 2024-11-06 14:29:42 +01:00
76d4d5b525 add Javacord impl 2024-11-05 15:55:01 +01:00
0ce0b3eb4f Add Button Interaction with specific method parameter support. 2024-11-05 15:54:49 +01:00
11fd16fa77 Add Command Execution test 2024-11-01 17:38:02 +01:00
bbeb58e5e4 fix isAssignableFrom order 2024-11-01 17:37:49 +01:00
e39ac2d67e fix random nulls in list 2024-11-01 17:37:36 +01:00
0f337696c1 change to selective catching 2024-11-01 17:36:12 +01:00
9f87f47b1f Add Javacord test 2024-11-01 16:07:18 +01:00
9d81522429 switch to user create wrapper instances 2024-11-01 16:07:10 +01:00
7888819f6e rework class invariant checks and cast issues 2024-11-01 16:06:52 +01:00
3d5201329b Add missing initialisation and nonNull filter 2024-11-01 16:05:33 +01:00
4b835187b5 remove unused dependencies 2024-11-01 16:05:04 +01:00
3778f45cf3 fix wrong logic 2024-11-01 15:30:50 +01:00
6bd6021b86 finish first version of javacord wrapper with implementation of:
- getInteractionType()
- convertCommandOption()
- getCommandDefinition
2024-10-31 16:06:23 +01:00
b0abb423d3 add register impl 2024-10-29 23:20:19 +01:00
22f9810f7b Add first version of command registration code 2024-10-29 23:08:15 +01:00
9768572577 add javacord dependency 2024-10-29 23:03:29 +01:00
2cdf574df0 add base structure 2024-10-29 23:03:29 +01:00
0c3aeed4f4 Add getFullSlashCommand() method to get the SlashCommand annotation instance that has options if it is a rootcommand 2024-10-29 23:03:09 +01:00
0615741def ae7e66d06d
rewrite both methods in a more efficient way
2024-10-29 20:58:08 +01:00
1aa052a85b minor refactoring and addition of isRootCommand() to SlashCommandDefinition 2024-10-29 19:51:37 +01:00
19bf9d24bc fix further naming
related to df94601816
2024-10-29 15:42:22 +01:00
ae7e66d06d Add Helper methods for easier command registration 2024-10-26 19:43:31 +02:00
df94601816 fix naming 2024-10-26 19:13:27 +02:00
d86c307166 rename and removal of ability for multiple subcommandgroups because I was dumb 2024-10-26 01:37:07 +02:00
f4a6bf937d rename to make more sense x2 2024-10-26 00:56:27 +02:00
11ebb3fdea rename to make more sense 2024-10-26 00:43:05 +02:00
fd749b31d8 add getUniqueExecutableDefinitions to ApplicationCommandDefinition 2024-10-25 21:04:10 +02:00
cf45d0e8ea remove unused import 2024-10-25 20:47:54 +02:00
4a13899975 change to register all commands at once to use bulk overwrite features 2024-10-25 20:47:06 +02:00
95 changed files with 3653 additions and 325 deletions

View File

@@ -0,0 +1,17 @@
name: github-mirror
on: push
jobs:
push-github:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push to GitHub
uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@github.com:EkiciLP/Marinara.git
ssh_private_key:
${{ secrets.MIRROR_SSH_PRIVATE_KEY }}

View File

@@ -0,0 +1,50 @@
name: Build
on:
push:
jobs:
Gradle-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '23'
check-latest: true
distribution: 'zulu'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary: always
cache-cleanup: on-success
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build
run: chmod +x gradlew; ./gradlew assemble
- name: Move artifacts
run: |
mkdir artifacts; mv lib/build/libs/*.jar artifacts;
mv wrapper/javacord/build/libs/*.jar artifacts;
mv wrapper/discord4j/build/libs/*.jar artifacts;
- name: Upload artifact
uses: christopherhx/gitea-upload-artifact@v4
with:
name: artifacts
path: artifacts

View File

@@ -0,0 +1,40 @@
name: Publish
on:
push:
branches: [dev]
jobs:
Gradle-Publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '23'
check-latest: true
distribution: 'zulu'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary: always
cache-cleanup: on-success
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Publish Dev
env:
GITEA_TOKEN: ${{ secrets.PUBLISH_PACKAGE_TOKEN }}
run: chmod +x gradlew; ./gradlew publishAllPublicationsToGiteaRepository

View File

@@ -0,0 +1,40 @@
name: Publish
on:
push:
branches: [master]
jobs:
Gradle-Publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '23'
check-latest: true
distribution: 'zulu'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary: always
cache-cleanup: on-success
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Publish Release
env:
GITEA_TOKEN: ${{ secrets.PUBLISH_PACKAGE_TOKEN }}
run: chmod +x gradlew; ./gradlew publishAllPublicationsToGiteaRepository -Prelease

View File

@@ -0,0 +1,54 @@
name: Test
on:
push:
jobs:
Gradle-Test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '23'
check-latest: true
distribution: 'zulu'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary: always
cache-cleanup: on-success
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Test
env:
DISCORD_TEST_TOKEN: ${{ secrets.DISCORD_TOKEN }}
run: chmod +x gradlew; ./gradlew test
- name: Move Test Results
if: always()
run: |
mkdir test-results/;
[ -d lib/build/test-results/test/ ] && mv lib/build/test-results/test/*.xml test-results/;
[ -d wrapper/javacord/build/test-results/test/ ] && mv wrapper/javacord/build/test-results/test/*.xml test-results/;
[ -d wrapper/discord4j/build/test-results/test/ ] && mv wrapper/discord4j/build/test-results/test/*.xml test-results/;
- name: Upload Test Result
uses: christopherhx/gitea-upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
.vscode .vscode
lib/bin
wrapper/javacord/bin
wrapper/discord4j/bin

53
build.gradle.kts Normal file
View File

@@ -0,0 +1,53 @@
import java.io.ByteArrayOutputStream
plugins {
`maven-publish`
}
allprojects {
group = "net.tomatentum.Marinara"
version = "1.0.0-RC1" + (if (!project.hasProperty("release")) ("-" + getGitHash()) else "")
description = "A simple but powerful, library-agnostic Discord Interaction Wrapper."
}
subprojects {
apply(plugin = "maven-publish")
apply(plugin = "java-library")
plugins.withType<MavenPublishPlugin> {
publishing {
publications {
create<MavenPublication>("maven") {
if (!project.hasProperty("release"))
artifactId = project.getName() + "-dev"
from(components["java"])
}
}
repositories {
maven {
name = "Gitea"
url = uri("https://git.tomatentum.net/api/packages/tueem/maven/")
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = "token " + System.getenv("GITEA_TOKEN")
}
authentication {
create<HttpHeaderAuthentication>("header")
}
}
}
}
}
}
fun getGitHash(): String {
val output = ByteArrayOutputStream()
project.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
standardOutput = output
}
return output.toString().trim()
}

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
org.gradle.caching=true

View File

@@ -2,13 +2,17 @@
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions] [versions]
commons-math3 = "3.6.1"
guava = "33.0.0-jre"
junit-jupiter = "5.10.2" junit-jupiter = "5.10.2"
log4j = "2.24.1" slf4j = "2.0.17"
javacord = "3.8.0"
discord4j = "3.2.7"
geantyref = "2.0.0"
mockito = "5.15.2"
[libraries] [libraries]
commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j"} slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j"}
javacord = { module = "org.javacord:javacord", version.ref = "javacord"}
discord4j = { module = "com.discord4j:discord4j-core", version.ref = "discord4j"}
geantyref = { module = "io.leangen.geantyref:geantyref", version.ref = "geantyref"}
mockito = {module = "org.mockito:mockito-core", version.ref = "mockito"}

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -20,14 +20,14 @@ dependencies {
testImplementation(libs.junit.jupiter) testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation(libs.log4j) implementation(libs.slf4j)
implementation(libs.geantyref)
} }
// Apply a specific Java toolchain to ease working on different environments. // Apply a specific Java toolchain to ease working on different environments.
java { java {
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(23)
} }
} }

View File

@@ -1,33 +1,40 @@
package net.tomatentum.marinara; package net.tomatentum.marinara;
import java.lang.reflect.Constructor; import org.slf4j.Logger;
import net.tomatentum.marinara.registry.InteractionCheckRegistry;
import net.tomatentum.marinara.registry.InteractionRegistry; import net.tomatentum.marinara.registry.InteractionRegistry;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.wrapper.LibraryWrapper; import net.tomatentum.marinara.wrapper.LibraryWrapper;
public class Marinara { public class Marinara {
public static <T extends LibraryWrapper> Marinara load(Class<T> clazz) { private Logger logger = LoggerUtil.getLogger(getClass());
try {
Constructor<T> ctor = clazz.getConstructor(); public static <T extends LibraryWrapper> Marinara load(LibraryWrapper wrapper) {
ctor.setAccessible(true); return new Marinara(wrapper);
T wrapper = ctor.newInstance();
InteractionRegistry registry = new InteractionRegistry(wrapper);
return new Marinara(registry);
}catch (Exception ex) {
System.err.println(ex);
System.exit(100);
return null;
}
} }
private InteractionRegistry registry; private InteractionRegistry registry;
private InteractionCheckRegistry checkRegistry;
private LibraryWrapper wrapper;
private Marinara(InteractionRegistry registry) { private Marinara(LibraryWrapper wrapper) {
this.registry = registry; this.wrapper = wrapper;
this.registry = new InteractionRegistry(this);
this.checkRegistry = new InteractionCheckRegistry();
logger.info("Marinara loaded successfully!");
} }
public InteractionRegistry getRegistry() { public InteractionRegistry getRegistry() {
return registry; return registry;
} }
public InteractionCheckRegistry getCheckRegistry() {
return checkRegistry;
}
public LibraryWrapper getWrapper() {
return wrapper;
}
} }

View File

@@ -0,0 +1,50 @@
package net.tomatentum.marinara.checks;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.slf4j.Logger;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ReflectionUtil;
public record AppliedCheck(InteractionCheck<?> check, Annotation annotation) {
private static Logger logger = LoggerUtil.getLogger(AppliedCheck.class);
public boolean pre(Object context) {
Method[] methods = Arrays.stream(check.getClass().getMethods())
.filter(x -> x.getName().equals("preExec"))
.filter(x -> !x.isBridge())
.toArray(s -> new Method[s]);
Method method = ReflectionUtil.getMostSpecificMethod(methods, context.getClass(), annotation.annotationType());
method.setAccessible(true);
try {
logger.debug("Executing pre check {} with context {}", check.getClass().getName(), context.toString());
boolean result = (boolean) method.invoke(check, context, annotation);
logger.debug("Pre Check {} {} with context {}", check.getClass().getName(), result ? "succeeded" : "failed", context.toString());
return result;
} catch (IllegalAccessException | InvocationTargetException | SecurityException e) {
logger.error("Failed executing pre-check", e);
return false;
}
}
public void post(Object context) {
Method[] methods = Arrays.stream(check.getClass().getMethods())
.filter(x -> x.getName().equals("postExec"))
.filter(x -> !x.isBridge())
.toArray(s -> new Method[s]);
Method method = ReflectionUtil.getMostSpecificMethod(methods, context.getClass(), annotation.annotationType());
method.setAccessible(true);
try {
logger.debug("Executing post check {} with context {}", check.getClass().getName(), context.toString());
method.invoke(check, context, annotation);
} catch (IllegalAccessException | InvocationTargetException | SecurityException e) {
logger.error("Failed executing post-check", e);
}
}
}

View File

@@ -0,0 +1,10 @@
package net.tomatentum.marinara.checks;
import java.lang.annotation.Annotation;
public interface InteractionCheck<A extends Annotation> {
public boolean preExec(Object context, A annotation);
public void postExec(Object context, A annotation);
}

View File

@@ -0,0 +1,12 @@
package net.tomatentum.marinara.interaction.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoComplete {
}

View File

@@ -0,0 +1,12 @@
package net.tomatentum.marinara.interaction.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Button {
public String value(); //aka customId
}

View File

@@ -1,56 +0,0 @@
package net.tomatentum.marinara.interaction.commands;
import java.util.ArrayList;
import java.util.List;
import net.tomatentum.marinara.interaction.commands.annotation.ApplicationCommand;
public class ApplicationCommandDefinition {
private List<ExecutableCommandDefinition> executableDefinitons;
private ApplicationCommand applicationCommand;
private int subCommandGroupCount = -1;
private boolean isRootCommand = false;
public ApplicationCommandDefinition(ApplicationCommand applicationCommand) {
this.executableDefinitons = new ArrayList<>();
this.applicationCommand = applicationCommand;
}
public ApplicationCommandDefinition addExecutableCommand(ExecutableCommandDefinition def) {
if (this.subCommandGroupCount == -1)
this.subCommandGroupCount = def.subCommandGroups().length;
if (def.subCommandGroups().length != subCommandGroupCount)
throw new IllegalArgumentException(def + ": has a non matching amount of subcommand groups. All subcommands must have the same amount of subcommand groups!");
if (def.applicationCommand() != null) {
if (applicationCommand == null)
this.applicationCommand = def.applicationCommand();
if (!this.applicationCommand.equals(def.applicationCommand()))
throw new IllegalArgumentException(def + ": has a non matching Application Command description. Please edit it to equal all other descriptions or remove it to use other definitions descriptions");
}
if (isRootCommand) {
if (!def.isRootCommand())
throw new IllegalArgumentException(def + ": cannot have subcommands and rootcommand definitions together");
long subCommandAmount = executableDefinitons.stream()
.filter((x) -> !x.isRootCommand())
.count();
if (subCommandAmount > 0)
throw new IllegalArgumentException(def + ": cannot have subcommands and rootcommand definitions together");
}
executableDefinitons.add(def);
return this;
}
public ApplicationCommand getApplicationCommand() {
return applicationCommand;
}
public List<ExecutableCommandDefinition> getExecutableDefinitons() {
return executableDefinitons;
}
public int getSubCommandGroupCount() {
return subCommandGroupCount;
}
}

View File

@@ -1,73 +0,0 @@
package net.tomatentum.marinara.interaction.commands;
import net.tomatentum.marinara.interaction.commands.annotation.ApplicationCommand;
import net.tomatentum.marinara.interaction.commands.annotation.CommandOption;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommand;
public record ExecutableCommandDefinition(
ApplicationCommand applicationCommand,
SubCommand subCommand,
String[] subCommandGroups,
CommandOption[] options) {
@Override
public final boolean equals(Object o) {
if (!(o instanceof ExecutableCommandDefinition))
return false;
ExecutableCommandDefinition other = (ExecutableCommandDefinition) o;
return other.applicationCommand.name().equals(this.applicationCommand.name()) &&
other.subCommandGroups.equals(this.subCommandGroups) &&
other.subCommand.equals(this.subCommand);
}
@Override
public final String toString() {
return applicationCommand.name() + subCommand.name() != null ? "::" + subCommand.name() : "";
}
public boolean isRootCommand() {
return subCommand == null;
}
public static class Builder {
private ApplicationCommand applicationCommand;
private SubCommand subCommand;
private String[] subCommandGroupNames;
public Builder() {
this.subCommandGroupNames = new String[0];
}
public ExecutableCommandDefinition build() {
if (applicationCommand == null)
throw new IllegalArgumentException("applicationCommandName cant be null");
return new ExecutableCommandDefinition(applicationCommand, subCommand, subCommandGroupNames, subCommand != null ? subCommand.options() : applicationCommand.options());
}
public void setApplicationCommand(ApplicationCommand applicationCommand) {
this.applicationCommand = applicationCommand;
}
public void setSubCommand(SubCommand subCommand) {
this.subCommand = subCommand;
}
public void setSubCommandGroupNames(String[] subCommandGroupNames) {
this.subCommandGroupNames = subCommandGroupNames;
}
public ApplicationCommand getApplicationCommand() {
return applicationCommand;
}
public SubCommand getSubCommand() {
return subCommand;
}
public String[] getSubCommandGroupNames() {
return subCommandGroupNames;
}
}
}

View File

@@ -0,0 +1,115 @@
package net.tomatentum.marinara.interaction.commands;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import net.tomatentum.marinara.interaction.commands.annotation.CommandChoices;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption.PlaceHolderEnum;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOptionChoice;
import net.tomatentum.marinara.interaction.commands.choice.EnumChoices;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.ident.RootCommandIdentifier;
import net.tomatentum.marinara.interaction.ident.SlashCommandIdentifier;
import net.tomatentum.marinara.util.LoggerUtil;
public class SlashCommandDefinition {
public static SlashCommandOptionChoice[] getActualChoices(SlashCommandOption option) {
CommandChoices choices = option.choices();
SlashCommandOptionChoice[] actualChoices = choices.value();
if (choices.value().length <= 0 && !choices.cenum().equals(PlaceHolderEnum.class))
actualChoices = EnumChoices.of(choices.cenum()).choices();
return actualChoices;
}
private Set<InteractionIdentifier> entries;
private RootCommandIdentifier rootIdentifier;
private boolean isRootCommand;
private Logger logger = LoggerUtil.getLogger(getClass());
public SlashCommandDefinition(RootCommandIdentifier rootIdentifier) {
this.entries = new HashSet<>();
this.rootIdentifier = rootIdentifier;
this.isRootCommand = false;
}
public SlashCommandDefinition addIdentifier(InteractionIdentifier identifier) {
RootCommandIdentifier rootIdentifier = (RootCommandIdentifier) identifier.rootNode();
if (!this.rootIdentifier.equals(rootIdentifier))
throw new IllegalArgumentException("Root Node did not match.");
if (this.rootIdentifier.description() == null)
this.rootIdentifier = rootIdentifier;
if (!isRootCommand)
this.isRootCommand = identifier.parent() == null ? true : false;
if ((isRootCommand && identifier.parent() != null) || (!isRootCommand && identifier.parent() == null)) {
throw new IllegalArgumentException(identifier.toString() + ": cannot have subcommands and rootcommand definitions together");
}
entries.add(identifier);
this.logger.debug("Added identifer {} to command {}", identifier, rootIdentifier);
return this;
}
public SlashCommandIdentifier[] getSubCommandGroups() {
if (isRootCommand)
return null;
List<InteractionIdentifier> subCommandGroups = entries().stream()
.filter(x -> x.parent().parent() != null)
.map(x -> x.parent())
.toList();
return subCommandGroups.toArray(SlashCommandIdentifier[]::new);
}
public SlashCommandIdentifier[] getSubCommands() {
if (isRootCommand)
return null;
return entries.stream().filter(x -> x.parent() instanceof RootCommandIdentifier).toArray(SlashCommandIdentifier[]::new);
}
public SlashCommandIdentifier[] getSubCommands(String groupName) {
if (isRootCommand)
return null;
List<InteractionIdentifier> subCommands = entries().stream()
.filter(x -> x.parent().parent() != null && x.parent().name().equals(groupName))
.map(x -> x.parent().parent())
.toList();
return subCommands.toArray(SlashCommandIdentifier[]::new);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SlashCommandDefinition))
return false;
SlashCommandDefinition other = (SlashCommandDefinition) obj;
return this.rootIdentifier().equals(other.rootIdentifier());
}
public long[] serverIds() {
return rootIdentifier().serverIds();
}
public Set<InteractionIdentifier> entries() {
return this.entries;
}
public RootCommandIdentifier rootIdentifier() {
return rootIdentifier;
}
public boolean isRootCommand() {
return isRootCommand;
}
}

View File

@@ -0,0 +1,8 @@
package net.tomatentum.marinara.interaction.commands.annotation;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption.PlaceHolderEnum;
public @interface CommandChoices {
public SlashCommandOptionChoice[] value() default {};
public Class<? extends Enum<?>> cenum() default PlaceHolderEnum.class;
}

View File

@@ -0,0 +1,6 @@
package net.tomatentum.marinara.interaction.commands.annotation;
public @interface Range {
public double min() default Double.MIN_VALUE;
public double max() default Double.MAX_VALUE;
}

View File

@@ -7,10 +7,9 @@ import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE}) @Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationCommand { public @interface SlashCommand {
public String name(); public String name();
public String description() default ""; public String description() default "";
public String[] aliases() default {}; public SlashCommandOption[] options() default {};
public CommandOption[] options() default {};
public long[] serverIds() default {}; public long[] serverIds() default {};
} }

View File

@@ -5,13 +5,20 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import net.tomatentum.marinara.interaction.commands.option.OptionType; import net.tomatentum.marinara.interaction.commands.option.SlashCommandOptionType;
@Target({ElementType.ANNOTATION_TYPE}) @Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface CommandOption { public @interface SlashCommandOption {
public String name(); public String name();
public String description() default ""; public String description() default "";
public OptionType type() default OptionType.STRING; public SlashCommandOptionType type() default SlashCommandOptionType.STRING;
public boolean required() default false; public boolean required() default false;
public boolean autocomplete() default false;
public Range range() default @Range;
public CommandChoices choices() default @CommandChoices;
public static enum PlaceHolderEnum {
}
} }

View File

@@ -0,0 +1,8 @@
package net.tomatentum.marinara.interaction.commands.annotation;
public @interface SlashCommandOptionChoice {
public String name();
public long longValue() default Long.MAX_VALUE;
public double doubleValue() default Double.MAX_VALUE;
public String stringValue() default "";
}

View File

@@ -10,5 +10,5 @@ import java.lang.annotation.Target;
public @interface SubCommand { public @interface SubCommand {
public String name(); public String name();
public String description() default ""; public String description() default "";
public CommandOption[] options() default {}; public SlashCommandOption[] options() default {};
} }

View File

@@ -0,0 +1,5 @@
package net.tomatentum.marinara.interaction.commands.choice;
public interface ChoiceValueProvider<T> {
T getChoiceValue();
}

View File

@@ -0,0 +1,78 @@
package net.tomatentum.marinara.interaction.commands.choice;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import io.leangen.geantyref.AnnotationFormatException;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.geantyref.TypeFactory;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOptionChoice;
public record EnumChoices(Class<? extends Enum<?>> enumClass, ChoiceType type, SlashCommandOptionChoice[] choices) {
private static Method method;
static {
try {
method = ChoiceValueProvider.class.getMethod("getChoiceValue");
} catch (NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
}
public static EnumChoices of(Class<? extends Enum<?>> enumClass) {
if (!ChoiceValueProvider.class.isAssignableFrom(enumClass))
throw new IllegalArgumentException("Provided class needs to implement the ChoiceValueProvider interface.");
ChoiceType type = parseChoiceType(enumClass);
SlashCommandOptionChoice[] choices = parseChoices(enumClass, type);
return new EnumChoices(enumClass, type, choices);
}
private static ChoiceType parseChoiceType(Class<? extends Enum<?>> enumClass) {
ParameterizedType type = (ParameterizedType) GenericTypeReflector.getExactSuperType(enumClass, ChoiceValueProvider.class);
Type typeParam = type.getActualTypeArguments()[0];
if (!(typeParam instanceof Class<?>))
throw new IllegalArgumentException("ChoiceValueProvider need either a String or Number type parameter.");
if (Long.class.isAssignableFrom((Class<?>) typeParam))
return ChoiceType.INTEGER;
if (Double.class.isAssignableFrom((Class<?>) typeParam))
return ChoiceType.DOUBLE;
if (String.class.isAssignableFrom((Class<?>) typeParam))
return ChoiceType.String;
throw new IllegalArgumentException("ChoiceValueProvider need either a String, Number or Decimal type parameter.");
}
private static SlashCommandOptionChoice[] parseChoices(Class<? extends Enum<?>> enumClass, ChoiceType type) {
Enum<? extends Enum<?>>[] constants = enumClass.getEnumConstants();
List<SlashCommandOptionChoice> choices = new ArrayList<>();
for (Enum<? extends Enum<?>> enumInstance : constants) {
Object value;
try {
value = method.invoke(enumInstance);
if (type.equals(ChoiceType.INTEGER))
choices.add(TypeFactory.annotation(SlashCommandOptionChoice.class, Map.of("name", enumInstance.name(), "longValue", value)));
if (type.equals(ChoiceType.DOUBLE))
choices.add(TypeFactory.annotation(SlashCommandOptionChoice.class, Map.of("name", enumInstance.name(), "doubleValue", value)));
if (type.equals(ChoiceType.String))
choices.add(TypeFactory.annotation(SlashCommandOptionChoice.class, Map.of("name", enumInstance.name(), "stringValue", value)));
} catch (IllegalAccessException | InvocationTargetException | AnnotationFormatException e) {
e.printStackTrace();
return null;
}
}
return choices.toArray(SlashCommandOptionChoice[]::new);
}
public static enum ChoiceType {
String,
INTEGER,
DOUBLE
}
}

View File

@@ -1,16 +0,0 @@
package net.tomatentum.marinara.interaction.commands.option;
public enum OptionType {
ATTACHMENT,
BOOLEAN,
CHANNEL,
DECIMAL,
LONG,
MENTIONABLE,
ROLE,
STRING,
SUB_COMMAND,
SUB_COMMAND_GROUP,
UNKNOW,
USER
}

View File

@@ -0,0 +1,35 @@
package net.tomatentum.marinara.interaction.commands.option;
public enum SlashCommandOptionType {
SUB_COMMAND(1),
SUB_COMMAND_GROUP(2),
STRING(3),
INTEGER(4),
BOOLEAN(5),
USER(6),
CHANNEL(7),
ROLE(8),
MENTIONABLE(9),
DOUBLE(10),
ATTACHMENT(11),
UNKNOWN(-1);
private final int value;
private SlashCommandOptionType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static SlashCommandOptionType fromValue(int value) {
for (SlashCommandOptionType type : values()) {
if (type.getValue() == value) {
return type;
}
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,141 @@
package net.tomatentum.marinara.interaction.ident;
import java.util.Objects;
import net.tomatentum.marinara.interaction.InteractionType;
public class InteractionIdentifier {
public static InteractionIdentifier.Builder builder() {
return new InteractionIdentifier.Builder();
}
public static RootCommandIdentifier.Builder rootBuilder() {
return new RootCommandIdentifier.Builder();
}
public static SlashCommandIdentifier.Builder slashBuilder() {
return new SlashCommandIdentifier.Builder();
}
public static InteractionIdentifier createHierarchy(InteractionType type, String... names) {
InteractionIdentifier last = null;
for (String string : names) {
last = builder().name(string).type(type).parent(last).build();
}
return last;
}
public static void tryAddDescriptions(InteractionIdentifier receiver, InteractionIdentifier provider) {
if (receiver == null || provider == null)
return;
if (receiver.description().isBlank())
receiver.description = provider.description();
tryAddDescriptions(receiver.parent(), provider.parent());
}
private InteractionIdentifier parent;
private String name;
private String description;
private InteractionType type;
InteractionIdentifier(InteractionIdentifier parent, String name, String description, InteractionType type) {
this.parent = parent;
this.name = name;
this.description = description;
this.type = type;
}
public InteractionIdentifier rootNode() { return rootNode(this); }
private InteractionIdentifier rootNode(InteractionIdentifier identifier) {
if (identifier.parent() == null)
return identifier;
return rootNode(identifier.parent());
}
public String name() {
return name;
}
public String description() {
return description;
}
public InteractionIdentifier parent() {
return parent;
}
public InteractionType type() {
return type;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof InteractionIdentifier))
return false;
InteractionIdentifier ident = (InteractionIdentifier) obj;
if (!type().equals(ident.type()))
return false;
if (!name().equals(ident.name()))
return false;
return Objects.equals(ident, obj);
}
@Override
public String toString() {
if (parent() == null)
return name();
return "{}.{}".formatted(name(), parent().toString());
}
public static class Builder {
private InteractionIdentifier parent;
private String name;
private String description;
private InteractionType type;
public InteractionIdentifier parent() {
return parent;
}
public Builder parent(InteractionIdentifier parent) {
this.parent = parent;
return this;
}
public String name() {
return name;
}
public Builder name(String name) {
this.name = name;
return this;
}
public String description() {
return description;
}
public Builder description(String description) {
this.description = description;
return this;
}
public InteractionType type() {
return type;
}
public Builder type(InteractionType type) {
this.type = type;
return this;
}
public InteractionIdentifier build() {
return new InteractionIdentifier(parent, name, description, type);
}
}
}

View File

@@ -0,0 +1,90 @@
package net.tomatentum.marinara.interaction.ident;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
public class RootCommandIdentifier extends SlashCommandIdentifier {
private long[] serverIds;
public RootCommandIdentifier(
InteractionIdentifier parent,
String name,
String description,
InteractionType type,
SlashCommandOption[] options,
long[] serverIds) {
super(parent, name, description, type, options);
this.serverIds = serverIds;
}
public long[] serverIds() {
return serverIds;
}
public static class Builder {
private InteractionIdentifier parent;
private String name;
private String description;
private SlashCommandOption[] options;
private long[] serverIds;
public InteractionIdentifier parent() {
return parent;
}
public Builder parent(InteractionIdentifier parent) {
this.parent = parent;
return this;
}
public String name() {
return name;
}
public Builder name(String name) {
this.name = name;
return this;
}
public String description() {
return this.description;
}
public Builder description(String description) {
this.description = description;
return this;
}
public SlashCommandOption[] options() {
return this.options;
}
public Builder options(SlashCommandOption[] options) {
this.options = options;
return this;
}
public long[] serverIds() {
return this.serverIds;
}
public Builder serverIds(long[] serverIds) {
this.serverIds = serverIds;
return this;
}
public SlashCommandIdentifier build(boolean autocomplete) {
return new RootCommandIdentifier(
parent,
name,
description,
autocomplete ? InteractionType.AUTOCOMPLETE : InteractionType.COMMAND,
options,
serverIds);
}
}
}

View File

@@ -0,0 +1,78 @@
package net.tomatentum.marinara.interaction.ident;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
public class SlashCommandIdentifier extends InteractionIdentifier {
private SlashCommandOption[] options;
protected SlashCommandIdentifier(
InteractionIdentifier parent,
String name,
String description,
InteractionType type,
SlashCommandOption[] options
) {
super(parent, name, description, type);
this.options = options;
}
public SlashCommandOption[] options() {
return this.options;
}
public static class Builder {
private InteractionIdentifier parent;
private String name;
private String description;
private SlashCommandOption[] options;
public InteractionIdentifier parent() {
return parent;
}
public Builder parent(InteractionIdentifier parent) {
this.parent = parent;
return this;
}
public String name() {
return name;
}
public Builder name(String name) {
this.name = name;
return this;
}
public String description() {
return this.description;
}
public Builder description(String description) {
this.description = description;
return this;
}
public SlashCommandOption[] options() {
return this.options;
}
public Builder options(SlashCommandOption[] options) {
this.options = options;
return this;
}
public SlashCommandIdentifier build(boolean autocomplete) {
return new SlashCommandIdentifier(
parent,
name,
description,
autocomplete ? InteractionType.AUTOCOMPLETE : InteractionType.COMMAND,
options);
}
}
}

View File

@@ -0,0 +1,44 @@
package net.tomatentum.marinara.interaction.methods;
import java.lang.reflect.Method;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.parser.AnnotationParser;
import net.tomatentum.marinara.parser.SlashCommandParser;
public class AutoCompleteInteractionMethod extends InteractionMethod {
private InteractionIdentifier interactionIdentifier;
public AutoCompleteInteractionMethod(Method method,
InteractionHandler handler,
Marinara marinara
) {
super(method, handler, marinara);
}
@Override
public AnnotationParser[] parsers() {
return new AnnotationParser[] {
new SlashCommandParser(method, true, (x) -> { this.interactionIdentifier = x; } )
};
}
@Override
public Object getParameter(Object context, int index) {
Class<?> type = method().getParameterTypes()[index+1];
Object autocompleteOptionValue = marinara.getWrapper().getContextObjectProvider().getAutocompleteFocusedOption(context);
if (autocompleteOptionValue != null)
return autocompleteOptionValue;
return marinara.getWrapper().getContextObjectProvider().getInteractionContextObject(context, type);
}
@Override
public InteractionIdentifier identifier() {
return interactionIdentifier;
}
}

View File

@@ -0,0 +1,42 @@
package net.tomatentum.marinara.interaction.methods;
import java.lang.reflect.Method;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.parser.AnnotationParser;
import net.tomatentum.marinara.parser.ButtonParser;
public class ButtonInteractionMethod extends InteractionMethod {
private String customId;
ButtonInteractionMethod(Method method, InteractionHandler handler, Marinara marinara) {
super(method, handler, marinara);
}
@Override
public AnnotationParser[] parsers() {
return new AnnotationParser[] {
new ButtonParser(method, (x) -> { this.customId = x; } )
};
}
@Override
public Object getParameter(Object context, int index) {
Class<?> type = method().getParameterTypes()[index+1];
return marinara.getWrapper().getContextObjectProvider().getComponentContextObject(context, type);
}
@Override
public InteractionIdentifier identifier() {
return InteractionIdentifier.builder()
.name(customId)
.description("Button")
.type(InteractionType.BUTTON)
.build();
}
}

View File

@@ -1,63 +0,0 @@
package net.tomatentum.marinara.interaction.methods;
import java.lang.reflect.Method;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.commands.ExecutableCommandDefinition;
import net.tomatentum.marinara.interaction.commands.annotation.ApplicationCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommandGroup;
import net.tomatentum.marinara.util.ReflectionUtil;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
public class CommandInteractionMethod extends InteractionMethod {
private ExecutableCommandDefinition commandDefinition;
CommandInteractionMethod(Method method, InteractionHandler handler, LibraryWrapper wrapper) {
super(method, handler, wrapper);
parseMethod();
}
@Override
public Object getParameter(Object context, int index) {
return wrapper.convertCommandOption(context, commandDefinition.options()[index].type(), commandDefinition.options()[index].name());
}
@Override
public boolean canRun(Object context) {
ExecutableCommandDefinition other = wrapper.getCommandDefinition(context);
return commandDefinition.equals(other);
}
@Override
public InteractionType getType() {
return InteractionType.COMMAND;
}
public ExecutableCommandDefinition getCommandDefinition() {
return commandDefinition;
}
private void parseMethod() {
ReflectionUtil.checkValidCommandMethod(method);
ApplicationCommand cmd = ReflectionUtil.getAnnotation(method, ApplicationCommand.class);
ExecutableCommandDefinition.Builder builder = new ExecutableCommandDefinition.Builder();
builder.setApplicationCommand(cmd);
if (ReflectionUtil.isAnnotationPresent(method, SubCommandGroup.class)) {
SubCommandGroup cmdGroup = ReflectionUtil.getAnnotation(method, SubCommandGroup.class);
builder.setSubCommandGroupNames(cmdGroup.name().split(" "));
}
if (ReflectionUtil.isAnnotationPresent(method, SubCommand.class)) {
SubCommand subCmd = ReflectionUtil.getAnnotation(method, SubCommand.class);
builder.setSubCommand(subCmd);
}
this.commandDefinition = builder.build();
}
}

View File

@@ -1,65 +1,105 @@
package net.tomatentum.marinara.interaction.methods; package net.tomatentum.marinara.interaction.methods;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.slf4j.Logger;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.checks.AppliedCheck;
import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.InteractionType; import net.tomatentum.marinara.interaction.annotation.AutoComplete;
import net.tomatentum.marinara.interaction.commands.annotation.ApplicationCommand; import net.tomatentum.marinara.interaction.annotation.Button;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommand; import net.tomatentum.marinara.interaction.commands.annotation.SubCommand;
import net.tomatentum.marinara.wrapper.LibraryWrapper; import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.parser.AnnotationParser;
import net.tomatentum.marinara.parser.InteractionCheckParser;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ReflectionUtil;
public abstract class InteractionMethod { public abstract class InteractionMethod {
public static InteractionMethod create(Method method, InteractionHandler handler, LibraryWrapper wrapper) { public static InteractionMethod create(Method method, InteractionHandler handler, Marinara marinara) {
if (method.isAnnotationPresent(ApplicationCommand.class) || method.isAnnotationPresent(SubCommand.class)) if (method.isAnnotationPresent(AutoComplete.class))
return new CommandInteractionMethod(method, handler, wrapper); return new AutoCompleteInteractionMethod(method, handler, marinara);
if (method.isAnnotationPresent(SlashCommand.class) || method.isAnnotationPresent(SubCommand.class))
return new SlashCommandInteractionMethod(method, handler, marinara);
if (method.isAnnotationPresent(Button.class))
return new ButtonInteractionMethod(method, handler, marinara);
return null; return null;
} }
protected Method method; protected Method method;
protected InteractionHandler handler; protected InteractionHandler handler;
protected LibraryWrapper wrapper; protected Marinara marinara;
protected List<AnnotationParser> parsers;
protected List<AppliedCheck> appliedChecks;
protected InteractionMethod(Method method, InteractionHandler handler, LibraryWrapper wrapper) { private Logger logger = LoggerUtil.getLogger(getClass());
protected InteractionMethod(
Method method,
InteractionHandler handler,
Marinara marinara
) {
if (!Arrays.asList(handler.getClass().getMethods()).contains(method)) if (!Arrays.asList(handler.getClass().getMethods()).contains(method))
throw new InvalidParameterException("Method does not apply to specified handler"); throw new InvalidParameterException("Method does not apply to specified handler");
this.method = method; this.method = method;
this.handler = handler; this.handler = handler;
this.wrapper = wrapper; this.marinara = marinara;
this.parsers = new ArrayList<>(Arrays.asList(parsers()));
this.appliedChecks = new ArrayList<>();
parsers.add(new InteractionCheckParser(method, appliedChecks::add, marinara.getCheckRegistry()));
parsers.stream().forEach(AnnotationParser::parse);
} }
public abstract Object getParameter(Object parameter, int index);
public abstract boolean canRun(Object context);
public abstract InteractionType getType();
public void run(Object context) { public void run(Object context) {
if (this.appliedChecks.stream().filter(x -> !x.pre(context)).count() > 0)
return;
method.setAccessible(true);
try {
method.invoke(handler, getParameters(context));
}catch (IllegalAccessException | InvocationTargetException ex) {
logger.error("InteractionMethod failed to run", ex);
}
this.appliedChecks.forEach(x -> x.post(context));
}
public abstract AnnotationParser[] parsers();
public abstract Object getParameter(Object context, int index);
public abstract InteractionIdentifier identifier();
public Method method() {
return method;
}
private Object[] getParameters(Object context) {
int parameterCount = method.getParameterCount(); int parameterCount = method.getParameterCount();
List<Object> parameters = new ArrayList<>(); List<Object> parameters = new ArrayList<>();
for (int i = 0; i < parameterCount; i++) { for (int i = 0; i < parameterCount; i++) {
Object parameter;
if (i == 0) { if (i == 0) {
parameters.add(context); parameter = context;
continue; }else
} parameter = getParameter(context, i-1);
parameters.add(getParameter(context, i-1));
}
method.setAccessible(true);
try {
method.invoke(handler, parameters);
}catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public Method getMethod() { logger.trace("Found parameter {}={} for method {}", parameter != null ? parameter.getClass().toString() : " ", parameter, ReflectionUtil.getFullMethodName(method));
return method; parameters.add(parameter);
}
return parameters.toArray();
} }
} }

View File

@@ -0,0 +1,37 @@
package net.tomatentum.marinara.interaction.methods;
import java.lang.reflect.Method;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.ident.SlashCommandIdentifier;
import net.tomatentum.marinara.parser.AnnotationParser;
import net.tomatentum.marinara.parser.SlashCommandParser;
public class SlashCommandInteractionMethod extends InteractionMethod {
private SlashCommandIdentifier interactionIdentifier;
SlashCommandInteractionMethod(Method method, InteractionHandler handler, Marinara marinara) {
super(method, handler, marinara);
}
@Override
public AnnotationParser[] parsers() {
return new AnnotationParser[] {
new SlashCommandParser(method, false, (x) -> { this.interactionIdentifier = x; } )
};
}
@Override
public Object getParameter(Object context, int index) {
return marinara.getWrapper().getContextObjectProvider().convertCommandOption(context, interactionIdentifier.options()[index].name());
}
@Override
public InteractionIdentifier identifier() {
return interactionIdentifier;
}
}

View File

@@ -0,0 +1,8 @@
package net.tomatentum.marinara.parser;
import java.lang.reflect.Method;
public interface AnnotationParser {
void parse();
Method getMethod();
}

View File

@@ -0,0 +1,36 @@
package net.tomatentum.marinara.parser;
import java.lang.reflect.Method;
import java.util.function.Consumer;
import org.slf4j.Logger;
import net.tomatentum.marinara.interaction.annotation.Button;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ReflectionUtil;
public class ButtonParser implements AnnotationParser {
private Method method;
private Consumer<String> consumer;
private Logger logger = LoggerUtil.getLogger(getClass());
public ButtonParser(Method method, Consumer<String> consumer) {
this.method = method;
this.consumer = consumer;
}
@Override
public void parse() {
Button button = getMethod().getAnnotation(Button.class);
logger.trace("Parsed Button annotation {} for method {}", button.toString(), ReflectionUtil.getFullMethodName(method));
this.consumer.accept(button.value());
}
@Override
public Method getMethod() {
return this.method;
}
}

View File

@@ -0,0 +1,51 @@
package net.tomatentum.marinara.parser;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Consumer;
import org.slf4j.Logger;
import net.tomatentum.marinara.checks.AppliedCheck;
import net.tomatentum.marinara.checks.InteractionCheck;
import net.tomatentum.marinara.registry.InteractionCheckRegistry;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ReflectionUtil;
public class InteractionCheckParser implements AnnotationParser {
private InteractionCheckRegistry checkRegistry;
private Method method;
private Consumer<AppliedCheck> consumer;
private Logger logger = LoggerUtil.getLogger(getClass());
public InteractionCheckParser(Method method, Consumer<AppliedCheck> consumer, InteractionCheckRegistry checkRegistry) {
this.checkRegistry = checkRegistry;
this.method = method;
this.consumer = consumer;
}
@Override
public void parse() {
Annotation[] annotations = method.getAnnotations();
Arrays.stream(annotations).forEach(this::convertAnnotation);
}
private void convertAnnotation(Annotation annotation) {
Optional<InteractionCheck<?>> check = this.checkRegistry.getCheckFromAnnotation(annotation.annotationType());
if (check.isPresent()) {
AppliedCheck appliedCheck = new AppliedCheck(check.get(), annotation);
logger.trace("Parsed InteractionCheck {} for annotation {} for method {}", check.getClass().getName(), annotation.toString(), ReflectionUtil.getFullMethodName(method));
consumer.accept(appliedCheck);
}
}
@Override
public Method getMethod() {
return this.method;
}
}

View File

@@ -0,0 +1,86 @@
package net.tomatentum.marinara.parser;
import java.lang.reflect.Method;
import java.util.function.Consumer;
import org.slf4j.Logger;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommandGroup;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.ident.SlashCommandIdentifier;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ReflectionUtil;
public class SlashCommandParser implements AnnotationParser {
private Method method;
private boolean isAutoComplete;
private Consumer<SlashCommandIdentifier> consumer;
private Logger logger = LoggerUtil.getLogger(getClass());
public SlashCommandParser(Method method, boolean isAutoComplete, Consumer<SlashCommandIdentifier> consumer) {
this.method = method;
this.isAutoComplete = isAutoComplete;
this.consumer = consumer;
}
@Override
public void parse() {
this.checkValidCommandMethod(method);
SlashCommand cmd = ReflectionUtil.getAnnotation(method, SlashCommand.class);
InteractionIdentifier lastIdentifier = InteractionIdentifier.rootBuilder()
.name(cmd.name())
.description(cmd.description())
.options(cmd.options())
.serverIds(cmd.serverIds())
.build(isAutoComplete);
if (ReflectionUtil.isAnnotationPresent(method, SubCommandGroup.class)) {
SubCommandGroup cmdGroup = ReflectionUtil.getAnnotation(method, SubCommandGroup.class);
lastIdentifier = InteractionIdentifier.builder()
.name(cmdGroup.name())
.description(cmdGroup.description())
.type(isAutoComplete ? InteractionType.AUTOCOMPLETE : InteractionType.COMMAND)
.parent(lastIdentifier)
.build();
}
if (ReflectionUtil.isAnnotationPresent(method, SubCommand.class)) {
SubCommand subCmd = ReflectionUtil.getAnnotation(method, SubCommand.class);
lastIdentifier = InteractionIdentifier.slashBuilder()
.name(subCmd.name())
.description(subCmd.description())
.options(subCmd.options())
.build(isAutoComplete);
}
logger.trace("Parsed using SlashCommandParser for method {} with the result: {}", ReflectionUtil.getFullMethodName(method), lastIdentifier.toString());
consumer.accept((SlashCommandIdentifier) lastIdentifier);
}
@Override
public Method getMethod() {
return this.method;
}
private void checkValidCommandMethod(Method method) {
if (method.isAnnotationPresent(SlashCommand.class) &&
method.getDeclaringClass().isAnnotationPresent(SlashCommand.class)) {
throw new RuntimeException(method.getName() + ": Can't have ApplicationCommand Annotation on Class and Method");
}
if (!ReflectionUtil.isAnnotationPresent(method, SlashCommand.class))
throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Class or Method");
if ((method.isAnnotationPresent(SubCommand.class) &&
!ReflectionUtil.isAnnotationPresent(method, SlashCommand.class))) {
throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Method or Class");
}
}
}

View File

@@ -0,0 +1,40 @@
package net.tomatentum.marinara.registry;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import io.leangen.geantyref.GenericTypeReflector;
import net.tomatentum.marinara.checks.InteractionCheck;
import net.tomatentum.marinara.util.LoggerUtil;
public class InteractionCheckRegistry {
private List<InteractionCheck<?>> checks;
private Logger logger = LoggerUtil.getLogger(getClass());
public InteractionCheckRegistry() {
this.checks = new ArrayList<>();
}
public void addCheck(InteractionCheck<?> check) {
checks.add(check);
logger.info("Registered Check {}", check.getClass().getName());
}
public Optional<InteractionCheck<?>> getCheckFromAnnotation(Type annotation) {
for (InteractionCheck<?> interactionCheck : checks) {
ParameterizedType type = (ParameterizedType) GenericTypeReflector.getExactSuperType(interactionCheck.getClass(), InteractionCheck.class);
Type typeParam = type.getActualTypeArguments()[0];
if (typeParam.equals(annotation))
return Optional.of(interactionCheck);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,65 @@
package net.tomatentum.marinara.registry;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.methods.InteractionMethod;
import net.tomatentum.marinara.util.LoggerUtil;
public class InteractionEntry {
private InteractionIdentifier identifier;
private Set<InteractionMethod> methods;
private Logger logger = LoggerUtil.getLogger(getClass());
public InteractionEntry(InteractionIdentifier identifier) {
this.identifier = identifier;
this.methods = new HashSet<>();
}
public InteractionEntry addMethod(InteractionMethod method) {
if (!method.identifier().equals(this.identifier))
throw new IllegalArgumentException("Method's identifier did not match the entry's identifier");
this.methods.add(method);
InteractionIdentifier.tryAddDescriptions(identifier, method.identifier());
return this;
}
public void runAll(Object context) {
this.methods.stream().forEach(x -> {
logger.debug("Running Method {} from {} with context {}", x.toString(), this.toString(), context.toString());
x.run(context);
});
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof InteractionEntry))
return false;
InteractionEntry other = (InteractionEntry) obj;
return other.identifier().equals(identifier());
}
@Override
public String toString() {
return "InteractionEntry(%s)".formatted(identifier().toString());
}
public InteractionType type() {
return identifier().type();
}
public InteractionIdentifier identifier() {
return identifier;
}
public Set<InteractionMethod> methods() {
return methods;
}
}

View File

@@ -1,58 +1,80 @@
package net.tomatentum.marinara.registry; package net.tomatentum.marinara.registry;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.interaction.InteractionHandler; import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.InteractionType; import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.commands.ApplicationCommandDefinition; import net.tomatentum.marinara.interaction.commands.SlashCommandDefinition;
import net.tomatentum.marinara.interaction.commands.ExecutableCommandDefinition; import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.methods.CommandInteractionMethod; import net.tomatentum.marinara.interaction.ident.RootCommandIdentifier;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ObjectAggregator;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
import net.tomatentum.marinara.interaction.methods.InteractionMethod; import net.tomatentum.marinara.interaction.methods.InteractionMethod;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
public class InteractionRegistry { public class InteractionRegistry {
private List<InteractionMethod> interactionMethods; private Logger logger = LoggerUtil.getLogger(getClass());
private LibraryWrapper wrapper; private Set<InteractionEntry> interactions;
private Marinara marinara;
private IdentifierProvider identifierProvider;
public InteractionRegistry(LibraryWrapper wrapper) { public InteractionRegistry(Marinara marinara) {
this.wrapper = wrapper; this.interactions = new HashSet<>();
wrapper.subscribeInteractions(this::handle); this.marinara = marinara;
this.identifierProvider = marinara.getWrapper().createIdentifierProvider();
marinara.getWrapper().subscribeInteractions(this::handle);
} }
/*
* TODO: Maybe relocate InteractionEntry checking to another class with description merging.
*/
public void addInteractions(InteractionHandler interactionHandler) { public void addInteractions(InteractionHandler interactionHandler) {
for (Method method : interactionHandler.getClass().getMethods()) { for (Method method : interactionHandler.getClass().getMethods()) {
interactionMethods.add(InteractionMethod.create(method, interactionHandler, wrapper)); InteractionMethod iMethod = InteractionMethod.create(method, interactionHandler, marinara);
if (iMethod != null) {
Optional<InteractionEntry> oentry = this.interactions.stream()
.filter(i -> i.identifier().equals(iMethod.identifier()))
.findFirst();
InteractionEntry entry = oentry.orElse(new InteractionEntry(iMethod.identifier())).addMethod(iMethod);
if (oentry.isEmpty()) this.interactions.add(entry);
logger.debug("Added {} method from {}", iMethod.method().getName(), interactionHandler.getClass().getSimpleName());
} }
} }
logger.info("Added all Interactions from {}", interactionHandler.getClass().getSimpleName());
}
public void registerCommands() { public void registerCommands() {
List<ApplicationCommandDefinition> defs = new ArrayList<>(); List<InteractionIdentifier> slashIdentifiers = interactions.stream()
List<ExecutableCommandDefinition> execDefs = interactionMethods.stream() .filter((x) -> x.type().equals(InteractionType.COMMAND))
.filter((x) -> x.getClass().isAssignableFrom(CommandInteractionMethod.class)) .map((x) -> x.identifier())
.map((x) -> ((CommandInteractionMethod)x).getCommandDefinition())
.toList(); .toList();
execDefs.forEach((def) -> { SlashCommandDefinition[] defs = new ObjectAggregator<InteractionIdentifier, RootCommandIdentifier, SlashCommandDefinition>(
Optional<ApplicationCommandDefinition> appDef = defs.stream() i -> Arrays.asList((RootCommandIdentifier)i.rootNode()),
.filter((x) -> x.getApplicationCommand().equals(def.applicationCommand())) SlashCommandDefinition::addIdentifier,
.findFirst(); SlashCommandDefinition::new)
if (appDef.isPresent()) .aggregate(slashIdentifiers)
appDef.get().addExecutableCommand(def); .toArray(SlashCommandDefinition[]::new);
else
defs.add(new ApplicationCommandDefinition(def.applicationCommand()).addExecutableCommand(def));
});
defs.forEach(wrapper::registerApplicationCommand); marinara.getWrapper().getRegisterer().register(defs);
} }
public void handle(Object context) { public void handle(Object context) {
interactionMethods.forEach((m) -> { logger.debug("Received {} interaction ", context);
InteractionType type = wrapper.getInteractionType(context.getClass()); interactions.forEach((e) -> {
if (m.getType().equals(type)) if (this.identifierProvider.provide(context).equals(e.identifier())) {
m.run(context); logger.info("Running {} interaction using {}\ncontext: {}", e.type(), e.toString(), context.toString());
e.runAll(context);
}
}); });
} }
} }

View File

@@ -0,0 +1,17 @@
package net.tomatentum.marinara.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.NOPLoggerFactory;
public class LoggerUtil {
public static Logger getLogger(String name) {
if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory)
return new SimpleLogger(name);
return LoggerFactory.getLogger(name);
}
public static Logger getLogger(Class<?> clazz) {
return getLogger(clazz.getName());
}
}

View File

@@ -0,0 +1,46 @@
package net.tomatentum.marinara.util;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class ObjectAggregator<O, K, V> {
private Function<O, Iterable<K>> keySupplier;
private BiConsumer<V, O> valueConsumer;
private Function<K, V> defaultGenerator;
public ObjectAggregator(
Function<O, Iterable<K>> keySupplier,
BiConsumer<V, O> valueConsumer,
Function<K, V> defaultGenerator) {
this.keySupplier = keySupplier;
this.valueConsumer = valueConsumer;
this.defaultGenerator = defaultGenerator;
}
public ObjectAggregator(
Function<O, Iterable<K>> keySupplier,
BiConsumer<V, O> valueConsumer,
Supplier<V> defaultGenerator) {
this.keySupplier = keySupplier;
this.valueConsumer = valueConsumer;
this.defaultGenerator = _ -> defaultGenerator.get();
}
public Collection<V> aggregate(Iterable<O> iterator) {
Map<K, V> map = new HashMap<>();
for (O element : iterator) {
Iterable<K> keys = this.keySupplier.apply(element);
for (K key : keys) {
V value = map.getOrDefault(key, this.defaultGenerator.apply(key));
this.valueConsumer.accept(value, element);
map.putIfAbsent(key, value);
}
}
return map.values();
}
}

View File

@@ -0,0 +1,15 @@
package net.tomatentum.marinara.util;
import java.util.ArrayList;
import java.util.function.Function;
public class ObjectListAggregator<O, K, V> extends ObjectAggregator<O, K, ArrayList<V>> {
public ObjectListAggregator(Function<O, Iterable<K>> keySupplier, Function<O, V> valueConsumer) {
super(keySupplier,
(l, o) -> l.add(valueConsumer.apply(o)),
() -> new ArrayList<>());
}
}

View File

@@ -2,9 +2,10 @@ package net.tomatentum.marinara.util;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import net.tomatentum.marinara.interaction.commands.annotation.ApplicationCommand; import java.util.Arrays;
import net.tomatentum.marinara.interaction.commands.annotation.SubCommand; import java.util.List;
import java.util.Objects;
public final class ReflectionUtil { public final class ReflectionUtil {
@@ -21,19 +22,86 @@ public final class ReflectionUtil {
method.getDeclaringClass().getAnnotation(annotationClass); method.getDeclaringClass().getAnnotation(annotationClass);
} }
public static void checkValidCommandMethod(Method method) { public static int getCastDepth(Class<?> child, Class<?> parent) {
if (method.isAnnotationPresent(ApplicationCommand.class) &&
method.getDeclaringClass().isAnnotationPresent(ApplicationCommand.class)) { if (parent.equals(Object.class))
throw new RuntimeException(method.getName() + ": Can't have ApplicationCommand Annotation on Class and Method"); return Integer.MAX_VALUE;
if (!parent.isAssignableFrom(child)) {
throw new IllegalArgumentException("The specified class is not a child class of the specified parent.");
} }
if (!isAnnotationPresent(method, ApplicationCommand.class)) int depth = 0;
throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Class or Method"); Class<?> curr = child;
List<Class<?>> parents = new ArrayList<>();
if (!(method.isAnnotationPresent(SubCommand.class) && while (!curr.equals(parent)) {
isAnnotationPresent(method, ApplicationCommand.class))) { depth++;
throw new RuntimeException(method.getName() + ": Missing ApplicationCommand Annotation on either Method or Class"); parents.add(curr.getSuperclass());
parents.addAll(Arrays.asList(curr.getInterfaces()));
for (Class<?> currParent : parents) {
if (currParent != null && parent.isAssignableFrom(currParent)) {
curr = currParent;
break;
} }
} }
parents.clear();
}
return depth;
}
public static Method getMostSpecificMethod(Method[] methods, Class<?>... parameters) {
List<Method> compatibleMethods = Arrays.stream(methods)
.filter(x -> isMethodCallable(x, parameters))
.toList();
if (compatibleMethods.size() == 0)
throw new IllegalArgumentException("There are no compatible Methods provided");
for (int i = 0; i < parameters.length; i++) {
final int currI = i;
Class<?>[] parameterTypes = compatibleMethods.stream()
.map(x -> x.getParameterTypes()[currI])
.toArray(x -> new Class[x]);
Class<?> mostSpecific = getMostSpecificClass(parameterTypes, parameters[i]);
compatibleMethods = compatibleMethods.stream()
.filter(x -> Objects.equals(x.getParameterTypes()[currI], mostSpecific))
.toList();
}
return compatibleMethods.getFirst();
}
public static Class<?> getMostSpecificClass(Class<?>[] classes, Class<?> base) {
int min = Integer.MAX_VALUE;
Class<?> currMostSpecific = null;
for (Class<?> currClass : classes) {
int currCastDepth = getCastDepth(base, currClass);
if (currCastDepth <= min) {
min = currCastDepth;
currMostSpecific = currClass;
}
}
return currMostSpecific;
}
public static boolean isMethodCallable(Method method, Class<?>... parameters) {
if (!Objects.equals(method.getParameterCount(), parameters.length))
return false;
Class<?>[] methodParams = method.getParameterTypes();
for (int i = 0; i < parameters.length; i++) {
if (!methodParams[i].isAssignableFrom(parameters[i]))
return false;
}
return true;
}
public static String getFullMethodName(Method method) {
return method.getDeclaringClass().getName() + "." + method.getName();
}
} }

View File

@@ -0,0 +1,55 @@
package net.tomatentum.marinara.util;
import org.slf4j.Marker;
import org.slf4j.event.Level;
import org.slf4j.helpers.LegacyAbstractLogger;
import org.slf4j.helpers.MessageFormatter;
public class SimpleLogger extends LegacyAbstractLogger {
private String name;
public SimpleLogger(String name) {
this.name = name;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
protected String getFullyQualifiedCallerName() {
return this.name;
}
@Override
protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments,
Throwable throwable) {
String formatted = MessageFormatter.basicArrayFormat(messagePattern, arguments);
System.out.println("[%s] %s => %s".formatted(level, this.name, formatted));
}
}

View File

@@ -0,0 +1,70 @@
package net.tomatentum.marinara.wrapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import net.tomatentum.marinara.interaction.commands.SlashCommandDefinition;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOptionChoice;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.ident.RootCommandIdentifier;
import net.tomatentum.marinara.interaction.ident.SlashCommandIdentifier;
import net.tomatentum.marinara.util.LoggerUtil;
public class CommandConverter<A extends Object, O extends Object, C extends Object> {
public static <A, O, C> CommandConverter<A, O, C> of(Spec<A, O, C> spec) {
return new CommandConverter<>(spec);
}
private Logger logger = LoggerUtil.getLogger(getClass());
private Spec<A, O, C> spec;
CommandConverter(Spec<A, O, C> spec) {
this.spec = spec;
}
public A convert(SlashCommandDefinition def) {
logger.debug("Converting command {}", def);
List<O> options = new ArrayList<>();
if (!def.isRootCommand()) {
Arrays.stream(def.getSubCommands()).map(this::convertSubCommand).forEach(options::add);
Arrays.stream(def.getSubCommandGroups()).map(x -> this.convertSubCommandGroup(def, x)).forEach(options::add);
}else
Arrays.stream(def.rootIdentifier().options()).map(this::convertOption).forEach(options::add);
return spec.convertCommand(def.rootIdentifier(), options);
}
private O convertSubCommandGroup(SlashCommandDefinition def, InteractionIdentifier identifier) {
logger.debug("Converting subCommandGroup {} of {}", identifier, def);
SlashCommandIdentifier[] subCommands = def.getSubCommands(identifier.name());
List<O> convertedSubCommands = Arrays.stream(subCommands).map(this::convertSubCommand).toList();
return spec.convertSubCommandGroup(identifier, convertedSubCommands);
}
private O convertSubCommand(SlashCommandIdentifier identifier) {
logger.debug("Converting subCommand {}", identifier);
List<O> options = Arrays.stream(identifier.options()).map(this::convertOption).toList();
return spec.convertSubCommand(identifier, options);
}
private O convertOption(SlashCommandOption option) {
logger.debug("Converting option {}", option);
List<C> choices = Arrays.stream(SlashCommandDefinition.getActualChoices(option)).map(spec::convertChoice).toList();
return spec.convertOption(option, choices);
}
public static interface Spec<A extends Object, O extends Object, C extends Object> {
public A convertCommand(RootCommandIdentifier rootIdentifier, List<O> options);
public O convertSubCommandGroup(InteractionIdentifier identifier, List<O> subCommands);
public O convertSubCommand(InteractionIdentifier identifier, List<O> options);
public O convertOption(SlashCommandOption option, List<C> choices);
public C convertChoice(SlashCommandOptionChoice choice);
}
}

View File

@@ -0,0 +1,51 @@
package net.tomatentum.marinara.wrapper;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import net.tomatentum.marinara.interaction.commands.SlashCommandDefinition;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ObjectAggregator;
public class CommandRegisterer<A extends Object> {
public static <A extends Object> CommandRegisterer<A> of(Strategy<A> strategy, CommandConverter<A, ?, ?> converter) {
return new CommandRegisterer<A>(strategy, converter);
}
private Logger logger = LoggerUtil.getLogger(getClass());
private Strategy<A> strategy;
private CommandConverter<A, ?, ?> converter;
CommandRegisterer(Strategy<A> strategy, CommandConverter<A, ?, ?> converter) {
this.strategy = strategy;
this.converter = converter;
}
public void register(SlashCommandDefinition[] slashDefs) {
Set<ServerCommandList<A>> serverCommands = new ObjectAggregator<SlashCommandDefinition, Long, ServerCommandList<A>>(
def -> Arrays.stream(def.serverIds()).boxed().toList(),
(l, o) -> l.add(converter.convert(o)),
ServerCommandList::new)
.aggregate(Arrays.asList(slashDefs)).stream()
.collect(Collectors.toSet());
Set<A> globalCommands = Arrays.stream(slashDefs)
.filter(x -> x.serverIds().length <= 0)
.map(converter::convert)
.collect(Collectors.toSet());
serverCommands.forEach(strategy::registerServer);
strategy.registerGlobal(globalCommands);
logger.info("Registered all SlashCommands");
}
public interface Strategy<A extends Object> {
void registerServer(ServerCommandList<A> commands);
void registerGlobal(Set<A> defs);
}
}

View File

@@ -0,0 +1,11 @@
package net.tomatentum.marinara.wrapper;
public interface ContextObjectProvider {
public Object convertCommandOption(Object context, String optionName);
public Object getComponentContextObject(Object context, Class<?> type);
public Object getInteractionContextObject(Object context, Class<?> type);
public Object getAutocompleteFocusedOption(Object context);
}

View File

@@ -0,0 +1,72 @@
package net.tomatentum.marinara.wrapper;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import io.leangen.geantyref.GenericTypeReflector;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.util.ReflectionUtil;
public class IdentifierProvider {
public static IdentifierProvider of(Converter<?>... converter) {
return new IdentifierProvider(Arrays.asList(converter));
}
private Map<Class<?>, Converter<?>> converter;
private Logger logger = LoggerUtil.getLogger(getClass());
private IdentifierProvider(List<Converter<?>> converter) {
this.converter = new HashMap<>();
for (Converter<?> conv : converter) {
if (conv.getClass().getName().contains("$$Lambda"))
throw new IllegalArgumentException("Lambdas cannot be used for IdentifierConverter because of Type erasure.");
Type type = GenericTypeReflector.getExactSuperType(conv.getClass(), Converter.class);
Type parameterType = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
if (!(parameterType instanceof Class))
throw new IllegalArgumentException("Only full Class types are supported by IdentiferConverters");
this.converter.put((Class<?>) parameterType, conv);
}
}
public InteractionIdentifier provide(Object context) {
Type type = ReflectionUtil.getMostSpecificClass(
converter.keySet().stream().filter(x -> x.isAssignableFrom(context.getClass())).toArray(Class<?>[]::new),
context.getClass());
if (type == null)
logger.debug("No Identifier converter found for context {}", context.getClass().toString());
@SuppressWarnings("unchecked")
Converter<Object> conv = (Converter<Object>) converter.get(type);
return conv.convert(context);
}
@FunctionalInterface
public interface Converter<T extends Object> {
InteractionIdentifier convert(T context);
}
public static class LambdaWrapper<T extends Object> implements Converter<T> {
private Converter<T> converter;
LambdaWrapper(Converter<T> converter) {
this.converter = converter;
}
@Override
public InteractionIdentifier convert(T context) {
return this.converter.convert(context);
}
}
}

View File

@@ -3,22 +3,14 @@ package net.tomatentum.marinara.wrapper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import net.tomatentum.marinara.interaction.commands.ApplicationCommandDefinition;
import net.tomatentum.marinara.interaction.commands.ExecutableCommandDefinition;
import net.tomatentum.marinara.interaction.commands.option.OptionType;
import net.tomatentum.marinara.interaction.InteractionType;
public abstract class LibraryWrapper { public abstract class LibraryWrapper {
private List<Consumer<Object>> interactionSubscriber; private List<Consumer<Object>> interactionSubscriber;
protected LibraryWrapper() { protected LibraryWrapper() {
interactionSubscriber = new ArrayList<>(); this.interactionSubscriber = new ArrayList<>();
} }
public abstract void registerApplicationCommand(ApplicationCommandDefinition def);
public void handleInteraction(Object context) { public void handleInteraction(Object context) {
interactionSubscriber.forEach((o) -> o.accept(context)); interactionSubscriber.forEach((o) -> o.accept(context));
} }
@@ -30,7 +22,8 @@ public abstract class LibraryWrapper {
interactionSubscriber.remove(consumer); interactionSubscriber.remove(consumer);
} }
public abstract InteractionType getInteractionType(Class<?> clazz); public abstract CommandRegisterer<?> getRegisterer();
public abstract Object convertCommandOption(Object context, OptionType type, String optionName); public abstract IdentifierProvider createIdentifierProvider();
public abstract ExecutableCommandDefinition getCommandDefinition(Object context); public abstract ContextObjectProvider getContextObjectProvider();
} }

View File

@@ -0,0 +1,16 @@
package net.tomatentum.marinara.wrapper;
import java.util.HashSet;
public class ServerCommandList<A> extends HashSet<A>{
private long serverId;
public ServerCommandList(long serverId) {
this.serverId = serverId;
}
public long serverId() {
return serverId;
}
}

View File

@@ -11,4 +11,11 @@ plugins {
} }
rootProject.name = "Marinara" rootProject.name = "Marinara"
include("lib") include(":lib")
include(":wrapper-javacord")
include(":wrapper-discord4j")
project(":wrapper-javacord").projectDir = file("wrapper/javacord")
project(":wrapper-discord4j").projectDir = file("wrapper/discord4j")

View File

@@ -0,0 +1,43 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java library project to get you started.
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.8/userguide/building_java_projects.html in the Gradle documentation.
*/
plugins {
// Apply the java-library plugin for API and implementation separation.
`java-library`
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
// Use JUnit Jupiter for testing.
testImplementation(libs.junit.jupiter)
testImplementation(libs.mockito)
testImplementation(libs.discord4j)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation(libs.slf4j)
implementation(libs.discord4j) {
exclude(module="discord4j-voice")
}
implementation(libs.geantyref)
implementation(project(":lib"))
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}
tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

View File

@@ -0,0 +1,102 @@
package net.tomatentum.marinara.wrapper.discord4j;
import java.util.List;
import discord4j.core.event.domain.interaction.ChatInputAutoCompleteEvent;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.event.domain.interaction.ComponentInteractionEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import net.tomatentum.marinara.interaction.commands.option.SlashCommandOptionType;
import net.tomatentum.marinara.wrapper.ContextObjectProvider;
public class Discord4JContextObjectProvider implements ContextObjectProvider {
@Override
public Object convertCommandOption(Object context, String optionName) {
if (!(context instanceof ChatInputInteractionEvent))
return null;
ChatInputInteractionEvent interactionEvent = (ChatInputInteractionEvent) context;
List<ApplicationCommandInteractionOption> subOptions = Discord4JWrapper.SUB_FILTER.apply(interactionEvent.getOptions());
if (subOptions.isEmpty())
return getOptionValue(interactionEvent.getOption(optionName).get());
ApplicationCommandInteractionOption subCommandOption = interactionEvent.getOptions().getFirst();
subOptions = Discord4JWrapper.SUB_FILTER.apply(subCommandOption.getOptions());
if (!subOptions.isEmpty())
subCommandOption = subOptions.getFirst();
return getOptionValue(interactionEvent.getOption(optionName).get());
}
private Object getOptionValue(ApplicationCommandInteractionOption option) {
if (!option.getValue().isPresent())
return null;
SlashCommandOptionType type = getOptionType(option);
switch (type) {
case ATTACHMENT:
return option.getValue().get().asAttachment();
case BOOLEAN:
return option.getValue().get().asBoolean();
case CHANNEL:
return option.getValue().get().asChannel();
case DOUBLE:
return option.getValue().get().asDouble();
case INTEGER:
return option.getValue().get().asLong();
case MENTIONABLE:
return option.getValue().get().asSnowflake();
case ROLE:
return option.getValue().get().asRole();
case STRING:
return option.getValue().get().asString();
case USER:
return option.getValue().get().asUser();
default:
return null;
}
}
private SlashCommandOptionType getOptionType(ApplicationCommandInteractionOption option) {
return SlashCommandOptionType.fromValue(option.getType().getValue());
}
@Override
public Object getComponentContextObject(Object context, Class<?> type) {
ComponentInteractionEvent componentInteractionEvent = (ComponentInteractionEvent) context;
switch (type.getName()) {
case "discord4j.core.object.entity.Message":
return componentInteractionEvent.getMessage().orElse(null);
default:
return getInteractionContextObject(context, type);
}
}
@Override
public Object getInteractionContextObject(Object context, Class<?> type) {
ComponentInteractionEvent componentInteractionEvent = (ComponentInteractionEvent) context;
switch (type.getName()) {
case "discord4j.core.object.entity.channel.MessageChannel":
return componentInteractionEvent.getInteraction().getChannel().block();
case "discord4j.core.object.entity.Guild":
return componentInteractionEvent.getInteraction().getGuild().block();
case "discord4j.core.object.entity.Member":
return componentInteractionEvent.getInteraction().getMember().orElse(null);
case "discord4j.core.object.entity.User":
return componentInteractionEvent.getInteraction().getUser();
}
return null;
}
@Override
public Object getAutocompleteFocusedOption(Object context) {
ChatInputAutoCompleteEvent interaction = (ChatInputAutoCompleteEvent) context;
return getOptionValue(interaction.getFocusedOption());
}
}

View File

@@ -0,0 +1,79 @@
package net.tomatentum.marinara.wrapper.discord4j;
import java.util.List;
import discord4j.core.object.command.ApplicationCommandOption.Type;
import discord4j.discordjson.json.ApplicationCommandOptionChoiceData;
import discord4j.discordjson.json.ApplicationCommandOptionData;
import discord4j.discordjson.json.ApplicationCommandRequest;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOptionChoice;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.ident.RootCommandIdentifier;
import net.tomatentum.marinara.wrapper.CommandConverter;
public class Discord4JConverterSpec implements CommandConverter.Spec<ApplicationCommandRequest, ApplicationCommandOptionData, ApplicationCommandOptionChoiceData> {
@Override
public ApplicationCommandRequest convertCommand(RootCommandIdentifier rootIdentifier,
List<ApplicationCommandOptionData> options) {
return ApplicationCommandRequest.builder()
.name(rootIdentifier.name())
.description(rootIdentifier.description())
.options(options)
.build();
}
@Override
public ApplicationCommandOptionData convertSubCommandGroup(InteractionIdentifier identifier,
List<ApplicationCommandOptionData> subCommands) {
return ApplicationCommandOptionData.builder()
.type(Type.SUB_COMMAND_GROUP.getValue())
.name(identifier.name())
.description(identifier.description())
.options(subCommands)
.build();
}
@Override
public ApplicationCommandOptionData convertSubCommand(InteractionIdentifier identifier,
List<ApplicationCommandOptionData> options) {
return ApplicationCommandOptionData.builder()
.type(Type.SUB_COMMAND_GROUP.getValue())
.name(identifier.name())
.description(identifier.description())
.options(options)
.build();
}
@Override
public ApplicationCommandOptionData convertOption(SlashCommandOption option,
List<ApplicationCommandOptionChoiceData> choices) {
Type type = Type.of(option.type().getValue());
return ApplicationCommandOptionData.builder()
.type(type.getValue())
.name(option.name())
.description(option.description())
.required(option.required())
.autocomplete(option.autocomplete())
.minLength(Double.valueOf(option.range().min()).intValue())
.minValue(option.range().min())
.maxLength(Double.valueOf(option.range().max()).intValue())
.maxValue(option.range().max())
.choices(choices)
.build();
}
@Override
public ApplicationCommandOptionChoiceData convertChoice(SlashCommandOptionChoice choice) {
var builder = ApplicationCommandOptionChoiceData.builder().name(choice.name());
if (choice.longValue() != Long.MAX_VALUE)
builder.value(choice.longValue());
if (choice.doubleValue() != Double.MAX_VALUE)
builder.value(choice.doubleValue());
if (!choice.stringValue().isEmpty())
builder.value(choice.stringValue());
return builder.build();
}
}

View File

@@ -0,0 +1,32 @@
package net.tomatentum.marinara.wrapper.discord4j;
import java.util.ArrayList;
import java.util.Set;
import discord4j.core.GatewayDiscordClient;
import discord4j.discordjson.json.ApplicationCommandRequest;
import discord4j.rest.service.ApplicationService;
import net.tomatentum.marinara.wrapper.CommandRegisterer;
import net.tomatentum.marinara.wrapper.ServerCommandList;
public class Discord4JRegistererStrategy implements CommandRegisterer.Strategy<ApplicationCommandRequest> {
private ApplicationService appService;
private long applicationId;
public Discord4JRegistererStrategy(GatewayDiscordClient api) {
this.appService = api.getRestClient().getApplicationService();
this.applicationId = api.getRestClient().getApplicationId().block();
}
@Override
public void registerServer(ServerCommandList<ApplicationCommandRequest> commands) {
appService.bulkOverwriteGuildApplicationCommand(applicationId, commands.serverId(), new ArrayList<>(commands));
}
@Override
public void registerGlobal(Set<ApplicationCommandRequest> defs) {
appService.bulkOverwriteGlobalApplicationCommand(applicationId, new ArrayList<>(defs));
}
}

View File

@@ -0,0 +1,74 @@
package net.tomatentum.marinara.wrapper.discord4j;
import java.util.List;
import java.util.function.Function;
import org.slf4j.Logger;
import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.interaction.InteractionCreateEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import discord4j.core.object.command.ApplicationCommandOption.Type;
import discord4j.discordjson.json.ApplicationCommandRequest;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.wrapper.CommandConverter;
import net.tomatentum.marinara.wrapper.CommandRegisterer;
import net.tomatentum.marinara.wrapper.ContextObjectProvider;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.discord4j.identifierconverter.AutocompleteIdentifierConverter;
import net.tomatentum.marinara.wrapper.discord4j.identifierconverter.ButtonIdentifierConverter;
import net.tomatentum.marinara.wrapper.discord4j.identifierconverter.SlashCommandIdentifierConverter;
public class Discord4JWrapper extends LibraryWrapper {
public static final Function<List<ApplicationCommandInteractionOption>, List<ApplicationCommandInteractionOption>> SUB_FILTER = (i) ->
i.stream()
.filter(o -> o.getType().equals(Type.SUB_COMMAND) || o.getType().equals(Type.SUB_COMMAND_GROUP))
.toList();
public static final Function<List<ApplicationCommandInteractionOption>, List<ApplicationCommandInteractionOption>> ARG_FILTER = (i) ->
i.stream()
.filter(o -> !o.getType().equals(Type.SUB_COMMAND) && !o.getType().equals(Type.SUB_COMMAND_GROUP))
.toList();
private Discord4JContextObjectProvider contextObjectProvider;
private CommandRegisterer<ApplicationCommandRequest> commandRegisterer;
private Logger logger = LoggerUtil.getLogger(getClass());
public Discord4JWrapper(GatewayDiscordClient api) {
this.contextObjectProvider = new Discord4JContextObjectProvider();
var converter = CommandConverter.of(new Discord4JConverterSpec());
if (api != null) {
this.commandRegisterer = CommandRegisterer.of(new Discord4JRegistererStrategy(api), converter);
api.on(InteractionCreateEvent.class)
.subscribe(event -> handleInteraction(event));
}else
logger.warn("GatewayDiscordClient was null so no Events were subscribed to.");
logger.info("Discord4J wrapper loaded!");
}
@Override
public CommandRegisterer<?> getRegisterer() {
return this.commandRegisterer;
}
@Override
public IdentifierProvider createIdentifierProvider() {
return IdentifierProvider.of(
new SlashCommandIdentifierConverter(),
new AutocompleteIdentifierConverter(),
new ButtonIdentifierConverter()
);
}
@Override
public ContextObjectProvider getContextObjectProvider() {
return this.contextObjectProvider;
}
}

View File

@@ -0,0 +1,42 @@
package net.tomatentum.marinara.wrapper.discord4j.checks;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Optional;
import discord4j.core.event.domain.interaction.InteractionCreateEvent;
import discord4j.core.object.entity.Member;
import discord4j.rest.util.Permission;
import discord4j.rest.util.PermissionSet;
import net.tomatentum.marinara.checks.InteractionCheck;
public class PermissionCheck implements InteractionCheck<PermissionCheck.HasPermission> {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public static @interface HasPermission {
public Permission[] value();
}
@Override
public boolean preExec(Object context, HasPermission annotation) {
throw new UnsupportedOperationException("Unimplemented method 'preExec'");
}
public boolean preExec(InteractionCreateEvent context, HasPermission annotation) {
Optional<Member> member = context.getInteraction().getMember();
if (member.isEmpty())
return false;
PermissionSet ownPerms = PermissionSet.of(annotation.value());
PermissionSet permSet = member.get().getBasePermissions().block();
return permSet.containsAll(ownPerms);
}
@Override
public void postExec(Object context, HasPermission annotation) {
}
}

View File

@@ -0,0 +1,38 @@
package net.tomatentum.marinara.wrapper.discord4j.identifierconverter;
import java.util.List;
import discord4j.core.event.domain.interaction.ChatInputAutoCompleteEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
import net.tomatentum.marinara.wrapper.discord4j.Discord4JWrapper;
public class AutocompleteIdentifierConverter implements IdentifierProvider.Converter<ChatInputAutoCompleteEvent> {
@Override
public InteractionIdentifier convert(ChatInputAutoCompleteEvent context) {
List<ApplicationCommandInteractionOption> options = Discord4JWrapper.SUB_FILTER.apply(context.getOptions());
String commandName = context.getCommandName();
if (!options.isEmpty()) {
List<ApplicationCommandInteractionOption> sub_options = Discord4JWrapper.SUB_FILTER.apply(options.getFirst().getOptions());
if (!sub_options.isEmpty())
return InteractionIdentifier.createHierarchy(
InteractionType.AUTOCOMPLETE,
commandName,
options.getFirst().getName(),
sub_options.getFirst().getName());
else
return InteractionIdentifier.createHierarchy(
InteractionType.AUTOCOMPLETE,
commandName,
options.getFirst().getName());
}else
return InteractionIdentifier.createHierarchy(
InteractionType.AUTOCOMPLETE,
commandName);
}
}

View File

@@ -0,0 +1,15 @@
package net.tomatentum.marinara.wrapper.discord4j.identifierconverter;
import discord4j.core.event.domain.interaction.ButtonInteractionEvent;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
public class ButtonIdentifierConverter implements IdentifierProvider.Converter<ButtonInteractionEvent> {
@Override
public InteractionIdentifier convert(ButtonInteractionEvent context) {
return InteractionIdentifier.builder().name(context.getCustomId()).type(InteractionType.BUTTON).build();
}
}

View File

@@ -0,0 +1,39 @@
package net.tomatentum.marinara.wrapper.discord4j.identifierconverter;
import java.util.List;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
import net.tomatentum.marinara.wrapper.discord4j.Discord4JWrapper;
public class SlashCommandIdentifierConverter implements IdentifierProvider.Converter<ChatInputInteractionEvent> {
@Override
public InteractionIdentifier convert(ChatInputInteractionEvent context) {
List<ApplicationCommandInteractionOption> options = Discord4JWrapper.SUB_FILTER.apply(context.getOptions());
String commandName = context.getCommandName();
if (!options.isEmpty()) {
List<ApplicationCommandInteractionOption> sub_options = Discord4JWrapper.SUB_FILTER.apply(options.getFirst().getOptions());
if (!sub_options.isEmpty())
return InteractionIdentifier.createHierarchy(
InteractionType.COMMAND,
commandName,
options.getFirst().getName(),
sub_options.getFirst().getName());
else
return InteractionIdentifier.createHierarchy(
InteractionType.COMMAND,
commandName,
options.getFirst().getName());
}else
return InteractionIdentifier.createHierarchy(
InteractionType.COMMAND,
commandName);
}
}

View File

@@ -0,0 +1,45 @@
package net.tomatentum.marinara.test.discord4j;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import discord4j.core.event.domain.interaction.ChatInputAutoCompleteEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import discord4j.core.object.command.ApplicationCommandInteractionOptionValue;
import discord4j.core.object.command.ApplicationCommandOption.Type;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.discord4j.Discord4JWrapper;
public class AutoCompleteTest {
@Test
public void testAutocomplete() {
ApplicationCommandInteractionOption optionMock = mock();
ChatInputAutoCompleteEvent autoCompleteEventMock = mock();
when(optionMock.getName()).thenReturn("foo");
when(optionMock.getType()).thenReturn(Type.STRING);
when(optionMock.getValue()).thenReturn(
Optional.of(
new ApplicationCommandInteractionOptionValue(null, null, Type.STRING.getValue(), "test", null)
));
when(autoCompleteEventMock.getCommandName()).thenReturn("test");
when(autoCompleteEventMock.getOptions()).thenReturn(new ArrayList<>());
when(autoCompleteEventMock.getFocusedOption()).thenReturn(optionMock);
LibraryWrapper wrapper = new Discord4JWrapper(null); //null okay as we don't use the discord API in this test.
Marinara marinara = Marinara.load(wrapper);
marinara.getRegistry().addInteractions(new TestAutocomplete());
wrapper.handleInteraction(autoCompleteEventMock);
verify(autoCompleteEventMock).respondWithSuggestions(any());
}
}

View File

@@ -0,0 +1,29 @@
package net.tomatentum.marinara.test.discord4j;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import discord4j.core.event.domain.interaction.ButtonInteractionEvent;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.test.discord4j.mocks.CommonMocks;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.discord4j.Discord4JWrapper;
@TestInstance(Lifecycle.PER_CLASS)
public class ButtonTest {
@Test
public void testButtonExecution() {
ButtonInteractionEvent buttonEventMock = CommonMocks.getButtonEventMock("test");
LibraryWrapper wrapper = new Discord4JWrapper(null); //null okay as we don't use the discord API in this test.
Marinara marinara = Marinara.load(wrapper);
marinara.getRegistry().addInteractions(new TestButton());
wrapper.handleInteraction(buttonEventMock);
assertTrue(TestButton.didRun);
}
}

View File

@@ -0,0 +1,70 @@
package net.tomatentum.marinara.test.discord4j;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import discord4j.core.event.domain.interaction.ButtonInteractionEvent;
import discord4j.core.object.command.Interaction;
import discord4j.core.object.entity.Member;
import discord4j.rest.util.Permission;
import discord4j.rest.util.PermissionSet;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.test.discord4j.mocks.CommonMocks;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.discord4j.Discord4JWrapper;
import net.tomatentum.marinara.wrapper.discord4j.checks.PermissionCheck;
import reactor.core.publisher.Mono;
@TestInstance(Lifecycle.PER_CLASS)
public class InteractionCheckTest {
@Test
public void testInteractionCheck() {
ButtonInteractionEvent buttonEventMock = CommonMocks.getButtonEventMock("test");
LibraryWrapper wrapper = new Discord4JWrapper(null);
Marinara marinara = Marinara.load(wrapper);
marinara.getCheckRegistry().addCheck(new TestInteractionCheck());
marinara.getRegistry().addInteractions(new TestButton());
wrapper.handleInteraction(buttonEventMock);
assertTrue(TestInteractionCheck.preExecuted);
assertTrue(TestInteractionCheck.postExecuted);
assertTrue(TestButton.didRun);
}
@Test
public void testPermissionCheck() {
Member memberMock = mock();
Interaction interactionMock = mock();
when(memberMock.getBasePermissions()).thenReturn(Mono.just(PermissionSet.none()));
when(interactionMock.getMember()).thenReturn(Optional.of(memberMock));
ButtonInteractionEvent buttonEventMock = CommonMocks.getButtonEventMock("permissionCheck", interactionMock);
LibraryWrapper wrapper = new Discord4JWrapper(null);
Marinara marinara = Marinara.load(wrapper);
marinara.getCheckRegistry().addCheck(new PermissionCheck());
marinara.getRegistry().addInteractions(new TestButton());
wrapper.handleInteraction(buttonEventMock);
assertFalse(TestButton.didPermRun);
TestButton.didPermRun = false;
when(memberMock.getBasePermissions()).thenReturn(Mono.just(PermissionSet.of(Permission.ATTACH_FILES)));
wrapper.handleInteraction(buttonEventMock);
assertTrue(TestButton.didPermRun);
}
}

View File

@@ -0,0 +1,74 @@
package net.tomatentum.marinara.test.discord4j;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import discord4j.core.DiscordClient;
import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import discord4j.core.object.command.ApplicationCommandInteractionOptionValue;
import discord4j.core.object.command.ApplicationCommandOption.Type;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.discord4j.Discord4JWrapper;
@TestInstance(Lifecycle.PER_CLASS)
public class SlashCommandTest {
String DISCORD_TOKEN = System.getenv("DISCORD_TEST_TOKEN");
GatewayDiscordClient client;
@BeforeAll
void setUp() {
client = DiscordClient.create(DISCORD_TOKEN).login().block();
}
@AfterAll
void tearDown() {
client.logout().block();
client = null;
}
@Test
void testSlashCommand() {
Marinara marinara = Marinara.load(new Discord4JWrapper(client));
marinara.getRegistry().addInteractions(new TestCommand());
marinara.getRegistry().registerCommands();
System.out.println("Success!");
}
@Test
void testSlashCommandExecution() {
ApplicationCommandInteractionOption optionMock = mock();
ChatInputInteractionEvent eventMock = mock();
when(optionMock.getName()).thenReturn("foo");
when(optionMock.getType()).thenReturn(Type.STRING);
when(optionMock.getValue()).thenReturn(
Optional.of(
new ApplicationCommandInteractionOptionValue(null, null, Type.STRING.getValue(), "test", null)
));
when(eventMock.getCommandName()).thenReturn("test");
when(eventMock.getOptions()).thenReturn(Arrays.asList(optionMock));
when(eventMock.getOption("foo")).thenReturn(Optional.of(optionMock));
LibraryWrapper wrapper = new Discord4JWrapper(client);
Marinara marinara = Marinara.load(wrapper);
marinara.getRegistry().addInteractions(new TestCommand());
wrapper.handleInteraction(eventMock);
}
}

View File

@@ -0,0 +1,22 @@
package net.tomatentum.marinara.test.discord4j;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Collections;
import discord4j.core.event.domain.interaction.ChatInputAutoCompleteEvent;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.annotation.AutoComplete;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand;
public class TestAutocomplete implements InteractionHandler {
@SlashCommand(name = "test")
@AutoComplete
public void autocomplete(ChatInputAutoCompleteEvent context, String value) {
System.out.println("Success!");
assertEquals(value, "test");
context.respondWithSuggestions(Collections.emptyList());
}
}

View File

@@ -0,0 +1,43 @@
package net.tomatentum.marinara.test.discord4j;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import discord4j.core.event.domain.interaction.ButtonInteractionEvent;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.User;
import discord4j.core.object.entity.channel.MessageChannel;
import discord4j.rest.util.Permission;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.annotation.Button;
import net.tomatentum.marinara.test.discord4j.TestInteractionCheck.TestCheck;
import net.tomatentum.marinara.wrapper.discord4j.checks.PermissionCheck.HasPermission;
public class TestButton implements InteractionHandler {
public static boolean didRun = false;
@Button("test")
@TestCheck
public void exec(ButtonInteractionEvent interaction, MessageChannel channel, Message message, Member member, User user, Guild server) {
assertNotNull(interaction);
assertNotNull(channel);
assertNotNull(message);
assertNotNull(member);
assertNotNull(user);
assertNotNull(server);
didRun = true;
System.out.println("Success!");
}
public static boolean didPermRun = false;
@Button("permissionCheck")
@HasPermission({Permission.ATTACH_FILES})
public void exec(ButtonInteractionEvent interaction) {
didPermRun = true;
System.out.println("It worked!");
}
}

View File

@@ -0,0 +1,20 @@
package net.tomatentum.marinara.test.discord4j;
import net.tomatentum.marinara.interaction.commands.choice.ChoiceValueProvider;
public enum TestChoiceEnum implements ChoiceValueProvider<String> {
TestValue("testValue"),
FooBar("fooBar"),
Spongebob("spongebob");
private String value;
private TestChoiceEnum(String value) {
this.value = value;
}
@Override
public String getChoiceValue() {
return value;
}
}

View File

@@ -0,0 +1,32 @@
package net.tomatentum.marinara.test.discord4j;
import static org.junit.jupiter.api.Assertions.assertEquals;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.commands.annotation.CommandChoices;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
import net.tomatentum.marinara.interaction.commands.option.SlashCommandOptionType;
public class TestCommand implements InteractionHandler {
@SlashCommand(
name = "test",
description = "testingen",
serverIds = {
1037753048602255440L
},
options = {
@SlashCommandOption(
name = "foo",
description = "foo bar is very fooby",
type = SlashCommandOptionType.STRING,
choices = @CommandChoices(cenum = TestChoiceEnum.class)
)
}
)
public void exec(ChatInputInteractionEvent event, String test) {
assertEquals(test, "test");
System.out.println("Success!");
}
}

View File

@@ -0,0 +1,37 @@
package net.tomatentum.marinara.test.discord4j;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import net.tomatentum.marinara.checks.InteractionCheck;
public class TestInteractionCheck implements InteractionCheck<TestInteractionCheck.TestCheck> {
public static boolean preExecuted = false;
public static boolean postExecuted = false;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public static @interface TestCheck {
}
@Override
public boolean preExec(Object context, TestCheck annotation) {
assertNotNull(annotation);
assertNotNull(context);
preExecuted = true;
return true;
}
@Override
public void postExec(Object context, TestCheck annotation) {
assertNotNull(annotation);
assertNotNull(context);
postExecuted = true;
}
}

View File

@@ -0,0 +1,55 @@
package net.tomatentum.marinara.test.discord4j.mocks;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Optional;
import discord4j.core.event.domain.interaction.ButtonInteractionEvent;
import discord4j.core.object.command.Interaction;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.User;
import discord4j.core.object.entity.channel.MessageChannel;
import reactor.core.publisher.Mono;
public class CommonMocks {
public static Interaction getInteractionMock() {
Interaction interaction = mock(Interaction.class);
Message message = mock(Message.class);
MessageChannel channel = mock(MessageChannel.class);
Guild guild = mock(Guild.class);
User user = mock(User.class);
Member member = mock(Member.class);
when(interaction.getMessage()).thenReturn(Optional.of(message));
when(interaction.getChannel()).thenReturn(Mono.just(channel));
when(interaction.getGuild()).thenReturn(Mono.just(guild));
when(interaction.getUser()).thenReturn(user);
when(interaction.getMember()).thenReturn(Optional.of(member));
return interaction;
}
public static ButtonInteractionEvent getButtonEventMock(String customId) {
ButtonInteractionEvent buttonEventMock = mock(ButtonInteractionEvent.class);
when(buttonEventMock.getCustomId()).thenReturn(customId);
Interaction interactionMock = getInteractionMock();
when(buttonEventMock.getInteraction()).thenReturn(interactionMock);
Optional<Message> message = interactionMock.getMessage();
when (buttonEventMock.getMessage()).thenReturn(message);
return buttonEventMock;
}
public static ButtonInteractionEvent getButtonEventMock(String customId, Interaction interaction) {
ButtonInteractionEvent buttonEventMock = mock(ButtonInteractionEvent.class);
when(buttonEventMock.getCustomId()).thenReturn(customId);
when(buttonEventMock.getInteraction()).thenReturn(interaction);
return buttonEventMock;
}
}

View File

@@ -0,0 +1,40 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java library project to get you started.
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.8/userguide/building_java_projects.html in the Gradle documentation.
*/
plugins {
// Apply the java-library plugin for API and implementation separation.
`java-library`
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
// Use JUnit Jupiter for testing.
testImplementation(libs.junit.jupiter)
testImplementation(libs.mockito)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation(libs.slf4j)
implementation(libs.javacord)
implementation(libs.geantyref)
implementation(project(":lib"))
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}
tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

View File

@@ -0,0 +1,110 @@
package net.tomatentum.marinara.wrapper.javacord;
import org.javacord.api.interaction.AutocompleteInteraction;
import org.javacord.api.interaction.ButtonInteraction;
import org.javacord.api.interaction.SlashCommandInteraction;
import org.javacord.api.interaction.SlashCommandInteractionOption;
import org.javacord.api.interaction.SlashCommandOptionType;
import net.tomatentum.marinara.wrapper.ContextObjectProvider;
public class JavacordContextObjectProvider implements ContextObjectProvider {
@Override
public Object convertCommandOption(Object context, String optionName) {
if (!(context instanceof SlashCommandInteraction))
return null;
SlashCommandInteraction interaction = (SlashCommandInteraction) context;
if (!interaction.getArguments().isEmpty())
return getOptionValue(interaction.getOptionByName(optionName).get());
SlashCommandInteractionOption subCommandOption = interaction.getOptions().getFirst();
if (!subCommandOption.getOptions().isEmpty())
subCommandOption = subCommandOption.getOptions().getFirst();
return getOptionValue(subCommandOption.getOptionByName(optionName).get());
}
private Object getOptionValue(SlashCommandInteractionOption option) {
SlashCommandOptionType type = getOptionType(option);
switch (type) {
case ATTACHMENT:
return option.getAttachmentValue().get();
case BOOLEAN:
return option.getBooleanValue().get();
case CHANNEL:
return option.getChannelValue().get();
case DECIMAL:
return option.getDecimalValue().get();
case LONG:
return option.getLongValue().get();
case MENTIONABLE:
return option.requestMentionableValue().get();
case ROLE:
return option.getRoleValue().get();
case STRING:
return option.getStringValue().get();
case USER:
return option.requestUserValue().get();
default:
return null;
}
}
private SlashCommandOptionType getOptionType(SlashCommandInteractionOption option) {
if (option.getAttachmentValue().isPresent())
return SlashCommandOptionType.ATTACHMENT;
if (option.getBooleanValue().isPresent())
return SlashCommandOptionType.BOOLEAN;
if (option.getChannelValue().isPresent())
return SlashCommandOptionType.CHANNEL;
if (option.getDecimalValue().isPresent())
return SlashCommandOptionType.DECIMAL;
if (option.getLongValue().isPresent())
return SlashCommandOptionType.LONG;
if (option.requestMentionableValue().isPresent())
return SlashCommandOptionType.MENTIONABLE;
if (option.getRoleValue().isPresent())
return SlashCommandOptionType.ROLE;
if (option.getStringValue().isPresent())
return SlashCommandOptionType.STRING;
if (option.requestUserValue().isPresent())
return SlashCommandOptionType.USER;
return SlashCommandOptionType.UNKNOWN;
}
@Override
public Object getComponentContextObject(Object context, Class<?> type) {
ButtonInteraction button = (ButtonInteraction) context;
switch (type.getName()) {
case "org.javacord.api.entity.message.Message":
return button.getMessage();
default:
return getInteractionContextObject(context, type);
}
}
@Override
public Object getInteractionContextObject(Object context, Class<?> type) {
ButtonInteraction button = (ButtonInteraction) context;
switch (type.getName()) {
case "org.javacord.api.entity.channel.TextChannel":
return button.getChannel().orElse(null);
case "org.javacord.api.entity.server.Server":
return button.getServer().orElse(null);
case "org.javacord.api.entity.user.User":
return button.getUser();
}
return null;
}
@Override
public Object getAutocompleteFocusedOption(Object context) {
AutocompleteInteraction interaction = (AutocompleteInteraction) context;
return getOptionValue(interaction.getFocusedOption());
}
}

View File

@@ -0,0 +1,77 @@
package net.tomatentum.marinara.wrapper.javacord;
import java.util.List;
import org.javacord.api.interaction.SlashCommand;
import org.javacord.api.interaction.SlashCommandBuilder;
import org.javacord.api.interaction.SlashCommandOption;
import org.javacord.api.interaction.SlashCommandOptionBuilder;
import org.javacord.api.interaction.SlashCommandOptionChoice;
import org.javacord.api.interaction.SlashCommandOptionChoiceBuilder;
import org.javacord.api.interaction.SlashCommandOptionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.interaction.ident.RootCommandIdentifier;
import net.tomatentum.marinara.wrapper.CommandConverter;
public class JavacordConverterSpec implements CommandConverter.Spec<SlashCommandBuilder, SlashCommandOption, SlashCommandOptionChoice> {
@Override
public SlashCommandBuilder convertCommand(RootCommandIdentifier rootIdentifier, List<SlashCommandOption> options) {
return SlashCommand.with(rootIdentifier.name(), rootIdentifier.description(), options);
}
@Override
public SlashCommandOption convertSubCommandGroup(InteractionIdentifier identifier,
List<SlashCommandOption> subCommands) {
return SlashCommandOption.createWithOptions(
SlashCommandOptionType.SUB_COMMAND_GROUP,
identifier.name(),
identifier.description(),
subCommands);
}
@Override
public SlashCommandOption convertSubCommand(InteractionIdentifier identifier, List<SlashCommandOption> options) {
return SlashCommandOption.createWithOptions(
SlashCommandOptionType.SUB_COMMAND,
identifier.name(),
identifier.description(),
options);
}
@Override
public SlashCommandOption convertOption(
net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption option,
List<SlashCommandOptionChoice> choices) {
SlashCommandOptionType type = SlashCommandOptionType.fromValue(option.type().getValue());
return new SlashCommandOptionBuilder()
.setType(type)
.setName(option.name())
.setDescription(option.description())
.setRequired(option.required())
.setAutocompletable(option.autocomplete())
.setMinLength(Double.valueOf(option.range().min()).longValue())
.setDecimalMinValue(option.range().min())
.setMaxLength(Double.valueOf(option.range().max()).longValue())
.setDecimalMaxValue(option.range().max())
.setChoices(choices)
.build();
}
@Override
public SlashCommandOptionChoice convertChoice(
net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOptionChoice choice) {
SlashCommandOptionChoiceBuilder builder = new SlashCommandOptionChoiceBuilder().setName(choice.name());
if (choice.longValue() != Long.MAX_VALUE)
builder.setValue(choice.longValue());
/*
not yet available
if (choice.doubleValue() != Double.MAX_VALUE)
builder.setValue(choice.doubleValue());
*/
if (!choice.stringValue().isEmpty())
builder.setValue(choice.stringValue());
return builder.build();
}
}

View File

@@ -0,0 +1,29 @@
package net.tomatentum.marinara.wrapper.javacord;
import java.util.Set;
import org.javacord.api.DiscordApi;
import org.javacord.api.interaction.SlashCommandBuilder;
import net.tomatentum.marinara.wrapper.CommandRegisterer;
import net.tomatentum.marinara.wrapper.ServerCommandList;
public class JavacordRegistererStrategy implements CommandRegisterer.Strategy<SlashCommandBuilder> {
private DiscordApi api;
public JavacordRegistererStrategy(DiscordApi api) {
this.api = api;
}
@Override
public void registerServer(ServerCommandList<SlashCommandBuilder> commands) {
api.bulkOverwriteServerApplicationCommands(commands.serverId(), commands);
}
@Override
public void registerGlobal(Set<SlashCommandBuilder> defs) {
api.bulkOverwriteGlobalApplicationCommands(defs);
}
}

View File

@@ -0,0 +1,55 @@
package net.tomatentum.marinara.wrapper.javacord;
import org.javacord.api.DiscordApi;
import org.javacord.api.interaction.SlashCommandBuilder;
import org.slf4j.Logger;
import net.tomatentum.marinara.wrapper.CommandConverter;
import net.tomatentum.marinara.wrapper.CommandRegisterer;
import net.tomatentum.marinara.wrapper.ContextObjectProvider;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
import net.tomatentum.marinara.util.LoggerUtil;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.javacord.identifierconverter.AutocompleteIdentifierConverter;
import net.tomatentum.marinara.wrapper.javacord.identifierconverter.ButtonIdentifierConverter;
import net.tomatentum.marinara.wrapper.javacord.identifierconverter.SlashCommandIdentifierConverter;
public class JavacordWrapper extends LibraryWrapper {
private JavacordContextObjectProvider contextObjectProvider;
private CommandRegisterer<SlashCommandBuilder> commandRegisterer;
private Logger logger = LoggerUtil.getLogger(getClass());
public JavacordWrapper(DiscordApi api) {
this.contextObjectProvider = new JavacordContextObjectProvider();
var converter = CommandConverter.of(new JavacordConverterSpec());
if (api != null) {
this.commandRegisterer = CommandRegisterer.of(new JavacordRegistererStrategy(api), converter);
api.addInteractionCreateListener((e) -> handleInteraction(e.getInteraction()));
}else
logger.warn("DiscordApi was null so no Events were subscribed to.");
logger.info("Javacord wrapper loaded!");
}
@Override
public CommandRegisterer<?> getRegisterer() {
return this.commandRegisterer;
}
@Override
public IdentifierProvider createIdentifierProvider() {
return IdentifierProvider.of(
new SlashCommandIdentifierConverter(),
new AutocompleteIdentifierConverter(),
new ButtonIdentifierConverter()
);
}
@Override
public ContextObjectProvider getContextObjectProvider() {
return contextObjectProvider;
}
}

View File

@@ -0,0 +1,41 @@
package net.tomatentum.marinara.wrapper.javacord.checks;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Optional;
import org.javacord.api.entity.permission.PermissionType;
import org.javacord.api.entity.server.Server;
import org.javacord.api.interaction.InteractionBase;
import net.tomatentum.marinara.checks.InteractionCheck;
public class PermissionCheck implements InteractionCheck<PermissionCheck.HasPermission> {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public static @interface HasPermission {
public PermissionType[] value();
}
@Override
public boolean preExec(Object context, HasPermission annotation) {
throw new UnsupportedOperationException("Unimplemented method 'preExec'");
}
public boolean preExec(InteractionBase context, HasPermission annotation) {
Optional<Server> server = context.getServer();
if (!server.isPresent())
return false;
return server.get().hasPermissions(context.getUser(), annotation.value());
}
@Override
public void postExec(Object context, HasPermission annotation) {
}
}

View File

@@ -0,0 +1,37 @@
package net.tomatentum.marinara.wrapper.javacord.identifierconverter;
import java.util.List;
import org.javacord.api.interaction.AutocompleteInteraction;
import org.javacord.api.interaction.SlashCommandInteractionOption;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
public class AutocompleteIdentifierConverter implements IdentifierProvider.Converter<AutocompleteInteraction> {
@Override
public InteractionIdentifier convert(AutocompleteInteraction context) {
List<SlashCommandInteractionOption> options = context.getOptions();
String commandName = context.getCommandName();
if (!options.isEmpty()) {
List<SlashCommandInteractionOption> sub_options = context.getOptions().getFirst().getOptions();
if (!sub_options.isEmpty())
return InteractionIdentifier.createHierarchy(
InteractionType.AUTOCOMPLETE,
commandName,
options.getFirst().getName(),
sub_options.getFirst().getName());
else
return InteractionIdentifier.createHierarchy(
InteractionType.AUTOCOMPLETE,
commandName,
options.getFirst().getName());
}else
return InteractionIdentifier.createHierarchy(
InteractionType.AUTOCOMPLETE,
commandName);
}
}

View File

@@ -0,0 +1,16 @@
package net.tomatentum.marinara.wrapper.javacord.identifierconverter;
import org.javacord.api.interaction.ButtonInteraction;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
public class ButtonIdentifierConverter implements IdentifierProvider.Converter<ButtonInteraction> {
@Override
public InteractionIdentifier convert(ButtonInteraction context) {
return InteractionIdentifier.builder().name(context.getCustomId()).type(InteractionType.BUTTON).build();
}
}

View File

@@ -0,0 +1,38 @@
package net.tomatentum.marinara.wrapper.javacord.identifierconverter;
import java.util.List;
import org.javacord.api.interaction.SlashCommandInteraction;
import org.javacord.api.interaction.SlashCommandInteractionOption;
import net.tomatentum.marinara.interaction.InteractionType;
import net.tomatentum.marinara.interaction.ident.InteractionIdentifier;
import net.tomatentum.marinara.wrapper.IdentifierProvider;
public class SlashCommandIdentifierConverter implements IdentifierProvider.Converter<SlashCommandInteraction> {
@Override
public InteractionIdentifier convert(SlashCommandInteraction context) {
List<SlashCommandInteractionOption> options = context.getOptions();
String commandName = context.getCommandName();
if (!options.isEmpty()) {
List<SlashCommandInteractionOption> sub_options = context.getOptions().getFirst().getOptions();
if (!sub_options.isEmpty())
return InteractionIdentifier.createHierarchy(
InteractionType.COMMAND,
commandName,
options.getFirst().getName(),
sub_options.getFirst().getName());
else
return InteractionIdentifier.createHierarchy(
InteractionType.COMMAND,
commandName,
options.getFirst().getName());
}else
return InteractionIdentifier.createHierarchy(
InteractionType.COMMAND,
commandName);
}
}

View File

@@ -0,0 +1,40 @@
package net.tomatentum.marinara.test.javacord;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.Optional;
import org.javacord.api.interaction.AutocompleteInteraction;
import org.javacord.api.interaction.SlashCommandInteractionOption;
import org.junit.jupiter.api.Test;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.javacord.JavacordWrapper;
public class AutoCompleteTest {
@Test
public void testAutocomplete() {
SlashCommandInteractionOption optionMock = mock();
AutocompleteInteraction autocompleteInteractionMock = mock();
when(optionMock.getName()).thenReturn("foo");
when(optionMock.getStringValue()).thenReturn(Optional.of("test"));
when(autocompleteInteractionMock.getCommandName()).thenReturn("test");
when(autocompleteInteractionMock.getFocusedOption()).thenReturn(optionMock);
when(autocompleteInteractionMock.getOptions()).thenReturn(Collections.emptyList());
LibraryWrapper wrapper = new JavacordWrapper(null); //null okay as we don't use the discord API in this test.
Marinara marinara = Marinara.load(wrapper);
marinara.getRegistry().addInteractions(new TestAutocomplete());
wrapper.handleInteraction(autocompleteInteractionMock);
verify(autocompleteInteractionMock).respondWithChoices(any());
}
}

View File

@@ -0,0 +1,26 @@
package net.tomatentum.marinara.test.javacord;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.test.javacord.mocks.CommonMocks;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.javacord.JavacordWrapper;
@TestInstance(Lifecycle.PER_CLASS)
public class ButtonTest {
@Test
public void testButtonExecution() {
LibraryWrapper wrapper = new JavacordWrapper(null); //null okay as we don't use the discord API in this test.
Marinara marinara = Marinara.load(wrapper);
marinara.getRegistry().addInteractions(new TestButton());
wrapper.handleInteraction(CommonMocks.getButtonInteractionMock("test"));
assertTrue(TestButton.didRun);
}
}

View File

@@ -0,0 +1,53 @@
package net.tomatentum.marinara.test.javacord;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.javacord.api.entity.server.Server;
import org.javacord.api.interaction.ButtonInteraction;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.test.javacord.mocks.CommonMocks;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.javacord.JavacordWrapper;
import net.tomatentum.marinara.wrapper.javacord.checks.PermissionCheck;
@TestInstance(Lifecycle.PER_CLASS)
public class InteractionCheckTest {
@Test
public void testInteractionCheck() {
LibraryWrapper wrapper = new JavacordWrapper(null);
Marinara marinara = Marinara.load(wrapper);
marinara.getCheckRegistry().addCheck(new TestInteractionCheck());
marinara.getRegistry().addInteractions(new TestButton());
wrapper.handleInteraction(CommonMocks.getButtonInteractionMock("test"));
assertTrue(TestInteractionCheck.preExecuted);
assertTrue(TestInteractionCheck.postExecuted);
}
@Test
public void testPermissionCheck() {
LibraryWrapper wrapper = new JavacordWrapper(null);
Marinara marinara = Marinara.load(wrapper);
marinara.getCheckRegistry().addCheck(new PermissionCheck());
marinara.getRegistry().addInteractions(new TestButton());
Server serverMock = mock();
ButtonInteraction buttonInteractionMock = CommonMocks.getButtonInteractionMock("permissionCheck", serverMock);
when(serverMock.hasPermissions(any(), any())).thenReturn(false);
wrapper.handleInteraction(buttonInteractionMock);
assertFalse(TestButton.didPermRun);
when(serverMock.hasPermissions(any(), any())).thenReturn(true);
wrapper.handleInteraction(buttonInteractionMock);
assertTrue(TestButton.didPermRun);
}
}

View File

@@ -0,0 +1,71 @@
package net.tomatentum.marinara.test.javacord;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Optional;
import org.javacord.api.DiscordApi;
import org.javacord.api.DiscordApiBuilder;
import org.javacord.api.interaction.SlashCommandInteraction;
import org.javacord.api.interaction.SlashCommandInteractionOption;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import net.tomatentum.marinara.Marinara;
import net.tomatentum.marinara.wrapper.LibraryWrapper;
import net.tomatentum.marinara.wrapper.javacord.JavacordWrapper;
@TestInstance(Lifecycle.PER_CLASS)
public class SlashCommandTest {
String DISCORD_TOKEN = System.getenv("DISCORD_TEST_TOKEN");
DiscordApi api;
@BeforeAll
void setUp() {
api = new DiscordApiBuilder()
.setToken(DISCORD_TOKEN)
.login().join();
}
@AfterAll
void tearDown() {
api.disconnect();
api = null;
}
@Test
void testSlashCommand() {
Marinara marinara = Marinara.load(new JavacordWrapper(api));
marinara.getRegistry().addInteractions(new TestCommand());
marinara.getRegistry().registerCommands();
System.out.println("Success!");
}
@Test
void testSlashCommandExecution() {
LibraryWrapper wrapper = new JavacordWrapper(api);
Marinara marinara = Marinara.load(wrapper);
marinara.getRegistry().addInteractions(new TestCommand());
SlashCommandInteractionOption optionMock = mock();
SlashCommandInteraction interactionMock = mock();
when(optionMock.getName()).thenReturn("foo");
when(optionMock.getStringValue()).thenReturn(Optional.of("test"));
when(interactionMock.getCommandName()).thenReturn("test");
when(interactionMock.getOptions()).thenReturn(Arrays.asList(optionMock));
when(interactionMock.getArguments()).thenReturn(Arrays.asList(optionMock));
when(interactionMock.getOptionByName("foo")).thenReturn(Optional.of(optionMock));
wrapper.handleInteraction(interactionMock);
}
}

View File

@@ -0,0 +1,23 @@
package net.tomatentum.marinara.test.javacord;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Collections;
import org.javacord.api.interaction.AutocompleteInteraction;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.annotation.AutoComplete;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand;
public class TestAutocomplete implements InteractionHandler {
@SlashCommand(name = "test")
@AutoComplete
public void autocomplete(AutocompleteInteraction context, String value) {
System.out.println("Success!");
assertEquals(value, "test");
context.respondWithChoices(Collections.emptyList());
}
}

View File

@@ -0,0 +1,42 @@
package net.tomatentum.marinara.test.javacord;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.message.Message;
import org.javacord.api.entity.permission.PermissionType;
import org.javacord.api.entity.server.Server;
import org.javacord.api.entity.user.User;
import org.javacord.api.interaction.ButtonInteraction;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.annotation.Button;
import net.tomatentum.marinara.test.javacord.TestInteractionCheck.TestCheck;
import net.tomatentum.marinara.wrapper.javacord.checks.PermissionCheck.HasPermission;
public class TestButton implements InteractionHandler {
public static boolean didRun = false;
@Button("test")
@TestCheck
public void exec(ButtonInteraction interaction, TextChannel channel, Message message, User member, Server server) {
assertNotNull(interaction);
assertNotNull(channel);
assertNotNull(message);
assertNotNull(member);
assertNotNull(server);
didRun = true;
System.out.println("Success!");
}
public static boolean didPermRun = false;
@Button("permissionCheck")
@HasPermission({PermissionType.ADMINISTRATOR})
public void exec(ButtonInteraction interaction) {
didPermRun = true;
System.out.println("It worked!");
}
}

View File

@@ -0,0 +1,20 @@
package net.tomatentum.marinara.test.javacord;
import net.tomatentum.marinara.interaction.commands.choice.ChoiceValueProvider;
public enum TestChoiceEnum implements ChoiceValueProvider<String> {
TestValue("testValue"),
FooBar("fooBar"),
Spongebob("spongebob");
private String value;
private TestChoiceEnum(String value) {
this.value = value;
}
@Override
public String getChoiceValue() {
return value;
}
}

View File

@@ -0,0 +1,33 @@
package net.tomatentum.marinara.test.javacord;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.javacord.api.interaction.SlashCommandInteraction;
import net.tomatentum.marinara.interaction.InteractionHandler;
import net.tomatentum.marinara.interaction.commands.annotation.CommandChoices;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommand;
import net.tomatentum.marinara.interaction.commands.annotation.SlashCommandOption;
import net.tomatentum.marinara.interaction.commands.option.SlashCommandOptionType;
public class TestCommand implements InteractionHandler {
@SlashCommand(
name = "test",
description = "testingen",
serverIds = {
1037753048602255440L
},
options = {
@SlashCommandOption(
name = "foo",
description = "foo bar is very fooby",
type = SlashCommandOptionType.STRING,
choices = @CommandChoices(cenum = TestChoiceEnum.class)
)
}
)
public void exec(SlashCommandInteraction interaction, String test) {
assertEquals(test, "test");
System.out.println("Success!");
}
}

View File

@@ -0,0 +1,37 @@
package net.tomatentum.marinara.test.javacord;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import net.tomatentum.marinara.checks.InteractionCheck;
public class TestInteractionCheck implements InteractionCheck<TestInteractionCheck.TestCheck> {
public static boolean preExecuted = false;
public static boolean postExecuted = false;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public static @interface TestCheck {
}
@Override
public boolean preExec(Object context, TestCheck annotation) {
assertNotNull(annotation);
assertNotNull(context);
preExecuted = true;
return true;
}
@Override
public void postExec(Object context, TestCheck annotation) {
assertNotNull(annotation);
assertNotNull(context);
postExecuted = true;
}
}

View File

@@ -0,0 +1,42 @@
package net.tomatentum.marinara.test.javacord.mocks;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Optional;
import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.message.Message;
import org.javacord.api.entity.server.Server;
import org.javacord.api.entity.user.User;
import org.javacord.api.interaction.ButtonInteraction;
public class CommonMocks {
public static ButtonInteraction getButtonInteractionMock(String customId) {
ButtonInteraction buttonInteractionMock = mock();
when(buttonInteractionMock.getCustomId()).thenReturn(customId);
when(buttonInteractionMock.getMessage()).thenReturn(mock(Message.class));
when(buttonInteractionMock.getServer()).thenReturn(Optional.of(mock(Server.class)));
when(buttonInteractionMock.getChannel()).thenReturn(Optional.of(mock(TextChannel.class)));
when(buttonInteractionMock.getUser()).thenReturn(mock(User.class));
return buttonInteractionMock;
}
public static ButtonInteraction getButtonInteractionMock(String customId, Server serverMock) {
ButtonInteraction buttonInteractionMock = mock();
when(buttonInteractionMock.getCustomId()).thenReturn(customId);
when(buttonInteractionMock.getMessage()).thenReturn(mock(Message.class));
when(buttonInteractionMock.getServer()).thenReturn(Optional.of(serverMock));
when(buttonInteractionMock.getChannel()).thenReturn(Optional.of(mock(TextChannel.class)));
when(buttonInteractionMock.getUser()).thenReturn(mock(User.class));
return buttonInteractionMock;
}
}