187 Commits

Author SHA1 Message Date
bea 8e17e55341 move to 0.9.3-snapshot
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-09-06 22:14:58 +02:00
bea 1f2bafa7f8 (wip) migrate commands to components
continuous-integration/drone/push Build is passing
2025-09-06 22:14:44 +02:00
bea eafa80f1d2 rename command classes
continuous-integration/drone/push Build is passing
2025-09-06 21:47:23 +02:00
bea 5ad1d9e891 start moving to @Components
continuous-integration/drone/push Build is passing
2025-09-06 21:44:55 +02:00
bea dcc4785edc edit text
continuous-integration/drone/push Build is passing
2025-09-06 17:59:28 +02:00
bea a1192db62b fix timeout duration check
continuous-integration/drone/push Build is passing
2025-09-05 13:15:39 +02:00
bea 910b7a406c show lowercase version
continuous-integration/drone/push Build is passing
2025-09-05 13:11:43 +02:00
bea eb953390e3 fix ver
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-09-05 13:07:22 +02:00
bea 5d8fc55276 move to 0.9.2-snapshot
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-09-05 13:03:15 +02:00
bea 784ce11d6b remove old #0000 discriminator
continuous-integration/drone/push Build is passing
2025-09-05 13:02:59 +02:00
bea 799891ad6b move to v0.9.1-snapshot
continuous-integration/drone/push Build is passing
2025-09-05 12:50:00 +02:00
bea 0c575378f3 fix concurrency and ack
continuous-integration/drone/push Build is passing
2025-09-05 12:49:37 +02:00
bea 5015e82700 fix build jar
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-09-05 01:53:05 +02:00
bea 29f486566b move util to service
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-09-05 01:35:15 +02:00
bea f80a49995b (wip) migrate to spring beans
continuous-integration/drone/push Build is passing
2025-09-05 01:32:46 +02:00
bea bbd0299103 Merge branch 'main' into develop
continuous-integration/drone/push Build is passing
2025-09-05 00:06:44 +02:00
bea fd2970fa59 cleanup and reformat
continuous-integration/drone/push Build is passing
2025-09-05 00:06:35 +02:00
bea bb674240bb (wip) switch to spring/hibernate
continuous-integration/drone/push Build is passing
2025-09-05 00:05:51 +02:00
bea 5a34e5994e add spring/hibernate deps
continuous-integration/drone/push Build is passing
2025-09-04 23:42:48 +02:00
bea 14b54501fd update drone
continuous-integration/drone/push Build is passing
2025-09-04 23:42:30 +02:00
bea 1928cfe858 update readme
continuous-integration/drone/push Build is passing
2025-09-04 23:41:54 +02:00
bea 0b7880af88 fix drone
continuous-integration/drone/push Build is passing
2025-09-04 23:36:49 +02:00
bea f1969c2043 Update README.MD
continuous-integration/drone/push Build is passing
2025-06-01 21:02:56 +02:00
bea acad4bad8b Merge pull request 'Update dependency org.junit.jupiter:junit-jupiter-api to v5.13.0' (#47) from renovate/org.junit.jupiter-junit-jupiter-api-5.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #47
2025-06-01 20:25:10 +02:00
bea 4ce9acd428 Merge pull request 'Update dependency org.json:json to v20250517' (#46) from renovate/org.json-json-20250517.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #46
2025-06-01 20:25:04 +02:00
bea 2ab52bd713 Merge pull request 'Update dependency com.google.protobuf:protobuf-java to v4.31.1' (#45) from renovate/com.google.protobuf-protobuf-java-4.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #45
2025-06-01 20:24:56 +02:00
bea f38e34c1ce Merge pull request 'Update dependency com.google.code.gson:gson to v2.13.1' (#43) from renovate/com.google.code.gson-gson-2.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #43
2025-06-01 20:24:46 +02:00
bea f87854459f Merge pull request 'Update dependency net.dv8tion:JDA to v5.5.1' (#42) from renovate/net.dv8tion-jda-5.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #42
2025-06-01 20:24:31 +02:00
renovate 72c4ae2133 Update dependency org.junit.jupiter:junit-jupiter-api to v5.13.0
continuous-integration/drone/pr Build is passing
2025-05-30 11:00:27 +00:00
renovate 7f4ca6aa8e Update dependency com.google.protobuf:protobuf-java to v4.31.1
continuous-integration/drone/pr Build is passing
2025-05-28 20:00:24 +00:00
renovate c3793aa159 Update dependency org.json:json to v20250517
continuous-integration/drone/pr Build is passing
2025-05-17 14:00:27 +00:00
renovate 63fc1feaea Update dependency net.dv8tion:JDA to v5.5.1
continuous-integration/drone/pr Build is passing
2025-05-03 12:00:25 +00:00
renovate 71d646ff69 Update dependency com.google.code.gson:gson to v2.13.1
continuous-integration/drone/pr Build is passing
2025-04-24 02:00:23 +00:00
bea 48d537d2db Merge pull request 'Update dependency com.google.code.gson:gson to v2.13.0' (#41) from renovate/com.google.code.gson-gson-2.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #41
2025-04-13 00:55:18 +02:00
bea 136447b9df Merge pull request 'Update dependency org.junit.jupiter:junit-jupiter-api to v5.12.2' (#40) from renovate/org.junit.jupiter-junit-jupiter-api-5.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #40
2025-04-13 00:54:58 +02:00
bea 372949a9e0 Merge pull request 'Update dependency org.apache.commons:commons-text to v1.13.1' (#39) from renovate/org.apache.commons-commons-text-1.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #39
2025-04-13 00:54:46 +02:00
bea 4f4549a81e Merge pull request 'Update dependency org.owasp:dependency-check-maven to v12.1.1' (#38) from renovate/org.owasp-dependency-check-maven-12.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #38
2025-04-13 00:54:33 +02:00
bea b0dd1c21b2 Merge pull request 'Update dependency net.dv8tion:JDA to v5.3.2' (#37) from renovate/net.dv8tion-jda-5.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #37
2025-04-13 00:54:12 +02:00
renovate d46368f0ce Update dependency com.google.code.gson:gson to v2.13.0
continuous-integration/drone/pr Build is failing
2025-04-11 15:00:26 +00:00
renovate 2032dc1d0e Update dependency org.junit.jupiter:junit-jupiter-api to v5.12.2
continuous-integration/drone/pr Build is failing
2025-04-11 15:00:24 +00:00
renovate 9c099230c9 Update dependency org.apache.commons:commons-text to v1.13.1
continuous-integration/drone/pr Build is passing
2025-04-10 23:00:22 +00:00
renovate f2134cbdb9 Update dependency org.owasp:dependency-check-maven to v12.1.1
continuous-integration/drone/pr Build is failing
2025-04-05 13:00:31 +00:00
renovate 97e846c3dc Update dependency net.dv8tion:JDA to v5.3.2
continuous-integration/drone/pr Build is passing
2025-04-05 11:00:29 +00:00
bea 7f16a011b3 update pipeline
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-03-29 16:45:14 +01:00
bea b32ece3f88 Update .drone.yml
continuous-integration/drone/push Build was killed
2025-03-29 13:02:17 +01:00
bea 681785ef0d Merge pull request 'Update dependency net.dv8tion:JDA to v5.3.1' (#36) from renovate/net.dv8tion-jda-5.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #36
2025-03-28 14:07:57 +01:00
renovate a72b6f690b Update dependency net.dv8tion:JDA to v5.3.1
continuous-integration/drone/pr Build is passing
2025-03-27 18:00:34 +00:00
bea 645fb5f4a6 build on arm
continuous-integration/drone/push Build is passing
2025-03-27 00:37:54 +01:00
bea a228067cce update drone
continuous-integration/drone/push Build is failing
2025-03-27 00:36:31 +01:00
bea 4f0eb7ce74 Update .drone.yml
continuous-integration/drone/push Build is passing
2025-03-27 00:22:01 +01:00
bea a7e36299b6 Update .drone.yml
continuous-integration/drone/push Build was killed
2025-03-27 00:21:29 +01:00
bea 1e7c43e360 Merge pull request 'Update dependency com.google.protobuf:protobuf-java to v4.30.2' (#35) from renovate/com.google.protobuf-protobuf-java-4.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2025-03-26 23:21:50 +01:00
renovate 2a4a80cc3f Update dependency com.google.protobuf:protobuf-java to v4.30.2
continuous-integration/drone/pr Build is passing
2025-03-26 20:00:37 +00:00
bea a50e8c050b update pull requests pipe
continuous-integration/drone/push Build is passing
2025-03-25 18:26:00 +01:00
bea f7c1b096bc Merge pull request 'Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5.1.0.4751' (#34) from renovate/org.sonarsource.scanner.maven-sonar-maven-plugin-5.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2025-03-25 14:06:04 +01:00
renovate 884814064c Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5.1.0.4751
continuous-integration/drone/pr Build is failing
2025-03-25 11:00:43 +00:00
bea ce0938bc2c Merge pull request 'Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5' (#33) from renovate/org.sonarsource.scanner.maven-sonar-maven-plugin-5.x into main
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
Reviewed-on: #33
2025-03-23 12:55:48 +01:00
renovate aabfbd3020 Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5
continuous-integration/drone/pr Build was killed
2025-03-23 11:28:03 +00:00
bea 25bc5a3ef2 Merge pull request 'Update dependency commons-codec:commons-codec to v1.18.0' (#25) from renovate/commons-codec-commons-codec-1.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #25
2025-03-23 11:33:45 +01:00
bea ab52f30eb2 Merge pull request 'Update dependency org.apache.commons:commons-text to v1.13.0' (#26) from renovate/org.apache.commons-commons-text-1.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #26
2025-03-23 11:33:36 +01:00
bea 2e0c2e4e14 Merge pull request 'Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.2' (#27) from renovate/org.apache.maven.plugins-maven-javadoc-plugin-3.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #27
2025-03-23 11:33:29 +01:00
bea edf896efb0 Merge pull request 'Update dependency org.yaml:snakeyaml to v2.4' (#31) from renovate/org.yaml-snakeyaml-2.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #31
2025-03-23 11:33:20 +01:00
bea bd7355add9 Merge pull request 'Update dependency org.json:json to v20250107' (#32) from renovate/org.json-json-20250107.x into main
continuous-integration/drone/push Build is failing
Reviewed-on: #32
2025-03-23 11:33:05 +01:00
renovate 6d210551af Update dependency org.json:json to v20250107
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:08 +00:00
renovate abd3c02be6 Update dependency org.yaml:snakeyaml to v2.4
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:06 +00:00
renovate 50749f2108 Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.2
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:04 +00:00
renovate fc7b6d54d1 Update dependency org.apache.commons:commons-text to v1.13.0
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:01 +00:00
renovate f5d684c5a4 Update dependency commons-codec:commons-codec to v1.18.0
continuous-integration/drone/pr Build is failing
2025-03-23 10:24:59 +00:00
bea 5ea12aa693 Merge pull request 'Update dependency org.junit.jupiter:junit-jupiter-api to v5.12.1' (#29) from renovate/org.junit.jupiter-junit-jupiter-api-5.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #29
2025-03-23 10:38:52 +01:00
bea 5c4f7e4252 Merge pull request 'Update dependency org.jsoup:jsoup to v1.19.1' (#28) from renovate/org.jsoup-jsoup-1.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2025-03-23 10:38:35 +01:00
bea d9dcde7560 Merge pull request 'Update dependency com.google.code.gson:gson to v2.12.1' (#24) from renovate/com.google.code.gson-gson-2.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #24
2025-03-23 10:38:05 +01:00
bea 64762a9a4f Merge pull request 'Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v3.11.0.3922' (#30) from renovate/org.sonarsource.scanner.maven-sonar-maven-plugin-3.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #30
2025-03-23 10:37:32 +01:00
bea 4542985431 Merge pull request 'Update dependency org.slf4j:slf4j-simple to v2.0.17' (#23) from renovate/org.slf4j-slf4j-simple-2.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2025-03-23 10:31:39 +01:00
bea 88d4b6461b Merge pull request 'Update dependency org.slf4j:slf4j-api to v2.0.17' (#22) from renovate/org.slf4j-slf4j-api-2.x into main
continuous-integration/drone/push Build is passing
Reviewed-on: #22
2025-03-23 10:31:28 +01:00
bea e8f1c85f08 Merge pull request 'Update dependency com.google.protobuf:protobuf-java to v4.30.1' (#21) from renovate/com.google.protobuf-protobuf-java-4.x into main
continuous-integration/drone/push Build was killed
Reviewed-on: #21
2025-03-23 10:31:14 +01:00
renovate 1ed389c18b Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v3.11.0.3922
continuous-integration/drone/pr Build is failing
2025-03-23 08:25:02 +00:00
renovate 4320c9698a Update dependency org.junit.jupiter:junit-jupiter-api to v5.12.1
continuous-integration/drone/pr Build is failing
2025-03-23 08:25:00 +00:00
renovate 3a8044dda1 Update dependency org.jsoup:jsoup to v1.19.1
continuous-integration/drone/pr Build is failing
2025-03-23 07:25:07 +00:00
renovate 96ca58de12 Update dependency com.google.code.gson:gson to v2.12.1
continuous-integration/drone/pr Build is failing
2025-03-23 05:24:54 +00:00
renovate 5ed444ab92 Update dependency org.slf4j:slf4j-simple to v2.0.17
continuous-integration/drone/pr Build is failing
2025-03-23 05:24:51 +00:00
renovate 19a5583594 Update dependency org.slf4j:slf4j-api to v2.0.17
continuous-integration/drone/pr Build is failing
2025-03-23 04:27:18 +00:00
renovate a39bcf68cb Update dependency com.google.protobuf:protobuf-java to v4.30.1
continuous-integration/drone/pr Build is failing
2025-03-23 04:27:16 +00:00
bea eb08f0bcab Merge pull request 'Configure Renovate' (#20) from renovate/configure into main
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2025-03-23 05:08:56 +01:00
renovate 87eadb88ef Add renovate.json
continuous-integration/drone/pr Build is failing
2025-03-22 23:11:34 +00:00
bea 4f74541d1d move to dev version
continuous-integration/drone/push Build is passing
2025-03-09 12:00:37 +01:00
bea 86f370643e bump to stable 0.6.2
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-03-09 12:00:28 +01:00
bea 27133b8e3c update protobuf dep
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-03-09 12:00:01 +01:00
bea 54fee8a010 move to dev version
continuous-integration/drone/push Build was killed
2025-03-09 11:54:25 +01:00
bea 9e3289c616 bump version to 0.6.1
continuous-integration/drone/push Build is failing
2025-03-09 11:54:09 +01:00
bea a5d647e6ba fix code smells
continuous-integration/drone/push Build is failing
2025-03-09 11:52:26 +01:00
bea 44add27d5e move to dev version
continuous-integration/drone/push Build was killed
2025-03-09 11:46:49 +01:00
bea fa17bf8ef6 bump version to 0.6.0
continuous-integration/drone/push Build is failing
2025-03-09 11:46:28 +01:00
bea 90e576923d update jda version
continuous-integration/drone/push Build is failing
2025-03-09 11:42:06 +01:00
bea ea31746442 move to dev build
continuous-integration/drone/push Build is passing
2025-03-09 11:37:06 +01:00
bea 3c522149c3 make stable version with updated security 2025-03-09 11:36:55 +01:00
bea ed1b25b403 update sonar token
continuous-integration/drone/push Build is passing
2025-03-09 11:31:27 +01:00
bea 04d93dd7a5 fix autocloseable executors
continuous-integration/drone/push Build is passing
2025-03-09 11:30:59 +01:00
bea f523e6cd92 add api key
continuous-integration/drone/push Build is passing
2025-03-09 11:17:30 +01:00
bea 35f52ec583 fix build
continuous-integration/drone/push Build was killed
2025-03-09 11:07:00 +01:00
bea 549f8bb48b update jdk and deps-ckeck plugin
continuous-integration/drone/push Build is failing
2025-03-09 11:03:01 +01:00
bea be9bd1a068 update vulnerable deps
continuous-integration/drone/push Build is failing
2025-03-09 11:01:18 +01:00
bea 2a7be8868c update drone
continuous-integration/drone/push Build is failing
2025-03-09 00:45:02 +01:00
bea e51646ace4 simplify method 2025-03-09 00:14:07 +01:00
bea 4abd3d6179 Suppress unneeded security warning
continuous-integration/drone/push Build is passing
2023-02-11 17:10:50 +01:00
bea 0f2e2f876d Revert "Add .deepsource.toml"
continuous-integration/drone/push Build is passing
This reverts commit b174c581e9.
2023-01-25 20:42:11 +01:00
bea 06b28aac70 Fix flawed logic in verbosity enable method
continuous-integration/drone/push Build is passing
2023-01-24 14:45:44 +01:00
bea 9f52e8747c Fix first arg not getting recognized in some cases (fixes #18)
continuous-integration/drone/push Build is failing
2023-01-24 14:41:32 +01:00
bea 829e19fac3 Prevent trying to delete "say" message in DMs
continuous-integration/drone/push Build is passing
2023-01-22 12:42:49 +01:00
bea ff323b9d8b Update JDA version and fix related CVEs
continuous-integration/drone/push Build is passing
2023-01-16 17:23:31 +01:00
bea 5aa99ae4bf Make builds fail on CVSS >= 8
continuous-integration/drone/push Build is failing
2023-01-16 08:21:35 +01:00
bea 96465af441 Update suppressions
continuous-integration/drone/push Build is passing
2023-01-16 08:20:43 +01:00
bea f4fc8811ef Suppress SnakeYaml detections
continuous-integration/drone/push Build is passing
2023-01-16 08:14:26 +01:00
bea 3abb48ba60 Revert to base Maven image due to missing arm64 binaries
continuous-integration/drone/push Build is passing
2023-01-16 08:11:55 +01:00
bea d58cc08082 Move to different dependency check container
continuous-integration/drone/push Build is failing
2023-01-16 08:03:01 +01:00
bea 1675b62967 Use prebuilt dependencies database
continuous-integration/drone/push Build is failing
2023-01-16 07:52:45 +01:00
bea cc29b63d78 Fix suppression files entry
continuous-integration/drone/push Build is passing
2023-01-16 07:47:46 +01:00
bea d816a1f1d9 Fix suppression files entry
continuous-integration/drone/push Build is passing
2023-01-16 07:42:30 +01:00
bea d788070eb8 Ignore CVE-2022-1471
continuous-integration/drone/push Build is passing
It doesn't affect the project
2023-01-16 07:40:03 +01:00
bea 627f6deb97 Add default cases to switches
continuous-integration/drone/push Build is passing
2023-01-16 07:20:49 +01:00
bea 668375367a Use Java 16 "instanceof" pattern matching
continuous-integration/drone/push Build is passing
2023-01-16 07:15:51 +01:00
bea 980cf5eef3 Prevent instantiating utility classes
continuous-integration/drone/push Build is passing
2023-01-16 07:07:42 +01:00
bea ccf69a2903 Update dependency checker configuration
continuous-integration/drone/push Build is passing
2023-01-16 05:48:27 +01:00
bea 6c6cdab9f4 Switch to XML dependency report
continuous-integration/drone/push Build is passing
2023-01-16 05:39:51 +01:00
bea 05efe6c0d3 Try to rely only on JSON dependency report
continuous-integration/drone/push Build is passing
2023-01-16 05:34:04 +01:00
bea 4f615378a6 Implement Maven dependency checker
continuous-integration/drone/push Build is passing
2023-01-16 05:24:40 +01:00
bea 118979bde4 Fix corrupted database sql statements
continuous-integration/drone/push Build is passing
2023-01-16 05:00:18 +01:00
bea 009fec3be3 Reformat database class and queries using text blocks
continuous-integration/drone/push Build is passing
2023-01-16 04:57:25 +01:00
bea 546637c188 Improve various small code quality issues
continuous-integration/drone/push Build is passing
2023-01-16 03:53:51 +01:00
bea d315b3f38a Bump to development version
continuous-integration/drone/push Build is passing
2023-01-16 02:32:46 +01:00
bea 94037b252f Improve final fields naming
continuous-integration/drone/push Build is passing
2023-01-16 02:31:11 +01:00
bea d62d6bdfdd Bump to stable version 0.5.19
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-01-16 02:18:24 +01:00
bea 7f73d4fb23 Fix shutdown method no longer shutting down when invocated directly
continuous-integration/drone/push Build is passing
2023-01-16 02:11:16 +01:00
bea 383b09a53e Register shutdown hook without relying on Sun proprietary method
continuous-integration/drone/push Build is passing
2023-01-16 02:07:27 +01:00
bea c9d69c512c Enable arm64 for Drone CI
continuous-integration/drone/push Build is passing
2023-01-16 01:47:58 +01:00
bea 8b1c1b4d04 Make repository URL a property
continuous-integration/drone/push Build was killed
2023-01-16 00:41:45 +01:00
bea a28781b806 Update repo URLs
continuous-integration/drone/push Build is running
2023-01-16 00:33:52 +01:00
bea 78bdadad06 Add info about random.org
continuous-integration/drone/push Build is passing
2023-01-15 23:02:41 +01:00
bea a5b9f9d993 Add basic development info to readme 2023-01-15 23:02:13 +01:00
bea 64f0b611ca Bump to stable version 0.5.18
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-01-15 22:21:20 +01:00
bea f87d461b89 Update README.md
continuous-integration/drone/push Build is passing
2023-01-15 22:14:08 +01:00
bea 4f71b5f599 Add direct download link for always up-to-date JAR file
continuous-integration/drone/push Build is passing
2023-01-15 22:08:45 +01:00
bea 12e3c5fc2f Remove maven reposity upload in normal commit builds
continuous-integration/drone/push Build is passing
2023-01-15 21:56:24 +01:00
bea bec705ea6c Finish implementing functional Maven build system
continuous-integration/drone/push Build is passing
2023-01-15 21:54:03 +01:00
bea 3acab18ff2 Update DroneCI configuration, include settings.xml
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:38:50 +01:00
bea 99a92badc2 Update DroneCI configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:32:15 +01:00
bea f24c93acc0 Update DroneCI configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:30:44 +01:00
bea 3cc37b4669 Update DroneCI configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:26:55 +01:00
bea 5a74401f74 Update DroneCI configuration
continuous-integration/drone/push Build is passing
2023-01-15 21:26:35 +01:00
bea db151a3f66 Update DroneCI configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:25:08 +01:00
bea c1e995225f Update DroneCI configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:23:54 +01:00
bea ae1313459d Fix typo
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:21:59 +01:00
bea ea29241d1d Attempt to fix Maven DoneCI integration 2023-01-15 21:21:26 +01:00
bea ecff8ee31b Update temporary DroneCI configuration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:18:10 +01:00
bea 9578fcdbc2 Fix SonarQube integration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:09:03 +01:00
bea 944b4cf905 Update .drone.yml
continuous-integration/drone/push Build is failing
2023-01-15 21:07:41 +01:00
bea 21a5e07b3e Attempt to implement Nexus Maven repo deployment
continuous-integration/drone/push Build is failing
2023-01-15 21:06:01 +01:00
bea 32a5a46565 Update badges, remove SFTP DroneCI integration
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-01-15 20:21:04 +01:00
bea 5dcfd7339e Update DroneCI integration to allow artifact uploading
continuous-integration/drone/push Build is running
continuous-integration/drone Build is failing
2023-01-15 17:53:07 +01:00
bea 3bb006e0c6 Update badges
continuous-integration/drone/push Build is passing
2023-01-15 05:32:11 +01:00
bea 0cc30ddf29 Implement MessageRespone base functions
continuous-integration/drone/push Build is passing
2023-01-15 05:29:57 +01:00
bea b55a27fdfb Optimize regex expressions
continuous-integration/drone/push Build is passing
2023-01-15 05:22:22 +01:00
bea ee4c5155fa Improve exception handling
continuous-integration/drone/push Build is passing
2023-01-15 05:00:44 +01:00
bea d2abeb35fc Implement various null checks
continuous-integration/drone/push Build is passing
2023-01-15 04:51:30 +01:00
bea 546eb49144 Add empty line to logo
continuous-integration/drone/push Build is passing
2023-01-15 04:42:43 +01:00
bea 6b97e0bb7e Remove unused variable
continuous-integration/drone/push Build is passing
2023-01-15 04:39:59 +01:00
bea 4df2429b09 Move random methods to random util class, fix footer
continuous-integration/drone/push Build is passing
2023-01-15 04:34:39 +01:00
bea 7de23d8207 Bump version to 0.5.17
continuous-integration/drone/push Build is passing
2023-01-15 04:26:43 +01:00
bea d7aa5d75eb Implement random.org API integration with random seed updater
continuous-integration/drone/push Build is passing
2023-01-15 04:26:06 +01:00
bea b81a7e65d2 Disable random seed update because SecureRandom is self-seeding with a better algorithm
continuous-integration/drone/push Build is passing
2023-01-15 03:34:34 +01:00
bea 14d2505dac Use Java 15's new text blocks for Unicode logo
continuous-integration/drone/push Build is passing
Looks way better than String concatenation
2023-01-15 02:13:35 +01:00
bea 528940a9d1 Disable JavaDoc generation in DroneCI pipeline
continuous-integration/drone/push Build is passing
2023-01-15 02:06:57 +01:00
bea 4c653fc93c Complete moving to SFL4J
continuous-integration/drone/push Build is passing
2023-01-15 02:05:23 +01:00
bea 6bbaf3fe7e Deprecate logger and start moving to SLF4J
continuous-integration/drone/push Build is passing
JDA already has SLF4J as a requirement, so we might as well use that instead of making our own.
2023-01-15 01:48:56 +01:00
bea 95b4f81235 Switch to SecureRandom class
continuous-integration/drone/push Build is passing
2023-01-15 01:07:31 +01:00
bea dee00e6814 Update badges
continuous-integration/drone/push Build is passing
2023-01-15 01:04:55 +01:00
bea d6ef0da167 Make multiple small improvements
continuous-integration/drone/push Build is passing
2023-01-15 01:00:44 +01:00
bea fb752fb9a9 Fix code style error
continuous-integration/drone/push Build is passing
2023-01-15 00:40:19 +01:00
bea 0d862da9ec Update badges in readme
continuous-integration/drone/push Build is passing
2023-01-15 00:08:31 +01:00
bea 57a5972d59 Add badges to readme
continuous-integration/drone/push Build is passing
2023-01-15 00:07:07 +01:00
bea 374f979ae3 Fix synchronized mismatch
continuous-integration/drone/push Build is passing
2023-01-14 23:52:12 +01:00
bea 53abab6bf3 Update DroneCI config
continuous-integration/drone/push Build is passing
2023-01-14 23:49:20 +01:00
bea 5591b8abab Make method synchronized
continuous-integration/drone/push Build is passing
2023-01-14 23:30:57 +01:00
bea 5afb398299 Update DroneCI config 2023-01-14 23:27:50 +01:00
bea 0d8c3e2be3 Implement SonarQube support
continuous-integration/drone/push Build is failing
2023-01-14 21:49:47 +01:00
bea 818a25346b Optimize imports
continuous-integration/drone/push Build is passing
2023-01-14 20:22:37 +01:00
bea 28286f5389 Fix date parser
continuous-integration/drone/push Build is passing
2023-01-14 20:20:59 +01:00
DeepSource Bot b174c581e9 Add .deepsource.toml
continuous-integration/drone/push Build is passing
2023-01-14 19:16:58 +00:00
104 changed files with 2889 additions and 1919 deletions
+13 -13
View File
@@ -1,14 +1,14 @@
kind: pipeline
name: default
trigger:
branch:
kind: template
load: java-build-deploy.yaml
data:
arch: arm64
os: linux
build_branches:
- main
steps:
- name: build
image: maven:3-eclipse-temurin-16
commands:
- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
- mvn javadoc:javadoc
- mvn test -B
- develop
build_events:
- push
- pull_request
sonar_project_key: HidekoBot
deploy_targets:
- production
+1
View File
@@ -1,3 +1,4 @@
target/
.idea/
scripts/
*.sqlite
+88
View File
@@ -0,0 +1,88 @@
---
kind: pipeline
type: docker
name: build
platform:
os: {{ .input.os }}
arch: {{ .input.arch }}
trigger:
branch:
{{- range .input.build_branches }}
- {{ . }}
{{- end }}
event:
{{- range .input.build_events }}
- {{ . }}
{{- end }}
# Global project-specific environment variables
environment:
{{- range .input.envs }}
{{ .name }}: {{ .value }}
{{- end }}
steps:
# Test if it compiles correctly
- name: build
image: maven:3-eclipse-temurin-21
commands:
- mvn verify --no-transfer-progress -DskipTests=true -Dmaven.javadoc.skip=true -B -V
# Run unit tests
- name: test
image: maven:3-eclipse-temurin-21
commands:
- mvn test --no-transfer-progress -B -V
# Check maven dependencies
- name: dependency-check
image: owasp/dependency-check:latest
commands:
- dependency-check --scan /src --format ALL --out /src/target --nvdApiKey $NVD_API_KEY
environment:
NVD_API_KEY:
from_secret: nvd_api_key
# Run code analysis
- name: code-analysis
when:
event:
- push
image: maven:3-eclipse-temurin-21
commands:
- mvn sonar:sonar --no-transfer-progress -Dsonar.projectKey={{ .input.sonar_project_key }} -Dsonar.host.url=$SONAR_INSTANCE_URL -Dsonar.token=$SONAR_LOGIN_KEY -B -V
environment:
SONAR_INSTANCE_URL:
from_secret: sonar_instance_url
SONAR_LOGIN_KEY:
from_secret: sonar_login_key
---
kind: pipeline
type: kubernetes
name: deploy
trigger:
event:
- promote
target:
{{- range .input.deploy_targets }}
- {{ . }}
{{- end }}
# Global project-specific environment variables
environment:
{{- range .input.envs }}
{{ .name }}: {{ .value }}
{{- end }}
steps:
# Upload to Maven repository
- name: maven-deploy
image: maven:3-eclipse-temurin-21
commands:
- mvn deploy --no-transfer-progress -DskipTests=true -Dmaven.javadoc.skip=true -B -V -gs settings.xml -Dmaven.repo.username=$MAVEN_REPO_USERNAME -Dmaven.repo.password=$MAVEN_REPO_PASSWORD
environment:
MAVEN_REPO_USERNAME:
from_secret: maven_repo_username
MAVEN_REPO_PASSWORD:
from_secret: maven_repo_password
+19 -3
View File
@@ -1,7 +1,16 @@
# HidekoBot
[![Reliability Rating](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=reliability_rating&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
[![Maintainability Rating](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=sqale_rating&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
[![Security Rating](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=security_rating&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
[![Build Status](https://drone.beatrice.wtf/api/badges/bea/HidekoBot/status.svg)](https://drone.beatrice.wtf/bea/HidekoBot)
[![Lines of Code](https://sonar.beatrice.wtf/api/project_badges/measure?project=HidekoBot&metric=ncloc&token=0a63c149148555d6d2ee40665af1afae8f67cc3f)](https://sonar.beatrice.wtf/dashboard?id=HidekoBot)
Hideko is a general-purpose Discord bot.
## Download
The latest stable version is always uploaded automatically to the [Maven repository](https://nexus.beatrice.wtf/#browse/browse:maven-releases:wtf%2Fbeatrice%2Fhidekobot%2FHidekoBot).
You can download the JAR directly by clicking [here](https://nexus.beatrice.wtf/service/rest/v1/search/assets/download?sort=version&repository=maven-releases&maven.groupId=wtf.beatrice.hidekobot&maven.artifactId=HidekoBot&maven.extension=jar).
## Startup
Download a prebuilt JAR file or build it from source, then run it with:
```bash
@@ -14,11 +23,11 @@ Additionally available parameters are:
- **verbose**: log every message that the bot receives, plus additional debugging messages. Very spammy and performance heavy.
- **refresh**: force refresh the slash commands. This is useful in case there was a simple update to a command that did not drastically change it, so no changes are found at bootup (eg: fixing a typo in the command description).
*Note: Java 16 or later is required.*
*Note: Java 21 or later is required.*
## Initial setup
Run the startup command once. The bot will generate a `config.yml` file in the directory you were when you ran it.
Run the startup command once. The bot will generate a `config.yml` file in your current directory (`$PWD` on GNU/Linux).
Edit the configuration file and set all values according to your needs.
@@ -28,5 +37,12 @@ already set-up. The bot supports both slash commands and message commands, with
commands support both systems, but some of them are limited in one way or another.
The bot currently supports SQLite as a database backend. A database file will be created after the first boot
in the same directory that you ran it. Do not delete the database file to avoid corruption and unpredictable
in your current directory. Do not delete the database file to avoid corruption and unpredictable
behavior.
# Development
## Versioning
This project uses the `x.y.z-releaseType` schema for releases.
Development builds are tagged as `x.y.z-SNAPSHOT` and sometimes pushed to the snapshots Maven repository.
Stable builds are tagged as `x.y.z` and always pushed to the releases Maven repository, by promoting the build on
[Drone](https://drone.beatrice.wtf/). Currently, promoting stable builds is a manual process.
+140 -37
View File
@@ -6,58 +6,129 @@
<groupId>wtf.beatrice.hidekobot</groupId>
<artifactId>HidekoBot</artifactId>
<version>0.5.16</version>
<version>0.9.3-SNAPSHOT</version>
<properties>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sonar.dependencyCheck.htmlReportPath>./target/dependency-check-report.html
</sonar.dependencyCheck.htmlReportPath>
<sonar.dependencyCheck.jsonReportPath>./target/dependency-check-report.json
</sonar.dependencyCheck.jsonReportPath>
<sonar.dependencyCheck.summarize>true</sonar.dependencyCheck.summarize>
</properties>
<dependencies>
<!-- Basic JDA dependency for Discord API -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-beta.2</version>
<version>5.5.1</version>
</dependency>
<!-- JDA depends on SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.4</version>
<version>2.0.17</version>
</dependency>
<!-- Dependency used for SQLite database connections-->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.39.4.1</version>
<version>3.49.1.0</version>
</dependency>
<!-- Dependency used for YAML configuration files -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
<version>2.4</version>
</dependency>
<!-- JSoup is used to parse HTML into JSON objects for better handling in Java -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
<version>1.19.1</version>
</dependency>
<!-- Various String manipulation utils -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
<version>1.13.1</version>
</dependency>
<!-- JSON dependency used for better parsing of JSON files -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220924</version>
<version>20250517</version>
</dependency>
<!-- Start Random.org dependencies -->
<dependency>
<groupId>com.github.jinahya</groupId>
<artifactId>random-org-json-rpc</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.18.0</version>
</dependency>
<!-- End Random.org dependencies -->
<!-- Unit Tests Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.4.5</version>
</dependency>
<!-- Hibernate core + SQLite dialects -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.6.1.Final</version>
</dependency>
<!-- Jakarta Persistence API (usually provided via Hibernate, but include explicitly for compile-time types) -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
<!-- override dependencies to use newer versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.31.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<resources>
@@ -70,34 +141,66 @@
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>wtf.beatrice.hidekobot.HidekoBot</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>5.1.0.4751</version>
</plugin>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.1.1</version>
<configuration>
<failBuildOnCVSS>8</failBuildOnCVSS>
<!--suppress UnresolvedMavenProperty -->
<nvdApiKey>${nvdApiKey}</nvdApiKey>
<knownExploitedUrl>
https://raw.githubusercontent.com/EugenMayer/cisa-known-exploited-mirror/main/known_exploited_vulnerabilities.json
</knownExploitedUrl>
<formats>
<format>html</format>
<format>json</format>
</formats>
<suppressionFiles>
<suppressionFile>./suppressions.xml</suppressionFile>
</suppressionFiles>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.4.5</version>
<configuration>
<!-- Replace the main artifact (no classifier) -->
<classifier></classifier>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>nexus-releases</id>
<url>https://nexus.beatrice.wtf/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<url>https://nexus.beatrice.wtf/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
<servers>
<server>
<id>nexus-snapshots</id>
<username>${maven.repo.username}</username>
<password>${maven.repo.password}</password>
</server>
<server>
<id>nexus-releases</id>
<username>${maven.repo.username}</username>
<password>${maven.repo.password}</password>
</server>
</servers>
</settings>
+141 -79
View File
@@ -1,35 +1,47 @@
package wtf.beatrice.hidekobot;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.datasources.ConfigurationEntry;
import wtf.beatrice.hidekobot.datasources.ConfigurationSource;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.datasources.PropertiesSource;
import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import wtf.beatrice.hidekobot.listeners.MessageLogger;
import wtf.beatrice.hidekobot.listeners.SlashCommandCompletionListener;
import wtf.beatrice.hidekobot.listeners.SlashCommandListener;
import wtf.beatrice.hidekobot.util.Logger;
import wtf.beatrice.hidekobot.util.Services;
import java.awt.*;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class Cache
{
private Cache()
{
throw new IllegalStateException("Utility class");
}
// todo: make this compatible with the message listener's regex
private static final String botPrefix = "hideko";
private static final Logger logger = new Logger(Cache.class);
private static final String BOT_PREFIX = "hideko";
private static final Logger LOGGER = LoggerFactory.getLogger(Cache.class);
// the Random instance that we should always use when looking for an RNG based thing.
// the seed is updated periodically.
private static final Random randomInstance = new Random();
private static Services SERVICES;
public static void setServices(Services services)
{
SERVICES = services;
}
public static Services getServices()
{
return SERVICES;
}
// map to store results of "love calculator", to avoid people re-running the same command until
// they get what they wanted.
@@ -39,14 +51,13 @@ public class Cache
private static PropertiesSource propertiesSource = null;
private static ConfigurationSource configurationSource = null;
private static DatabaseSource databaseSource = null;
private static boolean verbose = false;
private static MessageLogger verbosityLogger = null;
private static final long botMaintainerId = 979809420714332260L;
private final static String expiryTimestampFormat = "yy/MM/dd HH:mm:ss";
private static final long BOT_MAINTAINER_ID = 979809420714332260L;
private static final String EXPIRY_TIMESTAMP_FORMAT = "yy/MM/dd HH:mm:ss";
// note: discord sets interactions' expiry time to 15 minutes by default, so we can't go higher than that.
private final static long expiryTimeSeconds = 30L;
private static final long EXPIRY_TIME_SECONDS = 30L;
// used to count e.g. uptime
private static LocalDateTime startupTime = null;
@@ -55,16 +66,16 @@ public class Cache
private static final LocalDateTime botBirthDate = LocalDateTime.of(2022, 8, 25, 21, 50);
// the scheduler that should always be used when running a scheduled task.
private final static ScheduledExecutorService taskScheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources
private static final ScheduledExecutorService taskScheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources
private final static String execPath = System.getProperty("user.dir");
private static final String botName = "Hideko";
private static final String EXEC_PATH = System.getProperty("user.dir");
private static final String BOT_NAME = "Hideko";
private static SlashCommandListener slashCommandListener = null;
private static SlashCommandCompletionListener slashCommandCompletionListener = null;
private static MessageCommandListener messageCommandListener = null;
private final static String defaultInviteLink =
private static final String DEFAULT_INVITE_LINK =
"https://discord.com/api/oauth2/authorize?client_id=%userid%&scope=bot+applications.commands&permissions=8";
private static String botApplicationId = "";
@@ -78,22 +89,21 @@ public class Cache
*
* @return array of supported resolutions.
*/
public static int[] getSupportedAvatarResolutions() { return supportedAvatarResolutions; }
public static Random getRandom() {
return randomInstance;
public static int[] getSupportedAvatarResolutions()
{
return supportedAvatarResolutions;
}
public static void setRandomSeed(long seed) {
randomInstance.setSeed(seed);
}
/**
* Checks if the bot has been started with the verbose argument.
*
* @return a boolean which is true if the bot is in verbose-mode
*/
public static boolean isVerbose() { return verbose; }
public static synchronized boolean isVerbose()
{
return verbose;
}
/**
* Set the bot's verbosity status at runtime.
@@ -101,24 +111,20 @@ public class Cache
*
* @param v the verbosity boolean value
*/
public static void setVerbose(boolean v)
public static synchronized void setVerbose(boolean v)
{
verbose = v;
if(v)
{
if(verbosityLogger == null)
{
verbosityLogger = new MessageLogger();
}
HidekoBot.getAPI().addEventListener(verbosityLogger);
} else {
if (verbosityLogger != null)
{
HidekoBot.getAPI().removeEventListener(verbosityLogger);
verbosityLogger = null;
}
if (v)
{
verbosityLogger = new MessageLogger();
HidekoBot.getAPI().addEventListener(verbosityLogger);
}
}
@@ -127,7 +133,8 @@ public class Cache
*
* @return a long of the account's id
*/
public static long getBotOwnerId() {
public static long getBotOwnerId()
{
return configurationSource == null ? 0L : (Long) configurationSource.getConfigValue(ConfigurationEntry.BOT_OWNER_ID);
}
@@ -137,17 +144,22 @@ public class Cache
*
* @return a String of the bot's token.
*/
public static String getBotToken() {
public static String getBotToken()
{
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.BOT_TOKEN);
}
/**
* Get the bot maintainer's profile id.
*
* @return a long of the account's id
*/
public static long getBotMaintainerId() { return botMaintainerId; }
public static long getBotMaintainerId()
{
return BOT_MAINTAINER_ID;
}
/**
* Set the bot's application id.
@@ -164,34 +176,21 @@ public class Cache
*
* @return a string of the bot's application id
*/
public static String getBotApplicationId() { return botApplicationId; }
public static String getBotApplicationId()
{
return botApplicationId;
}
/**
* Function to generate an invite link for the bot
*
* @return a string containing the invite link
*/
public static String getInviteUrl() {
return defaultInviteLink.replace("%userid%", botApplicationId);
}
/**
* Set the already fully-initialized DatabaseSource instance, ready to be accessed and used.
*
* @param databaseSourceInstance the fully-initialized DatabaseSource instance.
*/
public static void setDatabaseSourceInstance(DatabaseSource databaseSourceInstance)
public static String getInviteUrl()
{
databaseSource = databaseSourceInstance;
return DEFAULT_INVITE_LINK.replace("%userid%", botApplicationId);
}
/**
* Get the fully-initialized DatabaseSource instance, ready to be used.
*
* @return the DatabaseSource instance.
*/
public static @Nullable DatabaseSource getDatabaseSource() { return databaseSource; }
/**
* Set the properties source instance loaded from the JAR archive.
*
@@ -207,25 +206,46 @@ public class Cache
*
* @return the String of the DateTimeFormatter format.
*/
public static String getExpiryTimestampFormat(){ return expiryTimestampFormat; }
public static String getExpiryTimestampFormat()
{
return EXPIRY_TIMESTAMP_FORMAT;
}
/**
* Get the amount of seconds after which a message expires.
*
* @return long value of the expiry seconds.
*/
public static long getExpiryTimeSeconds() { return expiryTimeSeconds; }
public static long getExpiryTimeSeconds()
{
return EXPIRY_TIME_SECONDS;
}
public static String getBotName() { return botName; };
public static String getBotName()
{
return BOT_NAME;
}
/**
* Get the bot's version.
*
* @return a String of the bot version.
*/
public static String getBotVersion() {
return propertiesSource.getProperty("bot.version");
public static String getBotVersion()
{
return propertiesSource.getProperty("bot.version").toLowerCase();
}
/**
* Get the bot's source code URL.
*
* @return a String containing the base URL of the repository, including a <b>trailing slash</b>.
*/
public static String getRepositoryUrl()
{
String url = propertiesSource.getProperty("repo.base_url");
return url.endsWith("/") ? url : url + "/";
}
/**
@@ -233,17 +253,20 @@ public class Cache
*
* @return the Color object.
*/
public static Color getBotColor() {
public static Color getBotColor()
{
Color defaultColor = Color.PINK;
if (configurationSource == null) return defaultColor;
String colorName = (String) configurationSource.getConfigValue(ConfigurationEntry.BOT_COLOR);
Color color = null;
try {
try
{
Field field = Color.class.getField(colorName);
color = (Color) field.get(null);
} catch (RuntimeException | NoSuchFieldException | IllegalAccessException e) {
logger.log("Unknown color: " + colorName);
} catch (RuntimeException | NoSuchFieldException | IllegalAccessException e)
{
LOGGER.error("Unknown color: {}", colorName);
}
return color == null ? defaultColor : color;
}
@@ -251,21 +274,36 @@ public class Cache
//todo javadocs
public static void setSlashCommandListener(SlashCommandListener commandListener)
{ slashCommandListener = commandListener; }
{
slashCommandListener = commandListener;
}
public static SlashCommandListener getSlashCommandListener() { return slashCommandListener; }
public static SlashCommandListener getSlashCommandListener()
{
return slashCommandListener;
}
public static void setSlashCommandCompletionListener(SlashCommandCompletionListener commandCompletionListener)
{ slashCommandCompletionListener = commandCompletionListener; }
{
slashCommandCompletionListener = commandCompletionListener;
}
public static SlashCommandCompletionListener getSlashCommandCompletionListener() { return slashCommandCompletionListener; }
public static SlashCommandCompletionListener getSlashCommandCompletionListener()
{
return slashCommandCompletionListener;
}
public static void setMessageCommandListener(MessageCommandListener commandListener)
{ messageCommandListener = commandListener; }
{
messageCommandListener = commandListener;
}
public static MessageCommandListener getMessageCommandListener() { return messageCommandListener; }
public static MessageCommandListener getMessageCommandListener()
{
return messageCommandListener;
}
/**
* Set the bot's startup time. Generally only used at boot time.
@@ -273,7 +311,9 @@ public class Cache
* @param time a LocalDateTime of the startup moment.
*/
public static void setStartupTime(LocalDateTime time)
{ startupTime = time; }
{
startupTime = time;
}
/**
@@ -281,33 +321,54 @@ public class Cache
*
* @return a LocalDateTime object of the startup instant.
*/
public static LocalDateTime getStartupTime() { return startupTime; }
public static LocalDateTime getStartupTime()
{
return startupTime;
}
/**
* Get the time of when the bot was created.
*
* @return a LocalDateTime object of the first commit's instant.
*/
public static LocalDateTime getBotBirthDate() { return botBirthDate; }
public static LocalDateTime getBotBirthDate()
{
return botBirthDate;
}
public static String getFullHeartBeatLink() {
public static String getFullHeartBeatLink()
{
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.HEARTBEAT_LINK);
}
//todo javadocs
public static String getExecPath() { return execPath; }
public static String getExecPath()
{
return EXEC_PATH;
}
/*private static ConfigurationSource getConfigurationSource()
{ return configurationSource; }*/
public static String getRandomOrgApiKey()
{
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.RANDOM_ORG_API_KEY);
}
public static void setConfigurationSource(ConfigurationSource configurationSource)
{ Cache.configurationSource = configurationSource; }
{
Cache.configurationSource = configurationSource;
}
/**
* Get the bot's prefix
*
* @return a String of the bot's prefix.
*/
public static String getBotPrefix() { return botPrefix; }
public static String getBotPrefix()
{
return BOT_PREFIX;
}
public static void cacheLoveCalculatorValue(String userId1, String userId2, int value)
{
@@ -332,7 +393,8 @@ public class Cache
loveCalculatorValues.remove(userId2 + "|" + userId1);
}
public static ScheduledExecutorService getTaskScheduler() {
public static ScheduledExecutorService getTaskScheduler()
{
return taskScheduler;
}
@@ -4,20 +4,27 @@ import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.requests.GatewayIntent;
import sun.misc.Signal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.context.ConfigurableApplicationContext;
import wtf.beatrice.hidekobot.commands.completer.ProfileImageCommandCompleter;
import wtf.beatrice.hidekobot.commands.message.HelloCommand;
import wtf.beatrice.hidekobot.commands.message.*;
import wtf.beatrice.hidekobot.commands.slash.*;
import wtf.beatrice.hidekobot.datasources.ConfigurationSource;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.datasources.PropertiesSource;
import wtf.beatrice.hidekobot.listeners.*;
import wtf.beatrice.hidekobot.runnables.ExpiredMessageTask;
import wtf.beatrice.hidekobot.runnables.HeartBeatTask;
import wtf.beatrice.hidekobot.runnables.RandomSeedTask;
import wtf.beatrice.hidekobot.runnables.RandomOrgSeedTask;
import wtf.beatrice.hidekobot.runnables.StatusUpdateTask;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.util.Logger;
import wtf.beatrice.hidekobot.services.CommandService;
import wtf.beatrice.hidekobot.services.DatabaseService;
import wtf.beatrice.hidekobot.util.FormatUtil;
import wtf.beatrice.hidekobot.util.RandomUtil;
import wtf.beatrice.hidekobot.util.Services;
import java.io.File;
import java.time.LocalDateTime;
@@ -28,39 +35,52 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class HidekoBot
{
private static JDA jda;
private static final Logger logger = new Logger(HidekoBot.class);
private static final Logger LOGGER = LoggerFactory.getLogger(HidekoBot.class);
public static void main(String[] args)
{
// load configuration
logger.log("Loading configuration...");
LOGGER.info("Loading configuration...");
String configFilePath = Cache.getExecPath() + File.separator + "config.yml";
ConfigurationSource configurationSource = new ConfigurationSource(configFilePath);
configurationSource.initConfig();
Cache.setConfigurationSource(configurationSource);
logger.log("Configuration loaded!");
LOGGER.info("Configuration loaded!");
// load properties
logger.log("Loading properties...");
LOGGER.info("Loading properties...");
PropertiesSource propertiesSource = new PropertiesSource();
propertiesSource.load();
Cache.setPropertiesSourceInstance(propertiesSource);
logger.log("Properties loaded!");
LOGGER.info("Properties loaded!");
// check loaded bot token
String botToken = Cache.getBotToken();
if (botToken == null || botToken.isEmpty())
{
logger.log("Invalid bot token!");
LOGGER.error("Invalid bot token!");
shutdown();
return;
}
ApplicationHome home = new ApplicationHome(HidekoBot.class);
System.setProperty("APP_HOME", home.getDir().getAbsolutePath());
ConfigurableApplicationContext context = SpringApplication.run(HidekoBot.class, args);
CommandService commandService = context.getBean(CommandService.class);
DatabaseService databaseService = context.getBean(DatabaseService.class);
Services services = new wtf.beatrice.hidekobot.util.Services(
commandService,
databaseService
);
Cache.setServices(services);
try
{
// try to create the bot object and authenticate it with discord.
@@ -74,9 +94,14 @@ public class HidekoBot
);
jda = jdaBuilder.build().awaitReady();
} catch (InterruptedException e)
{
LOGGER.error(e.getMessage()); // print the error message, omit the stack trace.
Thread.currentThread().interrupt(); // send interrupt to the thread.
shutdown(); // if we failed connecting and authenticating, then quit.
} catch (Exception e)
{
logger.log(e.getMessage()); // print the error message, omit the stack trace.
LOGGER.error(e.getMessage()); // print the error message, omit the stack trace.
shutdown(); // if we failed connecting and authenticating, then quit.
}
@@ -87,9 +112,10 @@ public class HidekoBot
// store if we have to force refresh commands despite no apparent changes.
boolean forceUpdateCommands = false;
// if there is more than 1 arg, then iterate through them because we have additional things to do.
// if there is at least one arg, then iterate through them because we have additional things to do.
// we are doing this at the end because we might need the API to be already initialized for some things.
if(args.length > 1) {
if (args.length > 0)
{
List<String> argsList = new ArrayList<>(Arrays.asList(args));
@@ -106,54 +132,68 @@ public class HidekoBot
}
boolean enableRandomSeedUpdaterTask = false;
// initialize random.org object if API key is provided
{
if (RandomUtil.isRandomOrgKeyValid())
{
LOGGER.info("Enabling Random.org integration... This might take a while!");
RandomUtil.initRandomOrg();
enableRandomSeedUpdaterTask = true;
LOGGER.info("Random.org integration enabled!");
}
}
// register slash commands and completers
SlashCommandListener slashCommandListener = new SlashCommandListener();
SlashCommandCompletionListener slashCommandCompletionListener = new SlashCommandCompletionListener();
AvatarCommand avatarCommand = new AvatarCommand();
ProfileImageCommandCompleter avatarCommandCompleter = new ProfileImageCommandCompleter(avatarCommand);
slashCommandListener.registerCommand(avatarCommand);
SlashCommandListener slashCommandListener = context.getBean(SlashCommandListener.class);
SlashCommandCompletionListener slashCommandCompletionListener = context.getBean(SlashCommandCompletionListener.class);
MessageCommandListener messageCommandListener = context.getBean(MessageCommandListener.class);
ButtonInteractionListener buttonInteractionListener = context.getBean(ButtonInteractionListener.class);
SelectMenuInteractionListener selectMenuInteractionListener = context.getBean(SelectMenuInteractionListener.class);
SlashAvatarCommand slashAvatarCommand = context.getBean(SlashAvatarCommand.class);
ProfileImageCommandCompleter avatarCommandCompleter = new ProfileImageCommandCompleter(slashAvatarCommand);
slashCommandListener.registerCommand(slashAvatarCommand);
slashCommandCompletionListener.registerCommandCompleter(avatarCommandCompleter);
slashCommandListener.registerCommand(new BanCommand());
BannerCommand bannerCommand = new BannerCommand();
ProfileImageCommandCompleter bannerCommandCompleter = new ProfileImageCommandCompleter(bannerCommand);
slashCommandListener.registerCommand(bannerCommand);
slashCommandListener.registerCommand(context.getBean(SlashBanCommand.class));
SlashBannerCommand slashBannerCommand = context.getBean(SlashBannerCommand.class);
ProfileImageCommandCompleter bannerCommandCompleter = new ProfileImageCommandCompleter(slashBannerCommand);
slashCommandListener.registerCommand(slashBannerCommand);
slashCommandCompletionListener.registerCommandCompleter(bannerCommandCompleter);
slashCommandListener.registerCommand(new BotInfoCommand());
slashCommandListener.registerCommand(new ClearCommand());
slashCommandListener.registerCommand(new CoinFlipCommand());
slashCommandListener.registerCommand(new DiceRollCommand());
slashCommandListener.registerCommand(new DieCommand());
slashCommandListener.registerCommand(new HelpCommand());
slashCommandListener.registerCommand(new InviteCommand());
slashCommandListener.registerCommand(new KickCommand());
slashCommandListener.registerCommand(new LoveCalculatorCommand());
slashCommandListener.registerCommand(new MagicBallCommand());
slashCommandListener.registerCommand(new PingCommand());
slashCommandListener.registerCommand(new SayCommand());
slashCommandListener.registerCommand(new TimeoutCommand());
slashCommandListener.registerCommand(new TriviaCommand());
slashCommandListener.registerCommand(new UrbanDictionaryCommand());
slashCommandListener.registerCommand(context.getBean(SlashBotInfoCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashClearCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashCoinFlipCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashDiceRollCommand.class));
slashCommandListener.registerCommand(new SlashDieCommand());
slashCommandListener.registerCommand(new SlashHelpCommand());
slashCommandListener.registerCommand(context.getBean(SlashInviteCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashKickCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashLoveCalculatorCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashMagicBallCommand.class));
slashCommandListener.registerCommand(new SlashPingCommand());
slashCommandListener.registerCommand(context.getBean(SlashSayCommand.class));
slashCommandListener.registerCommand(context.getBean(SlashTimeoutCommand.class));
slashCommandListener.registerCommand(new SlashTriviaCommand());
slashCommandListener.registerCommand(new SlashUrbanDictionaryCommand());
// register message commands
MessageCommandListener messageCommandListener = new MessageCommandListener();
messageCommandListener.registerCommand(new HelloCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.AliasCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.AvatarCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BanCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BannerCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.BotInfoCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.CoinFlipCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.ClearCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.DiceRollCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.HelpCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.InviteCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.KickCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.LoveCalculatorCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.MagicBallCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.SayCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.TimeoutCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.TriviaCommand());
messageCommandListener.registerCommand(new wtf.beatrice.hidekobot.commands.message.UrbanDictionaryCommand());
messageCommandListener.registerCommand(new MessageHelloCommand());
messageCommandListener.registerCommand(context.getBean(MessageAliasCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageAvatarCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageBanCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageBannerCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageBotInfoCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageCoinFlipCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageClearCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageDiceRollCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageHelpCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageInviteCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageKickCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageLoveCalculatorCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageMagicBallCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageSayCommand.class));
messageCommandListener.registerCommand(context.getBean(MessageTimeoutCommand.class));
messageCommandListener.registerCommand(new MessageTriviaCommand());
messageCommandListener.registerCommand(new MessageUrbanDictionaryCommand());
// register listeners
Cache.setSlashCommandListener(slashCommandListener);
@@ -162,59 +202,52 @@ public class HidekoBot
jda.addEventListener(messageCommandListener);
jda.addEventListener(slashCommandListener);
jda.addEventListener(slashCommandCompletionListener);
jda.addEventListener(new ButtonInteractionListener());
jda.addEventListener(new SelectMenuInteractionListener());
jda.addEventListener(buttonInteractionListener);
jda.addEventListener(selectMenuInteractionListener);
// update slash commands (delayed)
final boolean finalForceUpdateCommands = forceUpdateCommands;
Executors.newSingleThreadScheduledExecutor().schedule(() -> // todo: try-with-resources
CommandUtil.updateSlashCommands(finalForceUpdateCommands), 1, TimeUnit.SECONDS);
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.schedule(() -> commandService.updateSlashCommands(finalForceUpdateCommands),
1, TimeUnit.SECONDS);
}
// set the bot's status
jda.getPresence().setStatus(OnlineStatus.ONLINE);
// connect to database
logger.log("Connecting to database...");
String dbFilePath = Cache.getExecPath() + File.separator + "db.sqlite"; // in current directory
DatabaseSource databaseSource = new DatabaseSource(dbFilePath);
if(databaseSource.connect() && databaseSource.initDb())
{
logger.log("Database connection initialized!");
Cache.setDatabaseSourceInstance(databaseSource);
// load data here...
logger.log("Database data loaded into memory!");
} else {
logger.log("Error initializing database connection!");
}
// start scheduled runnables
ScheduledExecutorService scheduler = Cache.getTaskScheduler();
ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask();
scheduler.scheduleAtFixedRate(expiredMessageTask, 5, 5, TimeUnit.SECONDS); //every 5 seconds
ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask(services.databaseService(), services.commandService());
scheduler.scheduleAtFixedRate(expiredMessageTask, 5L, 5L, TimeUnit.SECONDS); //every 5 seconds
HeartBeatTask heartBeatTask = new HeartBeatTask();
scheduler.scheduleAtFixedRate(heartBeatTask, 10, 30, TimeUnit.SECONDS); //every 30 seconds
scheduler.scheduleAtFixedRate(heartBeatTask, 10L, 30L, TimeUnit.SECONDS); //every 30 seconds
StatusUpdateTask statusUpdateTask = new StatusUpdateTask();
scheduler.scheduleAtFixedRate(statusUpdateTask, 0, 60 * 5, TimeUnit.SECONDS); // every 5 minutes
RandomSeedTask randomSeedTask = new RandomSeedTask();
scheduler.scheduleAtFixedRate(randomSeedTask, 0, 60, TimeUnit.SECONDS); // every minute
scheduler.scheduleAtFixedRate(statusUpdateTask, 0L, 60L * 5L, TimeUnit.SECONDS); // every 5 minutes
if (enableRandomSeedUpdaterTask)
{
RandomOrgSeedTask randomSeedTask = new RandomOrgSeedTask();
scheduler.scheduleAtFixedRate(randomSeedTask, 15L, 15L, TimeUnit.MINUTES); // every 15 minutes
}
// register shutdown interrupt signal listener for proper shutdown.
Signal.handle(new Signal("INT"), signal -> shutdown());
Runtime.getRuntime().addShutdownHook(new Thread(HidekoBot::preShutdown));
// set startup time.
Cache.setStartupTime(LocalDateTime.now());
// print the bot logo.
logger.log("\n\n" + logger.getLogo() + "\nv" + Cache.getBotVersion() + " - bot is ready!\n", 2);
LOGGER.info("\n\n{}\nv{} - bot is ready!\n", FormatUtil.getLogo(), Cache.getBotVersion());
// log the invite-link to console so noob users can just click on it.
logger.log("Bot User ID: " + botUserId, 3);
logger.log("Invite Link: " + Cache.getInviteUrl(), 4);
LOGGER.info("Bot User ID: {}", botUserId);
LOGGER.info("Invite Link: {}", Cache.getInviteUrl());
}
public static JDA getAPI()
{
return jda;
@@ -222,9 +255,14 @@ public class HidekoBot
public static void shutdown()
{
logger.log("WARNING! Shutting down!");
if(jda != null) jda.shutdown();
preShutdown();
System.exit(0);
}
private static void preShutdown()
{
LOGGER.warn("WARNING! Shutting down!");
if (jda != null) jda.shutdown();
}
}
@@ -1,12 +1,15 @@
package wtf.beatrice.hidekobot.commands.base;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
@Component
public class Alias
{
public static String generateNiceAliases(MessageCommand command)
public String generateNiceAliases(MessageCommand command)
{
LinkedList<String> aliases = command.getCommandLabels();
StringBuilder aliasesStringBuilder = new StringBuilder();
@@ -2,17 +2,21 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.FormatUtil;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
import java.util.List;
@Component
public class BotInfo
{
public static MessageEmbed generateEmbed(List<String> commandLabels)
public MessageEmbed generateEmbed(List<String> commandLabels)
{
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
@@ -48,25 +52,38 @@ public class BotInfo
// keep track of how many total commands we have
int commandsCount = 0;
// message commands info fields
StringBuilder messageCommandsInfoBuilder = new StringBuilder();
// message commands info field
String messageCommandsInfo;
if (Cache.getMessageCommandListener() == null)
messageCommandsInfoBuilder.append("❌ disabled");
else {
messageCommandsInfoBuilder.append("✅ available");
messageCommandsInfo = "❌ disabled";
else
{
messageCommandsInfo = "✅ available";
commandsCount += Cache.getMessageCommandListener().getRegisteredCommands().size();
}
embedBuilder.addField("Message commands", messageCommandsInfoBuilder.toString(), true);
embedBuilder.addField("Message commands", messageCommandsInfo, true);
// slash commands info fields
StringBuilder slashCommandsInfoBuilder = new StringBuilder();
// slash commands info field
String slashCommandsInfo;
if (Cache.getMessageCommandListener() == null)
slashCommandsInfoBuilder.append("❌ disabled");
else {
slashCommandsInfoBuilder.append("✅ available");
slashCommandsInfo = "❌ disabled";
else
{
slashCommandsInfo = "✅ available";
commandsCount += Cache.getSlashCommandListener().getRegisteredCommands().size();
}
embedBuilder.addField("Slash commands", slashCommandsInfoBuilder.toString(), true);
embedBuilder.addField("Slash commands", slashCommandsInfo, true);
// random.org integration field
String randomOrgInfo;
if (RandomUtil.isRandomOrgKeyValid())
{
randomOrgInfo = "✅ connected";
} else
{
randomOrgInfo = "❌ disabled";
}
embedBuilder.addField("Random.org", randomOrgInfo, true);
// commands count fields
embedBuilder.addField("Total commands", "Loaded: `" + commandsCount + "`", true);
@@ -94,9 +111,10 @@ public class BotInfo
embedBuilder.addField("Uptime", FormatUtil.getNiceTimeDiff(Cache.getStartupTime()), true);
// issue tracker field
String link = "[Issue tracker](" + Cache.getRepositoryUrl() + "issues)";
embedBuilder.addField("Support",
"[Issue tracker](https://git.beatrice.wtf/mind-overflow/HidekoBot/issues)",
true); //todo: we should probably make this a final field in the config class
link, true);
// bot birthday field
embedBuilder.addField("Bot age",
@@ -8,42 +8,50 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class ClearChat
{
public static String getLabel() {
public String getLabel()
{
return "clear";
}
public static String getDescription() {
public String getDescription()
{
return "Clear the current channel's chat.";
}
public static Permission getPermission() {
public Permission getPermission()
{
return Permission.MESSAGE_MANAGE;
}
public static String checkDMs(Channel channel)
public String checkDMs(Channel channel)
{
if (!(channel instanceof TextChannel))
{ return "\uD83D\uDE22 Sorry! I can't delete messages here."; }
{
return "\uD83D\uDE22 Sorry! I can't delete messages here.";
}
return null;
}
public static String checkDeleteAmount(int toDeleteAmount)
public String checkDeleteAmount(int toDeleteAmount)
{
if (toDeleteAmount <= 0)
{ return "\uD83D\uDE22 Sorry, I can't delete that amount of messages!"; }
{
return "\uD83D\uDE22 Sorry, I can't delete that amount of messages!";
}
return null;
}
public static int delete(int toDeleteAmount,
public int delete(int toDeleteAmount,
long startingMessageId,
MessageChannel channel)
{
@@ -78,11 +86,10 @@ public class ClearChat
// set how many messages to delete for this iteration (usually <limit> unless there's a remainder)
int iterationSize = limit;
// if we are at the last iteration...
if(iteration+1 == iterations)
// if we are at the last iteration... check if we have <limit> or fewer messages to delete
if (iteration + 1 == iterations && remainder != 0)
{
// check if we have <limit> or fewer messages to delete
if(remainder != 0) iterationSize = remainder;
iterationSize = remainder;
}
if (iterationSize == 1)
@@ -94,7 +101,8 @@ public class ClearChat
else outOfBounds = true;
// increase deleted counter by 1
deleted++;
} else {
} else
{
// get the last <iterationSize - 1> messages.
MessageHistory.MessageRetrieveAction action = channel.getHistoryBefore(messageId, iterationSize - 1);
// note: first one is the most recent, last one is the oldest message.
@@ -107,25 +115,26 @@ public class ClearChat
if (messages.size() <= 1)
{
outOfBounds = true;
} else {
} else
{
// before deleting, we need to grab the <previous to the oldest> message's id for next iteration.
action = channel.getHistoryBefore(messages.get(messages.size() - 1).getIdLong(), 1);
action = channel.getHistoryBefore(messages.getLast().getIdLong(), 1);
List<Message> previousMessage = action.complete().getRetrievedHistory();
// if that message exists (we are not out of bounds)... store it
if(!previousMessage.isEmpty()) messageId = previousMessage.get(0).getIdLong();
if (!previousMessage.isEmpty()) messageId = previousMessage.getFirst().getIdLong();
else outOfBounds = true;
}
// queue messages for deletion
if (messages.size() == 1)
{
messages.get(0).delete().queue();
}
else if(!messages.isEmpty())
messages.getFirst().delete().queue();
} else if (!messages.isEmpty())
{
try
{
try {
((TextChannel) channel).deleteMessages(messages).complete();
/* alternatively, we could use purgeMessages, which is smarter...
however, it also tries to delete messages older than 2 weeks
@@ -146,13 +155,13 @@ public class ClearChat
return deleted;
}
public static Button getDismissButton()
public Button getDismissButton()
{
return Button.primary("generic_dismiss", "Dismiss")
.withEmoji(Emoji.fromUnicode(""));
}
public static String parseAmount(int deleted)
public String parseAmount(int deleted)
{
if (deleted < 1)
@@ -161,12 +170,16 @@ public class ClearChat
} else if (deleted == 1)
{
return "✂ Cleared 1 message!";
} else {
} else
{
return "✂ Cleared " + deleted + " messages!";
}
}
// cap the amount to avoid abuse.
public static int getMaxAmount() { return 1000; }
public int getMaxAmount()
{
return 1000;
}
}
@@ -6,21 +6,23 @@ import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.ArrayList;
import java.util.List;
@Component
public class CoinFlip
{
public static Button getReflipButton() {
public Button getReflipButton()
{
return Button.primary("coinflip_reflip", "Flip again")
.withEmoji(Emoji.fromUnicode("\uD83E\uDE99"));
}
public static String genRandom()
public String genRandom()
{
int rand = RandomUtil.getRandomNumber(0, 1);
String msg;
@@ -28,40 +30,61 @@ public class CoinFlip
if (rand == 1)
{
msg = ":coin: It's **Heads**!";
} else {
} else
{
msg = "It's **Tails**! :coin:";
}
return msg;
}
public static void buttonReFlip(ButtonInteractionEvent event)
public void buttonReFlip(ButtonInteractionEvent event)
{
// check if the user interacting is the same one who ran the command
if(!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
// Ack ASAP to avoid 3s timeout
event.deferEdit().queue(hook -> {
// Permission check **after** ack
if (!Cache.getServices().databaseService().isUserTrackedFor(event.getUser().getId(), event.getMessageId()))
{
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
hook.sendMessage("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// set old message's button as disabled
List<ActionRow> actionRows = event.getMessage().getActionRows();
actionRows.set(0, actionRows.get(0).asDisabled());
event.editComponents(actionRows).queue();
// Disable all components on the original message
List<ActionRow> oldRows = event.getMessage().getActionRows();
List<ActionRow> disabledRows = new ArrayList<>(oldRows.size());
for (ActionRow row : oldRows)
{
disabledRows.add(row.asDisabled());
}
hook.editOriginalComponents(disabledRows).queue();
// perform coin flip
event.getHook().sendMessage(genRandom())
// Send a follow-up with a fresh button
hook.sendMessage(genRandom())
.addActionRow(getReflipButton())
.queue((message) ->
.queue(msg -> trackAndRestrict(msg, event.getUser()), err -> {
});
}, failure -> {
// Rare: if we couldn't ack, try best-effort fallbacks
try
{
List<ActionRow> oldRows = event.getMessage().getActionRows();
List<ActionRow> disabledRows = new ArrayList<>(oldRows.size());
for (ActionRow row : oldRows) disabledRows.add(row.asDisabled());
event.getMessage().editMessageComponents(disabledRows).queue();
} catch (Exception ignored)
{
// set the command as expiring and restrict it to the user who ran it
trackAndRestrict(message, event.getUser());
}, (error) -> {});
}
public static void trackAndRestrict(Message replyMessage, User user)
event.getChannel().sendMessage(genRandom())
.addActionRow(getReflipButton())
.queue(msg -> trackAndRestrict(msg, event.getUser()), err -> {
});
});
}
public void trackAndRestrict(Message replyMessage, User user)
{
Cache.getDatabaseSource().queueDisabling(replyMessage);
Cache.getDatabaseSource().trackRanCommandReply(replyMessage, user);
Cache.getServices().databaseService().queueDisabling(replyMessage);
Cache.getServices().databaseService().trackRanCommandReply(replyMessage, user);
}
}
@@ -2,21 +2,26 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.fun.Dice;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.UUID;
@Component
public class DiceRoll
{
public static MessageResponse buildResponse(User author, String[] args)
public MessageResponse buildResponse(User author, String[] args)
{
LinkedHashMap<Dice, Integer> dicesToRoll = new LinkedHashMap<>();
String diceRegex = "d[0-9]+";
String amountRegex = "[0-9]+";
String diceRegex = "d\\d+";
String amountRegex = "\\d+";
Dice currentDice = null;
int currentAmount;
@@ -37,7 +42,8 @@ public class DiceRoll
if (currentDice == null)
{
currentDice = new Dice(6);
} else {
} else
{
currentDice = new Dice(currentDice);
}
@@ -49,8 +55,7 @@ public class DiceRoll
lastPushedDice = currentDice.getUUID();
dicesToRoll.put(currentDice, currentAmount);
totalRolls += currentAmount;
}
else if(arg.matches(diceRegex))
} else if (arg.matches(diceRegex))
{
int sides = Integer.parseInt(arg.substring(1));
@@ -106,9 +111,12 @@ public class DiceRoll
totalRolls = 1;
}
for(Dice dice : dicesToRoll.keySet())
for (Map.Entry<Dice, Integer> entry : dicesToRoll.entrySet())
{
for(int roll = 0; roll < dicesToRoll.get(dice); roll++)
Dice dice = entry.getKey();
Integer rollsToMake = entry.getValue();
for (int roll = 0; roll < rollsToMake; roll++)
{
dice.roll();
rolledDices.add(new Dice(dice));
@@ -118,20 +126,26 @@ public class DiceRoll
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setAuthor(author.getName(), null, author.getAvatarUrl());
embedBuilder.setTitle("Dice Roll");
if (RandomUtil.isRandomOrgKeyValid())
embedBuilder.setFooter("Seed provided by random.org");
StringBuilder message = new StringBuilder();
int total = 0;
int previousDiceSides = 0;
for (Dice dice : rolledDices) {
for (Dice dice : rolledDices)
{
int diceSize = dice.getSides();
if (previousDiceSides != diceSize) {
if (previousDiceSides != diceSize)
{
message.append("\nd").append(diceSize).append(": ");
previousDiceSides = diceSize;
} else if (previousDiceSides != 0) {
} else if (previousDiceSides != 0)
{
message.append(", ");
}
@@ -4,13 +4,15 @@ import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
@Component
public class Invite
{
public static MessageEmbed generateEmbed()
public MessageEmbed generateEmbed()
{
EmbedBuilder embedBuilder = new EmbedBuilder();
@@ -28,7 +30,7 @@ public class Invite
return embedBuilder.build();
}
public static Button getInviteButton()
public Button getInviteButton()
{
String inviteUrl = Cache.getInviteUrl();
return Button.link(inviteUrl, "Invite " + Cache.getBotName())
@@ -3,14 +3,17 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.concurrent.TimeUnit;
@Component
public class LoveCalculator
{
public static MessageEmbed buildEmbedAndCacheResult(User author, User user1, User user2)
public MessageEmbed buildEmbedAndCacheResult(User author, User user1, User user2)
{
String userId1 = user1.getId();
String userId2 = user2.getId();
@@ -32,7 +35,7 @@ public class LoveCalculator
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setAuthor(author.getName(), null, author.getAvatarUrl());
embedBuilder.setTitle("Love Calculator");
embedBuilder.addField("\uD83D\uDC65 People",
@@ -3,6 +3,7 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
@@ -11,15 +12,16 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@Component
public class MagicBall
{
public static LinkedList<String> getLabels()
public LinkedList<String> getLabels()
{
return new LinkedList<>(Arrays.asList("8ball", "8b", "eightball", "magicball"));
}
private final static List<String> answers = new ArrayList<>(
private final List<String> answers = new ArrayList<>(
Arrays.asList("It is certain.",
"It is decidedly so.",
"Without a doubt.",
@@ -41,13 +43,13 @@ public class MagicBall
"Outlook not so good.",
"Very doubtful."));
public static String getRandomAnswer()
public String getRandomAnswer()
{
int answerPos = RandomUtil.getRandomNumber(0, answers.size() - 1);
return answers.get(answerPos);
}
public static MessageEmbed generateEmbed(String question, User author)
public MessageEmbed generateEmbed(String question, User author)
{
// add a question mark at the end, if missing.
// this might not always apply but it's fun
@@ -56,7 +58,7 @@ public class MagicBall
String answer = getRandomAnswer();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setAuthor(author.getName(), null, author.getAvatarUrl());
embedBuilder.setTitle("Magic Ball");
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.addField("❓ Question", question, false);
@@ -3,21 +3,26 @@ package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.utils.ImageProxy;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
@Component
public class ProfileImage
{
public static int parseResolution(int resolution)
public int parseResolution(int resolution)
{
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
// method to find closest value to accepted values
int distance = Math.abs(acceptedSizes[0] - resolution);
int idx = 0;
for(int c = 1; c < acceptedSizes.length; c++){
for (int c = 1; c < acceptedSizes.length; c++)
{
int cdistance = Math.abs(acceptedSizes[c] - resolution);
if(cdistance < distance){
if (cdistance < distance)
{
idx = c;
distance = cdistance;
}
@@ -26,7 +31,7 @@ public class ProfileImage
return acceptedSizes[idx];
}
public static MessageResponse buildResponse(int resolution, User user, ImageType imageType)
public MessageResponse buildResponse(int resolution, User user, ImageType imageType)
{
String imageTypeName = imageType.name().toLowerCase();
String resolutionString;
@@ -39,7 +44,8 @@ public class ProfileImage
{
resolutionString = resolution + " × " + resolution;
imageLink = user.getEffectiveAvatar().getUrl(resolution);
} else {
} else
{
int verticalRes = 361 * resolution / 1024;
resolutionString = resolution + " × " + verticalRes;
if (bannerProxy != null)
@@ -67,7 +73,8 @@ public class ProfileImage
if (imageType == ImageType.AVATAR)
{
currLink = user.getEffectiveAvatar().getUrl(currSize);
} else {
} else
{
if (bannerProxy == null) break;
currLink = bannerProxy.getUrl(currSize);
}
@@ -82,15 +89,16 @@ public class ProfileImage
embedBuilder.addField("Available resolutions", links.toString(), false);
if (imageLink != null)
embedBuilder.setImage(imageLink);
if(imageLink == null) {
if (imageLink == null)
{
String error = "I couldn't find " + user.getAsMention() + "'s " + imageTypeName + "!";
return new MessageResponse(error, null);
} else {
} else
{
return new MessageResponse(null, embedBuilder.build());
}
}
@@ -1,11 +1,13 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.Permission;
import org.springframework.stereotype.Component;
@Component
public class Say
{
public static Permission getPermission() {
public Permission getPermission()
{
return Permission.MESSAGE_MANAGE;
}
}
@@ -11,6 +11,7 @@ import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import org.apache.commons.text.StringEscapeUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.comparators.TriviaCategoryComparator;
@@ -18,7 +19,6 @@ import wtf.beatrice.hidekobot.objects.fun.TriviaCategory;
import wtf.beatrice.hidekobot.objects.fun.TriviaQuestion;
import wtf.beatrice.hidekobot.objects.fun.TriviaScore;
import wtf.beatrice.hidekobot.runnables.TriviaTask;
import wtf.beatrice.hidekobot.util.CommandUtil;
import java.io.BufferedReader;
import java.io.IOException;
@@ -26,42 +26,63 @@ import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class Trivia
{
private final static String triviaLink = "https://opentdb.com/api.php?amount=10&type=multiple&category=";
private final static String categoriesLink = "https://opentdb.com/api_category.php";
public static List<String> channelsRunningTrivia = new ArrayList<>();
private Trivia()
{
throw new IllegalStateException("Utility class");
}
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Trivia.class);
private static final String TRIVIA_API_LINK = "https://opentdb.com/api.php?amount=10&type=multiple&category=";
private static final String TRIVIA_API_CATEGORIES_LINK = "https://opentdb.com/api_category.php";
public static List<String> channelsRunningTrivia = Collections.synchronizedList(new ArrayList<>());
// first string is the channelId, the list contain all users who responded there
public static HashMap<String, List<String>> channelAndWhoResponded = new HashMap<>();
public static ConcurrentHashMap<String, List<String>> channelAndWhoResponded = new ConcurrentHashMap<>();
// first string is the channelId, the list contain all score records for that channel
public static HashMap<String, LinkedList<TriviaScore>> channelAndScores = new HashMap<>();
public static ConcurrentHashMap<String, LinkedList<TriviaScore>> channelAndScores = new ConcurrentHashMap<>();
public static String getTriviaLink(int categoryId) {return triviaLink + categoryId; }
public static String getCategoriesLink() {return categoriesLink; }
public static String getTriviaLink(int categoryId)
{
return TRIVIA_API_LINK + categoryId;
}
public static String getNoDMsError() {
public static String getCategoriesLink()
{
return TRIVIA_API_CATEGORIES_LINK;
}
public static String getNoDMsError()
{
return "\uD83D\uDE22 Sorry! Trivia doesn't work in DMs.";
}
public static String getTriviaAlreadyRunningError() {
public static String getTriviaAlreadyRunningError()
{
// todo nicer looking
return "Trivia is already running here!";
}
public static MessageResponse generateMainScreen()
{
// todo null checks
JSONObject categoriesJson = Trivia.fetchJson(Trivia.getCategoriesLink());
if (categoriesJson == null)
return new MessageResponse("Error fetching trivia!", null); // todo nicer with emojis
List<TriviaCategory> categories = Trivia.parseCategories(categoriesJson);
if (categories.isEmpty())
return new MessageResponse("Error parsing trivia categories!", null); // todo nicer with emojis
categories.sort(new TriviaCategoryComparator());
EmbedBuilder embedBuilder = new EmbedBuilder();
@@ -91,7 +112,8 @@ public class Trivia
public static JSONObject fetchJson(String link)
{
try {
try
{
URL url = new URL(link);
URLConnection connection = url.openConnection();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
@@ -104,8 +126,9 @@ public class Trivia
}
bufferedReader.close();
return new JSONObject(jsonStrBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (IOException e)
{
LOGGER.error("JSON Parsing Exception", e);
}
return null;
@@ -158,6 +181,8 @@ public class Trivia
public static void handleAnswer(ButtonInteractionEvent event, AnswerType answerType)
{
// Ack immediately with an ephemeral deferral to avoid 3s timeout
event.deferReply(true).queue(hook -> {
User user = event.getUser();
String channelId = event.getChannel().getId();
@@ -165,6 +190,7 @@ public class Trivia
{
LinkedList<TriviaScore> scores = channelAndScores.get(channelId);
if (scores == null) scores = new LinkedList<>();
TriviaScore currentUserScore = null;
for (TriviaScore score : scores)
{
@@ -183,26 +209,35 @@ public class Trivia
if (answerType.equals(AnswerType.CORRECT))
{
event.reply(user.getAsMention() + " got it right! \uD83E\uDD73 (**+3**)").queue();
// Public message in channel
event.getChannel().sendMessage(user.getAsMention() + " got it right! \uD83E\uDD73 (**+3**)").queue();
currentUserScore.changeScore(3);
} else {
event.reply("" + user.getAsMention() + ", that's not the right answer! (**-1**)").queue();
} else
{
event.getChannel().sendMessage("" + user.getAsMention() + ", that's not the right answer! (**-1**)").queue();
currentUserScore.changeScore(-1);
}
scores.add(currentUserScore);
channelAndScores.put(channelId, scores);
} else {
event.reply("☹️ " + user.getAsMention() + ", you can't answer twice!")
.queue(interaction ->
Cache.getTaskScheduler().schedule(() ->
interaction.deleteOriginal().queue(), 3, TimeUnit.SECONDS));
}
} else
{
// Show the warning **in the original ephemeral message**, then delete it after 5s.
hook.editOriginal("☹️ " + user.getAsMention() + ", you can't answer twice!").queue(v ->
hook.deleteOriginal().queueAfter(3, TimeUnit.SECONDS, null, __ -> {
})
);
return; // don't run the generic cleanup below; we want the message visible for ~5s
}
private static boolean trackResponse(User user, MessageChannel channel)
// Clean up the ephemeral deferral (no visible ephemeral message left) for the normal path
hook.deleteOriginal().queue(null, __ -> {
});
}, __ -> {
});
}
private static synchronized boolean trackResponse(User user, MessageChannel channel)
{
String userId = user.getId();
String channelId = channel.getId();
@@ -219,22 +254,25 @@ public class Trivia
responders.add(userId);
channelAndWhoResponded.put(channelId, responders);
return true; // response was successfully tracked
} else {
} else
{
return false; // response wasn't tracked because there already was an entry
}
}
public static void handleMenuSelection(StringSelectInteractionEvent event)
{
// Ack immediately (ephemeral) so we can safely do DB/work
event.deferReply(true).queue(hook -> {
// check if the user interacting is the same one who ran the command
if(!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
if (!(Cache.getServices().databaseService().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
{
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
hook.sendMessage("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// todo: we shouldn't use this method, since it messes with the database... look at coin reflip
CommandUtil.disableExpired(event.getMessageId());
// Disable buttons on the original message via service (uses separate REST calls)
Cache.getServices().commandService().disableExpired(event.getMessageId());
SelectOption pickedOption = event.getInteraction().getSelectedOptions().get(0);
String categoryName = pickedOption.getLabel();
@@ -244,6 +282,12 @@ public class Trivia
TriviaCategory category = new TriviaCategory(categoryName, categoryId);
startTrivia(event, category);
// remove the ephemeral deferral to keep things clean
hook.deleteOriginal().queue(null, __ -> {
});
}, __ -> {
});
}
public static void startTrivia(StringSelectInteractionEvent event, TriviaCategory category)
@@ -254,19 +298,19 @@ public class Trivia
if (Trivia.channelsRunningTrivia.contains(channel.getId()))
{
// todo nicer looking
// todo: also what if the bot stops (database...?)
// todo: also what if the message is already deleted
Message err = event.reply("Trivia is already running here!").complete().retrieveOriginal().complete();
Cache.getTaskScheduler().schedule(() -> err.delete().queue(), 10, TimeUnit.SECONDS);
// Already running: inform ephemerally via hook (the interaction was deferred in the caller)
event.getHook().sendMessage(Trivia.getTriviaAlreadyRunningError())
.setEphemeral(true)
.queue(msg -> Cache.getTaskScheduler().schedule(() -> msg.delete().queue(), 10, TimeUnit.SECONDS));
return;
} else {
// todo nicer looking
event.reply("Starting new Trivia session!").queue();
} else
{
// Public info that a new session is starting
channel.sendMessage("Starting new Trivia session!").queue();
}
TriviaTask triviaTask = new TriviaTask(author, channel, category);
TriviaTask triviaTask = new TriviaTask(author, channel, category,
Cache.getServices().databaseService(), Cache.getServices().commandService());
ScheduledFuture<?> future =
Cache.getTaskScheduler().scheduleAtFixedRate(triviaTask,
0,
@@ -277,7 +321,8 @@ public class Trivia
Trivia.channelsRunningTrivia.add(channel.getId());
}
public enum AnswerType {
public enum AnswerType
{
CORRECT, WRONG
}
@@ -14,7 +14,7 @@ import org.apache.commons.text.WordUtils;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.services.DatabaseService;
import wtf.beatrice.hidekobot.util.SerializationUtil;
import java.util.ArrayList;
@@ -25,11 +25,19 @@ import java.util.List;
public class UrbanDictionary
{
private UrbanDictionary()
{
throw new IllegalStateException("Utility class");
}
public static LinkedList<String> getCommandLabels()
{ return new LinkedList<>(Arrays.asList("urban", "urbandictionary", "ud")); }
{
return new LinkedList<>(Arrays.asList("urban", "urbandictionary", "ud"));
}
public static String getBaseUrl() {
public static String getBaseUrl()
{
return "https://www.urbandictionary.com/define.php?term=";
}
@@ -51,7 +59,8 @@ public class UrbanDictionary
.withEmoji(Emoji.fromFormatted("\uD83D\uDDD1"));
}
public static String getNoArgsError() {
public static String getNoArgsError()
{
return "\uD83D\uDE22 I need to know what to search for!";
}
@@ -79,7 +88,7 @@ public class UrbanDictionary
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle(term + ", on Urban Dictionary", url);
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setAuthor(author.getName(), null, author.getAvatarUrl());
embedBuilder.addField("\uD83D\uDCD6 Definition", search.getPlaintextMeanings().get(page), false);
embedBuilder.addField("\uD83D\uDCAD Example", search.getPlaintextExamples().get(page), false);
embedBuilder.addField("\uD83D\uDCCC Submission",
@@ -98,9 +107,9 @@ public class UrbanDictionary
public static void track(Message message, User user, UrbanSearch search, String sanitizedTerm)
{
Cache.getDatabaseSource().queueDisabling(message);
Cache.getDatabaseSource().trackRanCommandReply(message, user);
Cache.getDatabaseSource().trackUrban(search.getSerializedMeanings(),
Cache.getServices().databaseService().queueDisabling(message);
Cache.getServices().databaseService().trackRanCommandReply(message, user);
Cache.getServices().databaseService().trackUrban(search.getSerializedMeanings(),
search.getSerializedExamples(),
search.getSerializedContributors(),
search.getSerializedDates(),
@@ -110,17 +119,19 @@ public class UrbanDictionary
public static void changePage(ButtonInteractionEvent event, ChangeType changeType)
{
event.deferEdit().queue();
String messageId = event.getMessageId();
DatabaseSource database = Cache.getDatabaseSource();
DatabaseService database = Cache.getServices().databaseService();
// check if the user interacting is the same one who ran the command
if (!(database.isUserTrackedFor(event.getUser().getId(), messageId))) {
if (!(database.isUserTrackedFor(event.getUser().getId(), messageId)))
{
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// get current page and calculate how many pages there are
int page = Cache.getDatabaseSource().getUrbanPage(messageId);
int page = database.getUrbanPage(messageId);
String term = database.getUrbanTerm(messageId);
String url = generateUrl(term);
@@ -152,14 +163,16 @@ public class UrbanDictionary
if (page > 0)
{
components.add(UrbanDictionary.getPreviousPageButton().asEnabled());
} else {
} else
{
components.add(UrbanDictionary.getPreviousPageButton().asDisabled());
}
if (page + 1 == search.getPages())
{
components.add(UrbanDictionary.getNextPageButton().asDisabled());
} else {
} else
{
components.add(UrbanDictionary.getNextPageButton().asEnabled());
}
@@ -168,7 +181,9 @@ public class UrbanDictionary
ActionRow currentRow = ActionRow.of(components);
// update the message
event.editComponents(currentRow).setEmbeds(updatedEmbed).queue();
event.getHook().editOriginalEmbeds(updatedEmbed)
.setComponents(currentRow)
.queue();
database.setUrbanPage(messageId, page);
database.resetExpiryTimestamp(messageId);
}
@@ -263,10 +278,10 @@ public class UrbanDictionary
htmlContributor.indexOf("</a>") + 4);
contributorsNames.add(htmlContributorName
.replaceAll("<.*?>", "")); // remove all html tags;
.replaceAll("<.*?>", "")); // remove all html tags
submissionDates.add(htmlSubmitDate
.replaceAll("<.*?>", "")); // remove all html tags;
.replaceAll("<.*?>", "")); // remove all html tags
}
}
@@ -278,39 +293,48 @@ public class UrbanDictionary
pages = submissionDates.size();
}
public List<String> getPlaintextMeanings() {
public List<String> getPlaintextMeanings()
{
return this.plaintextMeanings;
}
public List<String> getPlaintextExamples() {
public List<String> getPlaintextExamples()
{
return this.plaintextExamples;
}
public List<String> getContributorsNames() {
public List<String> getContributorsNames()
{
return this.contributorsNames;
}
public List<String> getSubmissionDates() {
public List<String> getSubmissionDates()
{
return this.submissionDates;
}
public String getSerializedMeanings() {
public String getSerializedMeanings()
{
return serializedMeanings;
}
public String getSerializedExamples() {
public String getSerializedExamples()
{
return serializedExamples;
}
public String getSerializedContributors() {
public String getSerializedContributors()
{
return serializedContributors;
}
public String getSerializedDates() {
public String getSerializedDates()
{
return serializedDates;
}
public int getPages() {
public int getPages()
{
return pages;
}
}
@@ -12,6 +12,7 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.objects.MessageResponse;
@@ -20,18 +21,18 @@ import wtf.beatrice.hidekobot.util.FormatUtil;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class UserPunishment
{
private final static Duration maxTimeoutDuration = Duration.of(28, ChronoUnit.DAYS);
private final static Duration minTimeoutDuration = Duration.of(30, ChronoUnit.SECONDS);
private static final Duration maxTimeoutDuration = Duration.of(28, ChronoUnit.DAYS);
private static final Duration minTimeoutDuration = Duration.of(30, ChronoUnit.SECONDS);
public static void handle(SlashCommandInteractionEvent event, PunishmentType punishmentType)
public void handle(SlashCommandInteractionEvent event, PunishmentType punishmentType)
{
// this might take a sec
event.deferReply().queue();
@@ -88,7 +89,7 @@ public class UserPunishment
event.getHook().editOriginal(response.content()).queue();
}
public static void handle(MessageReceivedEvent event, String[] args, PunishmentType punishmentType)
public void handle(MessageReceivedEvent event, String[] args, PunishmentType punishmentType)
{
Mentions msgMentions = event.getMessage().getMentions();
List<IMentionable> mentions = msgMentions.getMentions();
@@ -105,7 +106,7 @@ public class UserPunishment
event.getMessage().reply(response.content()).queue();
}
public static MessageResponse getResponse(User author,
public MessageResponse getResponse(User author,
PunishmentType punishmentType,
MessageChannelUnion channel,
List<IMentionable> mentions,
@@ -129,7 +130,8 @@ public class UserPunishment
String mentionedId = mentions.get(0).getId();
User mentioned = null;
try {
try
{
mentioned = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
} catch (RuntimeException ignored)
{
@@ -167,17 +169,24 @@ public class UserPunishment
Duration duration = null;
AuditableRestAction<Void> punishmentAction = null;
boolean impossible = false;
try {
switch (punishmentType) {
try
{
switch (punishmentType)
{
case BAN -> punishmentAction = guild.ban(mentioned, 0, TimeUnit.SECONDS);
case KICK -> punishmentAction = guild.kick(mentioned);
case TIMEOUT -> {
if(args != null)
case TIMEOUT ->
{
// Ensure a duration argument is provided at index 1 (after mention/user)
if (args == null || args.length <= 1)
{
return new MessageResponse("Please specify a punishment duration!", null);
}
String durationStr = args[1];
duration = FormatUtil.parseDuration(durationStr);
}
boolean isDurationValid = true;
@@ -188,7 +197,7 @@ public class UserPunishment
if (minTimeoutDuration.compareTo(duration) > 0) isDurationValid = false;
}
if(duration == null || !isDurationValid)
if (!isDurationValid)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, but the specified duration is invalid!", null);
@@ -197,16 +206,26 @@ public class UserPunishment
punishmentAction = guild.timeoutFor(mentioned, duration);
}
}
} catch (RuntimeException ignored) {
} catch (RuntimeException ignored)
{
impossible = true;
}
if (punishmentAction == null)
impossible = true;
if (impossible)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, I couldn't " + punishmentTypeName + " " + mentioned.getAsMention() + "!",
null);
}
if (!reason.isEmpty() && !reasonBuilder.isEmpty())
punishmentAction.reason("[" + author.getAsTag() + "] " + reason);
punishmentAction.reason("[" + author.getName() + "] " + reason);
try {
try
{
punishmentAction.complete();
} catch (RuntimeException ignored)
{
@@ -217,7 +236,7 @@ public class UserPunishment
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setAuthor(author.getName(), null, author.getAvatarUrl());
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("User " + punishmentType.getPastTense());
@@ -236,7 +255,8 @@ public class UserPunishment
}
public enum PunishmentType {
public enum PunishmentType
{
KICK("kicked"),
BAN("banned"),
TIMEOUT("timed out"),
@@ -13,12 +13,14 @@ import java.util.List;
public class ProfileImageCommandCompleter extends SlashArgumentsCompleterImpl
{
public ProfileImageCommandCompleter(SlashCommand parentCommand) {
public ProfileImageCommandCompleter(SlashCommand parentCommand)
{
super(parentCommand);
}
@Override
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event) {
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event)
{
if (event.getFocusedOption().getName().equals("size"))
{
@@ -4,6 +4,8 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Alias;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
@@ -13,40 +15,54 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class AliasCommand implements MessageCommand
@Component
public class MessageAliasCommand implements MessageCommand
{
private final Alias alias;
public MessageAliasCommand(@Autowired Alias alias)
{
this.alias = alias;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("alias", "aliases"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "See other command aliases.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<command>";
}
@@ -67,7 +83,7 @@ public class AliasCommand implements MessageCommand
return;
}
String aliases = Alias.generateNiceAliases(command);
String aliases = alias.generateNiceAliases(command);
aliases = "Aliases for **" + command.getCommandLabels().get(0) + "**: " + aliases;
event.getMessage()
@@ -6,7 +6,8 @@ import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
@@ -17,48 +18,59 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class AvatarCommand implements MessageCommand
@Component
public class MessageAvatarCommand implements MessageCommand
{
private final ProfileImage profileImage;
public MessageAvatarCommand(@Autowired ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("avatar"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Get someone's avatar, or your own. You can additionally specify a resolution.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "[mentioned user] [resolution]";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
User user;
int resolution = -1;
@@ -66,18 +78,22 @@ public class AvatarCommand implements MessageCommand
// (mentions are handled differently by a specific method)
boolean resFound = false;
for (String arg : args) {
try {
for (String arg : args)
{
try
{
int givenRes = Integer.parseInt(arg);
resolution = ProfileImage.parseResolution(givenRes);
resolution = profileImage.parseResolution(givenRes);
resFound = true;
break;
} catch (NumberFormatException ignored) {
} catch (NumberFormatException ignored)
{
// ignored because we're running a check after this block
}
}
// fallback in case we didn't find any specified resolution
if(!resFound) resolution = ProfileImage.parseResolution(512);
if (!resFound) resolution = profileImage.parseResolution(512);
// check if someone is mentioned
Mentions mentions = event.getMessage().getMentions();
@@ -94,7 +110,7 @@ public class AvatarCommand implements MessageCommand
if (user == null) user = event.getAuthor();
// send a response
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
MessageResponse response = profileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if (response.content() != null)
{
event.getMessage().reply(response.content()).queue();
@@ -1,19 +1,12 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -21,48 +14,60 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class BanCommand implements MessageCommand
@Component
public class MessageBanCommand implements MessageCommand
{
private final UserPunishment userPunishment;
public MessageBanCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("ban"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return new ArrayList<Permission>(Collections.singletonList(Permission.BAN_MEMBERS));
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Ban the mentioned user.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<mentioned user> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.BAN);
userPunishment.handle(event, args, UserPunishment.PunishmentType.BAN);
}
}
@@ -6,6 +6,8 @@ import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
@@ -16,40 +18,53 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BannerCommand implements MessageCommand
@Component
public class MessageBannerCommand implements MessageCommand
{
private final ProfileImage profileImage;
public MessageBannerCommand(@Autowired ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("banner"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Get someone's profile banner, or your own.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "[mentioned user] [resolution]";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@@ -63,18 +78,22 @@ public class BannerCommand implements MessageCommand
// (mentions are handled differently by a specific method)
boolean resFound = false;
for (String arg : args) {
try {
for (String arg : args)
{
try
{
int givenRes = Integer.parseInt(arg);
resolution = ProfileImage.parseResolution(givenRes);
resolution = profileImage.parseResolution(givenRes);
resFound = true;
break;
} catch (NumberFormatException ignored) {
} catch (NumberFormatException ignored)
{
// ignored because we're running a check after this block
}
}
// fallback in case we didn't find any specified resolution
if(!resFound) resolution = ProfileImage.parseResolution(512);
if (!resFound) resolution = profileImage.parseResolution(512);
// check if someone is mentioned
Mentions mentions = event.getMessage().getMentions();
@@ -91,7 +110,7 @@ public class BannerCommand implements MessageCommand
if (user == null) user = event.getAuthor();
// send a response
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
MessageResponse response = profileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
if (response.content() != null)
{
event.getMessage().reply(response.content()).queue();
@@ -5,65 +5,82 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BotInfoCommand implements MessageCommand
@Component
public class MessageBotInfoCommand implements MessageCommand
{
private final BotInfo botInfo;
public MessageBotInfoCommand(@Autowired BotInfo botInfo)
{
this.botInfo = botInfo;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("botinfo", "info"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Get general info about the bot.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) {
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
// get a list of message commands
LinkedList<MessageCommand> messageCommands = Cache.getMessageCommandListener().getRegisteredCommands();
LinkedList<String> commandNames = new LinkedList<>();
for (MessageCommand command : messageCommands) {
for (MessageCommand command : messageCommands)
{
commandNames.add(command.getCommandLabels().get(0));
}
// send the list
MessageEmbed embed = BotInfo.generateEmbed(commandNames);
MessageEmbed embed = botInfo.generateEmbed(commandNames);
event.getMessage().replyEmbeds(embed).queue();
}
}
@@ -6,6 +6,8 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
@@ -15,48 +17,63 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class ClearCommand implements MessageCommand
@Component
public class MessageClearCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList(ClearChat.getLabel()));
private final ClearChat clearChat;
public MessageClearCommand(@Autowired ClearChat clearChat)
{
this.clearChat = clearChat;
}
@Override
public List<Permission> getPermissions() { return Collections.singletonList(ClearChat.getPermission()); }
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList(clearChat.getLabel()));
}
@Override
public boolean passRawArgs() {
public List<Permission> getPermissions()
{
return Collections.singletonList(clearChat.getPermission());
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Clear the current channel's chat history.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "[amount]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String senderId = event.getMessage().getAuthor().getId();
// check if user is trying to run command in dms.
String error = ClearChat.checkDMs(event.getChannel());
if (error != null) {
String error = clearChat.checkDMs(event.getChannel());
if (error != null)
{
event.getMessage().reply(error).queue();
return;
}
@@ -66,7 +83,8 @@ public class ClearCommand implements MessageCommand
if (args.length == 0) toDeleteAmount = 1;
else
{
try {
try
{
toDeleteAmount = Integer.parseInt(args[0]);
} catch (NumberFormatException e)
{
@@ -75,10 +93,11 @@ public class ClearCommand implements MessageCommand
}
// cap the amount to avoid abuse.
if(toDeleteAmount > ClearChat.getMaxAmount()) toDeleteAmount = 0;
if (toDeleteAmount > clearChat.getMaxAmount()) toDeleteAmount = 0;
error = ClearChat.checkDeleteAmount(toDeleteAmount);
if (error != null) {
error = clearChat.checkDeleteAmount(toDeleteAmount);
if (error != null)
{
event.getMessage().reply(error).queue();
return;
}
@@ -87,20 +106,20 @@ public class ClearCommand implements MessageCommand
String content = "\uD83D\uDEA7 Clearing...";
Message botMessage = event.getMessage().reply(content).complete();
int deleted = ClearChat.delete(toDeleteAmount,
int deleted = clearChat.delete(toDeleteAmount,
event.getMessageIdLong(),
event.getChannel());
// get a nicely formatted message that logs the deletion of messages.
content = ClearChat.parseAmount(deleted);
content = clearChat.parseAmount(deleted);
// edit the message text and attach a button.
Button dismiss = ClearChat.getDismissButton();
Button dismiss = clearChat.getDismissButton();
Message finalMessage = event.getChannel().sendMessage(content).setActionRow(dismiss).complete();
// add the message to database.
Cache.getDatabaseSource().queueDisabling(finalMessage);
Cache.getDatabaseSource().trackRanCommandReply(finalMessage, event.getAuthor());
Cache.getServices().databaseService().queueDisabling(finalMessage);
Cache.getServices().databaseService().trackRanCommandReply(finalMessage, event.getAuthor());
// delete the sender's message.
event.getMessage().delete().queue();
@@ -4,6 +4,8 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -12,53 +14,69 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class CoinFlipCommand implements MessageCommand
@Component
public class MessageCoinFlipCommand implements MessageCommand
{
private final CoinFlip coinFlip;
public MessageCoinFlipCommand(@Autowired CoinFlip coinFlip)
{
this.coinFlip = coinFlip;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("coinflip", "flip", "flipcoin"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // null because it can be used anywhere
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Flip a coin.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) {
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
// perform coin flip
event.getMessage().reply(CoinFlip.genRandom())
.addActionRow(CoinFlip.getReflipButton())
event.getMessage().reply(coinFlip.genRandom())
.addActionRow(coinFlip.getReflipButton())
.queue((message) ->
{
// set the command as expiring and restrict it to the user who ran it
CoinFlip.trackAndRestrict(message, event.getAuthor());
}, (error) -> {});
coinFlip.trackAndRestrict(message, event.getAuthor());
}, (error) -> {
});
}
}
@@ -4,6 +4,8 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
@@ -13,44 +15,60 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class DiceRollCommand implements MessageCommand
@Component
public class MessageDiceRollCommand implements MessageCommand
{
private final DiceRoll diceRoll;
public MessageDiceRollCommand(@Autowired DiceRoll diceRoll)
{
this.diceRoll = diceRoll;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("diceroll", "droll", "roll"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
return "Roll dice. You can roll multiple dice at the same time." +
"\nExamples:" +
"\n - `d8 10` to roll an 8-sided die 10 times." +
"\n - `d12 3 d5 10` to roll a 12-sided die 3 times, and then a 5-sided die 10 times." +
"\n - `30` to roll a standard 6-sided die 30 times." +
"\n - `d10` to roll a 10-sided die once.";
public String getDescription()
{
return """
Roll dice. You can roll multiple dice at the same time.
Examples:
- `d8 10` to roll an 8-sided die 10 times.
- `d12 3 d5 10` to roll a 12-sided die 3 times, and then a 5-sided die 10 times.
- `30` to roll a standard 6-sided die 30 times.
- `d10` to roll a 10-sided die once.
""";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "[dice size] [rolls]";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@@ -58,7 +76,7 @@ public class DiceRollCommand implements MessageCommand
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
MessageResponse response = DiceRoll.buildResponse(event.getAuthor(), args);
MessageResponse response = diceRoll.buildResponse(event.getAuthor(), args);
if (response.content() != null)
{
@@ -4,6 +4,7 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -11,37 +12,46 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class HelloCommand implements MessageCommand
@Component
public class MessageHelloCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("hi", "hello", "heya"));
}
@Override
public List<Permission> getPermissions() { return null; }
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Get pinged by the bot.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@@ -6,49 +6,63 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Alias;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
public class HelpCommand implements MessageCommand
@Component
public class MessageHelpCommand implements MessageCommand
{
private final Alias alias;
public MessageHelpCommand(@Autowired Alias alias)
{
this.alias = alias;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("help"));
}
@Nullable
@Override
public List<Permission> getPermissions() { return null; }
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Get general help on the bot. Specify a command if you want specific help about that command.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "[command]";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@@ -82,10 +96,11 @@ public class HelpCommand implements MessageCommand
"\nYou will find a list of commands organized in categories below.",
false);
for(CommandCategory category : commandCategories.keySet())
for (Map.Entry<CommandCategory, LinkedList<MessageCommand>> entry : commandCategories.entrySet())
{
StringBuilder commandsList = new StringBuilder();
LinkedList<MessageCommand> commandsOfThisCategory = commandCategories.get(category);
CommandCategory category = entry.getKey();
LinkedList<MessageCommand> commandsOfThisCategory = entry.getValue();
for (int pos = 0; pos < commandsOfThisCategory.size(); pos++)
{
@@ -104,7 +119,8 @@ public class HelpCommand implements MessageCommand
}
event.getMessage().replyEmbeds(embedBuilder.build()).queue();
} else {
} else
{
String commandLabel = args[0].toLowerCase();
MessageCommand command = Cache.getMessageCommandListener().getRegisteredCommand(commandLabel);
@@ -120,14 +136,15 @@ public class HelpCommand implements MessageCommand
if (internalUsage != null) usage += " " + internalUsage;
usage += "`";
String aliases = Alias.generateNiceAliases(command);
String aliases = alias.generateNiceAliases(command);
List<Permission> permissions = command.getPermissions();
StringBuilder permissionsStringBuilder = new StringBuilder();
if (permissions == null)
{
permissionsStringBuilder = new StringBuilder("Available to everyone");
} else {
} else
{
for (int i = 0; i < permissions.size(); i++)
{
Permission permission = permissions.get(i);
@@ -7,6 +7,8 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Invite;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -15,40 +17,53 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class InviteCommand implements MessageCommand
@Component
public class MessageInviteCommand implements MessageCommand
{
private final Invite invite;
public MessageInviteCommand(@Autowired Invite invite)
{
this.invite = invite;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("invite"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Get the bot's invite link.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@@ -57,8 +72,8 @@ public class InviteCommand implements MessageCommand
{
MessageEmbed inviteEmbed = Invite.generateEmbed();
Button inviteButton = Invite.getInviteButton();
MessageEmbed inviteEmbed = invite.generateEmbed();
Button inviteButton = invite.getInviteButton();
// if this is a guild, don't spam the invite in public but DM it
if (event.getChannelType().isGuild())
@@ -70,7 +85,8 @@ public class InviteCommand implements MessageCommand
.queue();
event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
}, error -> event.getMessage().addReaction(Emoji.fromUnicode("")).queue());
} else {
} else
{
event.getMessage()
.replyEmbeds(inviteEmbed)
.addActionRow(inviteButton)
@@ -1,19 +1,12 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -22,46 +15,59 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class KickCommand implements MessageCommand
@Component
public class MessageKickCommand implements MessageCommand
{
private final UserPunishment userPunishment;
public MessageKickCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("kick"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return new ArrayList<Permission>(Collections.singletonList(Permission.KICK_MEMBERS));
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Kick the mentioned user from the guild.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<mentioned user> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.KICK);
userPunishment.handle(event, args, UserPunishment.PunishmentType.KICK);
}
}
@@ -8,6 +8,8 @@ import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
@@ -17,8 +19,15 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class LoveCalculatorCommand implements MessageCommand
@Component
public class MessageLoveCalculatorCommand implements MessageCommand
{
private final LoveCalculator loveCalculator;
public MessageLoveCalculatorCommand(@Autowired LoveCalculator loveCalculator)
{
this.loveCalculator = loveCalculator;
}
@Override
@@ -29,30 +38,35 @@ public class LoveCalculatorCommand implements MessageCommand
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; //anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Calculate how much two people love each other. You can mention two people or just one.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<person 1> [person 2]";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@@ -80,12 +94,13 @@ public class LoveCalculatorCommand implements MessageCommand
if (mentions.size() == 1)
{
user2 = event.getAuthor();
} else {
} else
{
mentionedUserId = mentions.get(1).getId();
user2 = HidekoBot.getAPI().retrieveUserById(mentionedUserId).complete();
}
MessageEmbed embed = LoveCalculator.buildEmbedAndCacheResult(event.getAuthor(), user1, user2);
MessageEmbed embed = loveCalculator.buildEmbedAndCacheResult(event.getAuthor(), user1, user2);
event.getChannel().sendMessageEmbeds(embed).queue();
}
@@ -4,6 +4,8 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -11,40 +13,53 @@ import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
import java.util.List;
public class MagicBallCommand implements MessageCommand
@Component
public class MessageMagicBallCommand implements MessageCommand
{
private final MagicBall magicBall;
public MessageMagicBallCommand(@Autowired MagicBall magicBall)
{
this.magicBall = magicBall;
}
@Override
public LinkedList<String> getCommandLabels() {
return MagicBall.getLabels();
public LinkedList<String> getCommandLabels()
{
return magicBall.getLabels();
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Ask a question to the Magic Ball.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<question>";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@@ -69,6 +84,6 @@ public class MagicBallCommand implements MessageCommand
String question = questionBuilder.toString();
event.getChannel().sendMessageEmbeds(MagicBall.generateEmbed(question, event.getAuthor())).queue();
event.getChannel().sendMessageEmbeds(magicBall.generateEmbed(question, event.getAuthor())).queue();
}
}
@@ -1,9 +1,12 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -12,39 +15,54 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class SayCommand implements MessageCommand
@Component
public class MessageSayCommand implements MessageCommand
{
private final Say say;
public MessageSayCommand(@Autowired Say say)
{
this.say = say;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("say"));
}
@Nullable
@Override
public List<Permission> getPermissions() { return Collections.singletonList(Say.getPermission()); }
public List<Permission> getPermissions()
{
return Collections.singletonList(say.getPermission());
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return true;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Make the bot say something for you.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<text>";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@@ -56,14 +74,23 @@ public class SayCommand implements MessageCommand
if (args.length != 0 && !args[0].isEmpty())
{
messageContent = args[0];
} else {
} else
{
event.getMessage().reply("\uD83D\uDE20 Hey, you have to tell me what to say!")
.queue();
return;
}
event.getChannel().sendMessage(messageContent).queue();
event.getMessage().delete().queue();
if (event.getChannel() instanceof TextChannel)
{
event.getMessage().delete().queue(response -> {
// nothing to do with the response
}, error -> {
// ignore the error if we couldn't delete it, we were probably missing permissions
// without this block it would print a stack trace in console
});
}
}
}
@@ -4,6 +4,8 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
@@ -13,46 +15,59 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class TimeoutCommand implements MessageCommand
@Component
public class MessageTimeoutCommand implements MessageCommand
{
private final UserPunishment userPunishment;
public MessageTimeoutCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("timeout"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return new ArrayList<Permission>(Collections.singletonList(Permission.MODERATE_MEMBERS));
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Timeout the mentioned user.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<mentioned user> <duration> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.TIMEOUT);
userPunishment.handle(event, args, UserPunishment.PunishmentType.TIMEOUT);
}
}
@@ -5,6 +5,7 @@ import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.Cache;
@@ -18,40 +19,46 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class TriviaCommand implements MessageCommand
public class MessageTriviaCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("trivia"));
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Start a Trivia session and play with others!";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return null;
}
@@ -77,11 +84,20 @@ public class TriviaCommand implements MessageCommand
MessageResponse response = Trivia.generateMainScreen();
event.getMessage().replyEmbeds(response.embed()).addActionRow(response.components()).queue(message ->
Message recvMessage = event.getMessage();
MessageCreateAction responseAction = null;
if (response.content() != null) responseAction = recvMessage.reply(response.content());
else if (response.embed() != null) responseAction = recvMessage.replyEmbeds(response.embed());
if (responseAction != null)
{
Cache.getDatabaseSource().trackRanCommandReply(message, event.getAuthor());
Cache.getDatabaseSource().queueDisabling(message);
if (response.components() != null) responseAction = responseAction.addActionRow(response.components());
responseAction.queue(message -> {
Cache.getServices().databaseService().trackRanCommandReply(message, event.getAuthor());
Cache.getServices().databaseService().queueDisabling(message);
});
}
}
@@ -17,40 +17,46 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class UrbanDictionaryCommand implements MessageCommand
public class MessageUrbanDictionaryCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
public LinkedList<String> getCommandLabels()
{
return UrbanDictionary.getCommandLabels();
}
@Nullable
@Override
public List<Permission> getPermissions() {
public List<Permission> getPermissions()
{
return null; //anyone can use it
}
@Override
public boolean passRawArgs() {
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription() {
public String getDescription()
{
return "Look something up in the Urban Dictionary.";
}
@Nullable
@Override
public String getUsage() {
public String getUsage()
{
return "<query>";
}
@NotNull
@Override
public CommandCategory getCategory() {
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@@ -66,7 +72,8 @@ public class UrbanDictionaryCommand implements MessageCommand
// sanitize args by only keeping letters and numbers, and adding "+" instead of spaces for HTML parsing
StringBuilder termBuilder = new StringBuilder();
for (int i = 0; i < args.length; i++) {
for (int i = 0; i < args.length; i++)
{
String arg = args[i];
termBuilder.append(arg);
@@ -79,9 +86,11 @@ public class UrbanDictionaryCommand implements MessageCommand
Document doc;
try {
try
{
doc = Jsoup.connect(url).get();
} catch (IOException e) {
} catch (IOException e)
{
event.getMessage().reply(UrbanDictionary.getTermNotFoundError()).queue();
return;
}
@@ -7,14 +7,24 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class AvatarCommand extends SlashCommandImpl
@Component
public class SlashAvatarCommand extends SlashCommandImpl
{
private final ProfileImage profileImage;
public SlashAvatarCommand(@NotNull ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@Override
public CommandData getSlashCommandData() {
public CommandData getSlashCommandData()
{
return Commands.slash("avatar", "Get someone's profile picture.")
.addOption(OptionType.USER, "user", "User you want to grab the avatar of.")
.addOption(OptionType.INTEGER, "size", "The size of the returned image.",
@@ -35,19 +45,21 @@ public class AvatarCommand extends SlashCommandImpl
if (userArg != null)
{
user = userArg.getAsUser();
} else {
} else
{
user = event.getUser();
}
OptionMapping sizeArg = event.getOption("size");
if (sizeArg != null)
{
resolution = ProfileImage.parseResolution(sizeArg.getAsInt());
} else {
resolution = ProfileImage.parseResolution(512);
resolution = profileImage.parseResolution(sizeArg.getAsInt());
} else
{
resolution = profileImage.parseResolution(512);
}
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
MessageResponse response = profileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if (response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
@@ -7,11 +7,21 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class BanCommand extends SlashCommandImpl
@Component
public class SlashBanCommand extends SlashCommandImpl
{
private final UserPunishment userPunishment;
public SlashBanCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@Override
public CommandData getSlashCommandData()
{
@@ -31,6 +41,6 @@ public class BanCommand extends SlashCommandImpl
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.BAN);
userPunishment.handle(event, UserPunishment.PunishmentType.BAN);
}
}
@@ -7,14 +7,24 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class BannerCommand extends SlashCommandImpl
@Component
public class SlashBannerCommand extends SlashCommandImpl
{
private final ProfileImage profileImage;
public SlashBannerCommand(@NotNull ProfileImage profileImage)
{
this.profileImage = profileImage;
}
@Override
public CommandData getSlashCommandData() {
public CommandData getSlashCommandData()
{
return Commands.slash("banner", "Get someone's profile banner.")
.addOption(OptionType.USER, "user", "User you want to grab the banner of.")
.addOption(OptionType.INTEGER, "size", "The size of the returned image.",
@@ -35,19 +45,21 @@ public class BannerCommand extends SlashCommandImpl
if (userArg != null)
{
user = userArg.getAsUser();
} else {
} else
{
user = event.getUser();
}
OptionMapping sizeArg = event.getOption("size");
if (sizeArg != null)
{
resolution = ProfileImage.parseResolution(sizeArg.getAsInt());
} else {
resolution = ProfileImage.parseResolution(512);
resolution = profileImage.parseResolution(sizeArg.getAsInt());
} else
{
resolution = profileImage.parseResolution(512);
}
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
MessageResponse response = profileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
if (response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
@@ -5,6 +5,8 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
@@ -13,10 +15,20 @@ import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.LinkedList;
import java.util.List;
public class BotInfoCommand extends SlashCommandImpl
@Component
public class SlashBotInfoCommand extends SlashCommandImpl
{
private final BotInfo botInfo;
public SlashBotInfoCommand(@Autowired BotInfo botInfo)
{
this.botInfo = botInfo;
}
@Override
public CommandData getSlashCommandData() {
public CommandData getSlashCommandData()
{
return Commands.slash("botinfo", "Get info about the bot.");
}
@@ -36,7 +48,7 @@ public class BotInfoCommand extends SlashCommandImpl
}
// send the list
MessageEmbed embed = BotInfo.generateEmbed(registeredCommandNames);
MessageEmbed embed = botInfo.generateEmbed(registeredCommandNames);
event.getHook().editOriginalEmbeds(embed).queue();
}
}
@@ -9,19 +9,29 @@ import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class ClearCommand extends SlashCommandImpl
@Component
public class SlashClearCommand extends SlashCommandImpl
{
private final ClearChat clearChat;
public SlashClearCommand(@Autowired ClearChat clearChat)
{
this.clearChat = clearChat;
}
@Override
public CommandData getSlashCommandData() {
return Commands.slash(ClearChat.getLabel(),
ClearChat.getDescription())
public CommandData getSlashCommandData()
{
return Commands.slash(clearChat.getLabel(),
clearChat.getDescription())
.addOption(OptionType.INTEGER, "amount", "The amount of messages to delete.")
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(ClearChat.getPermission()));
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(clearChat.getPermission()));
}
@Override
@@ -31,7 +41,7 @@ public class ClearCommand extends SlashCommandImpl
event.deferReply().queue();
// check if user is trying to run command in dms.
String error = ClearChat.checkDMs(event.getChannel());
String error = clearChat.checkDMs(event.getChannel());
if (error != null)
{
event.getHook().editOriginal(error).queue();
@@ -45,9 +55,9 @@ public class ClearCommand extends SlashCommandImpl
int toDeleteAmount = amountOption == null ? 1 : amountOption.getAsInt();
// cap the amount to avoid abuse.
if(toDeleteAmount > ClearChat.getMaxAmount()) toDeleteAmount = 0;
if (toDeleteAmount > clearChat.getMaxAmount()) toDeleteAmount = 0;
error = ClearChat.checkDeleteAmount(toDeleteAmount);
error = clearChat.checkDeleteAmount(toDeleteAmount);
if (error != null)
{
event.getHook().editOriginal(error).queue();
@@ -59,20 +69,20 @@ public class ClearCommand extends SlashCommandImpl
Message botMessage = event.getHook().editOriginal(content).complete();
// actually delete the messages.
int deleted = ClearChat.delete(toDeleteAmount,
int deleted = clearChat.delete(toDeleteAmount,
event.getInteraction().getIdLong(),
event.getChannel());
// get a nicely formatted message that logs the deletion of messages.
content = ClearChat.parseAmount(deleted);
content = clearChat.parseAmount(deleted);
// edit the message text and attach a button.
Button dismiss = ClearChat.getDismissButton();
Button dismiss = clearChat.getDismissButton();
botMessage = botMessage.editMessage(content).setActionRow(dismiss).complete();
// add the message to database.
Cache.getDatabaseSource().queueDisabling(botMessage);
Cache.getDatabaseSource().trackRanCommandReply(botMessage, event.getUser());
Cache.getServices().databaseService().queueDisabling(botMessage);
Cache.getServices().databaseService().trackRanCommandReply(botMessage, event.getUser());
}
}
@@ -4,11 +4,20 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class CoinFlipCommand extends SlashCommandImpl
@Component
public class SlashCoinFlipCommand extends SlashCommandImpl
{
private final CoinFlip coinFlip;
public SlashCoinFlipCommand(@Autowired CoinFlip coinFlip)
{
this.coinFlip = coinFlip;
}
@Override
public CommandData getSlashCommandData()
@@ -21,20 +30,19 @@ public class CoinFlipCommand extends SlashCommandImpl
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// perform coin flip
event.reply(CoinFlip.genRandom())
.addActionRow(CoinFlip.getReflipButton())
event.reply(coinFlip.genRandom())
.addActionRow(coinFlip.getReflipButton())
.queue((interaction) ->
{
// set the command as expiring and restrict it to the user who ran it
interaction.retrieveOriginal().queue((message) ->
{
CoinFlip.trackAndRestrict(message, event.getUser());
}, (error) -> {});
}, (error) -> {});
coinFlip.trackAndRestrict(message, event.getUser());
}, (error) -> {
});
}, (error) -> {
});
}
}
@@ -6,12 +6,21 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class DiceRollCommand extends SlashCommandImpl
@Component
public class SlashDiceRollCommand extends SlashCommandImpl
{
private final DiceRoll diceRoll;
public SlashDiceRollCommand(@NotNull DiceRoll diceRoll)
{
this.diceRoll = diceRoll;
}
@Override
public CommandData getSlashCommandData()
{
@@ -37,7 +46,7 @@ public class DiceRollCommand extends SlashCommandImpl
String[] args = messageContent.split("\\s");
MessageResponse response = DiceRoll.buildResponse(event.getUser(), args);
MessageResponse response = diceRoll.buildResponse(event.getUser(), args);
if (response.content() != null)
{
@@ -10,12 +10,14 @@ import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DieCommand extends SlashCommandImpl
public class SlashDieCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData() {
public CommandData getSlashCommandData()
{
return Commands.slash("die", "Stop the bot's process.")
.setDefaultPermissions(DefaultMemberPermissions.DISABLED);
}
@@ -26,9 +28,13 @@ public class DieCommand extends SlashCommandImpl
if (Cache.getBotOwnerId() != event.getUser().getIdLong())
{
event.reply("Sorry, only the bot owner can run this command!").setEphemeral(true).queue();
} else {
} else
{
event.reply("Going to sleep! Cya ✨").queue();
Executors.newSingleThreadScheduledExecutor().schedule(HidekoBot::shutdown, 3, TimeUnit.SECONDS);
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.schedule(HidekoBot::shutdown, 3, TimeUnit.SECONDS);
}
}
}
}
@@ -8,7 +8,7 @@ import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class HelpCommand extends SlashCommandImpl
public class SlashHelpCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
@@ -10,11 +10,19 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Invite;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class InviteCommand extends SlashCommandImpl
@Component
public class SlashInviteCommand extends SlashCommandImpl
{
private final Invite invite;
public SlashInviteCommand(@NotNull Invite invite)
{
this.invite = invite;
}
@Override
public CommandData getSlashCommandData()
@@ -36,8 +44,8 @@ public class InviteCommand extends SlashCommandImpl
}
replyCallbackAction.queue();
MessageEmbed inviteEmbed = Invite.generateEmbed();
Button inviteButton = Invite.getInviteButton();
MessageEmbed inviteEmbed = invite.generateEmbed();
Button inviteButton = invite.getInviteButton();
WebhookMessageEditAction<Message> reply =
event.getHook()
@@ -1,27 +1,28 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.Say;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class KickCommand extends SlashCommandImpl
@Component
public class SlashKickCommand extends SlashCommandImpl
{
private final UserPunishment userPunishment;
public SlashKickCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@Override
public CommandData getSlashCommandData()
{
@@ -41,6 +42,6 @@ public class KickCommand extends SlashCommandImpl
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.KICK);
userPunishment.handle(event, UserPunishment.PunishmentType.KICK);
}
}
@@ -8,11 +8,20 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class LoveCalculatorCommand extends SlashCommandImpl
@Component
public class SlashLoveCalculatorCommand extends SlashCommandImpl
{
private final LoveCalculator loveCalculator;
public SlashLoveCalculatorCommand(@NotNull LoveCalculator loveCalculator)
{
this.loveCalculator = loveCalculator;
}
@Override
public CommandData getSlashCommandData()
{
@@ -40,7 +49,8 @@ public class LoveCalculatorCommand extends SlashCommandImpl
if (firsUserArg != null)
{
firstUser = firsUserArg.getAsUser(); //todo null check?
} else {
} else
{
event.reply("\uD83D\uDE22 I need to know who to check! Please mention them.")
.setEphemeral(true)
.queue();
@@ -51,11 +61,12 @@ public class LoveCalculatorCommand extends SlashCommandImpl
if (secondUserArg != null)
{
secondUser = secondUserArg.getAsUser(); //todo null check?
} else {
} else
{
secondUser = event.getUser();
}
MessageEmbed embed = LoveCalculator.buildEmbedAndCacheResult(event.getUser(), firstUser, secondUser);
MessageEmbed embed = loveCalculator.buildEmbedAndCacheResult(event.getUser(), firstUser, secondUser);
event.replyEmbeds(embed).queue();
}
}
@@ -7,16 +7,25 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class MagicBallCommand extends SlashCommandImpl
@Component
public class SlashMagicBallCommand extends SlashCommandImpl
{
private final MagicBall magicBall;
public SlashMagicBallCommand(@NotNull MagicBall magicBall)
{
this.magicBall = magicBall;
}
@Override
public CommandData getSlashCommandData()
{
return Commands.slash(MagicBall.getLabels().get(0),
return Commands.slash(magicBall.getLabels().get(0),
"Ask a question to the magic ball.")
.addOption(OptionType.STRING, "question",
"The question to ask.",
@@ -43,7 +52,7 @@ public class MagicBallCommand extends SlashCommandImpl
return;
}
MessageEmbed response = MagicBall.generateEmbed(question, event.getUser());
MessageEmbed response = magicBall.generateEmbed(question, event.getUser());
event.replyEmbeds(response).queue();
}
}
@@ -6,7 +6,7 @@ import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class PingCommand extends SlashCommandImpl
public class SlashPingCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
@@ -8,11 +8,20 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class SayCommand extends SlashCommandImpl
@Component
public class SlashSayCommand extends SlashCommandImpl
{
private final Say say;
public SlashSayCommand(@NotNull Say say)
{
this.say = say;
}
@Override
public CommandData getSlashCommandData()
{
@@ -22,7 +31,7 @@ public class SayCommand extends SlashCommandImpl
"The message to send.",
true,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Say.getPermission()));
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(say.getPermission()));
}
@Override
@@ -7,11 +7,22 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class TimeoutCommand extends SlashCommandImpl
@Component
public class SlashTimeoutCommand extends SlashCommandImpl
{
private final UserPunishment userPunishment;
public SlashTimeoutCommand(@Autowired UserPunishment userPunishment)
{
this.userPunishment = userPunishment;
}
@Override
public CommandData getSlashCommandData()
{
@@ -35,6 +46,6 @@ public class TimeoutCommand extends SlashCommandImpl
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.TIMEOUT);
userPunishment.handle(event, UserPunishment.PunishmentType.TIMEOUT);
}
}
@@ -11,7 +11,7 @@ import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class TriviaCommand extends SlashCommandImpl
public class SlashTriviaCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
@@ -44,8 +44,8 @@ public class TriviaCommand extends SlashCommandImpl
event.getHook().editOriginalEmbeds(response.embed()).setActionRow(response.components()).queue(message ->
{
Cache.getDatabaseSource().trackRanCommandReply(message, event.getUser());
Cache.getDatabaseSource().queueDisabling(message);
Cache.getServices().databaseService().trackRanCommandReply(message, event.getUser());
Cache.getServices().databaseService().queueDisabling(message);
});
}
}
@@ -17,7 +17,7 @@ import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.io.IOException;
public class UrbanDictionaryCommand extends SlashCommandImpl
public class SlashUrbanDictionaryCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
@@ -54,9 +54,11 @@ public class UrbanDictionaryCommand extends SlashCommandImpl
Document doc;
try {
try
{
doc = Jsoup.connect(url).get();
} catch (IOException e) {
} catch (IOException e)
{
event.reply(UrbanDictionary.getTermNotFoundError())
.setEphemeral(true)
.queue();
@@ -7,18 +7,27 @@ public enum ConfigurationEntry
BOT_OWNER_ID("bot-owner-id", 100000000000000000L),
BOT_COLOR("bot-color", "PINK"),
HEARTBEAT_LINK("heartbeat-link", "https://your-heartbeat-api.com/api/push/apikey?status=up&msg=OK&ping="),
RANDOM_ORG_API_KEY("random-org-api-key", "00000000-0000-0000-0000-000000000000"),
;
private String path;
private Object defaultValue;
ConfigurationEntry(String path, Object defaultValue)
{
this.path = path;
this.defaultValue = defaultValue;
}
public String getPath() { return path; }
public Object getDefaultValue() { return defaultValue; }
public String getPath()
{
return path;
}
public Object getDefaultValue()
{
return defaultValue;
}
}
@@ -1,26 +1,26 @@
package wtf.beatrice.hidekobot.datasources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.Logger;
import java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
public class ConfigurationSource
{
private final LinkedHashMap<String, Object> configurationEntries = new LinkedHashMap<>();
private final Logger logger;
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationSource.class);
private final String configFilePath;
public ConfigurationSource(String configFilePath)
{
this.configFilePath = configFilePath;
logger = new Logger(getClass());
}
public void initConfig()
@@ -39,7 +39,7 @@ public class ConfigurationSource
if (internalConfigContents.isEmpty())
{
logger.log("Error reading internal configuration!");
LOGGER.error("Error reading internal configuration!");
HidekoBot.shutdown();
return;
}
@@ -49,20 +49,32 @@ public class ConfigurationSource
if (!fsConfigFile.exists())
{
// try to create config file
try { fsConfigFile.createNewFile(); }
catch (IOException e) {
logger.log("Error creating configuration file!");
logger.log(e.getMessage());
try
{
if (!fsConfigFile.createNewFile())
{
LOGGER.error("We tried creating a file that already exists!");
HidekoBot.shutdown();
return;
}
} catch (IOException e)
{
LOGGER.error("Error creating configuration file!", e);
HidekoBot.shutdown();
return;
}
}
// load the YAML file from the filesystem
Yaml fsConfigYaml = new Yaml(new SafeConstructor());
LoaderOptions options = new LoaderOptions();
Yaml fsConfigYaml = new Yaml(new SafeConstructor(options));
LinkedHashMap<String, Object> fsConfigContents = null; // map holding all file entries
try (InputStream fsConfigStream = new FileInputStream(fsConfigFile))
{ fsConfigContents = fsConfigYaml.load(fsConfigStream); }
catch (IOException e) { logger.log(e.getMessage()); }
{
fsConfigContents = fsConfigYaml.load(fsConfigStream);
} catch (IOException e)
{
LOGGER.error(e.getMessage());
}
if (fsConfigContents == null) // if file contents are empty or corrupted...
@@ -90,19 +102,24 @@ public class ConfigurationSource
// create a new mixed map that will take existing values from the non-missing keys
// and fill everything else with the default values
LinkedHashMap<String, Object> filledEntries = new LinkedHashMap<>();
for(String key : internalConfigContents.keySet())
for (Map.Entry<String, Object> entry : internalConfigContents.entrySet())
{
String key = entry.getKey();
if (fsConfigContents.containsKey(key))
{
// if the key already exists, copy the original value
filledEntries.put(key, fsConfigContents.get(key));
} else {
} else
{
// else, copy the value from the example config file
filledEntries.put(key, internalConfigContents.get(key));
filledEntries.put(key, entry.getValue());
}
}
try {
try
{
// new writer to actually write the contents to the file
PrintWriter missingKeysWriter = new PrintWriter(fsConfigFile);
@@ -115,15 +132,17 @@ public class ConfigurationSource
// create the yaml object and dump the values to filesystem
Yaml yaml = new Yaml(dumperOptions);
yaml.dump(filledEntries, missingKeysWriter);
} catch (FileNotFoundException e) {
logger.log(e.getMessage());
} catch (FileNotFoundException e)
{
LOGGER.error(e.getMessage());
HidekoBot.shutdown();
return;
}
// finally, dump all entries to cache.
loadConfig(filledEntries);
} else {
} else
{
// if no key is missing, just cache all entries and values from filesystem.
loadConfig(fsConfigContents);
}
@@ -134,6 +153,7 @@ public class ConfigurationSource
{
this.configurationEntries.putAll(configurationEntries);
}
public Object getConfigValue(ConfigurationEntry key)
{
return configurationEntries.get(key.getPath());
@@ -1,619 +0,0 @@
package wtf.beatrice.hidekobot.datasources;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.Logger;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class DatabaseSource
{
private final static String sqliteURL = "jdbc:sqlite:%path%";
private Connection dbConnection = null;
private final String dbPath;
private final Logger logger;
public DatabaseSource(String dbPath)
{
this.dbPath = dbPath;
this.logger = new Logger(getClass());
}
public boolean connect()
{
String url = sqliteURL.replace("%path%", dbPath);
if(!close()) return false;
try {
dbConnection = DriverManager.getConnection(url);
logger.log("Database connection established!");
return true;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public boolean close()
{
if (dbConnection != null)
{
try {
if(!dbConnection.isClosed())
{
dbConnection.close();
}
} catch (SQLException e) {
e.printStackTrace();
return false;
}
dbConnection = null;
}
return true;
}
/*
* DB STRUCTURE
* TABLE 1: pending_disabled_messages
* ----------------------------------------------------------------------------------
* | guild_id | channel_id | message_id | expiry_timestamp |
* ----------------------------------------------------------------------------------
* |39402849302 | 39402849302 | 39402849302 | 2022-11-20 22:45:53:300 |
* ---------------------------------------------------------------------------------
*
*
* TABLE 2: command_runners
* --------------------------------------------------------------------------------------------
* | guild_id | channel_id | message_id | user_id | channel_type |
* --------------------------------------------------------------------------------------------
* | 39402849302 | 39402849302 | 39402849302 | 39402849302 | PRIVATE |
* --------------------------------------------------------------------------------------------
*
* TABLE 3: urban_dictionary
* -----------------------------------------------------------------------------------------------------
* | message_id | page | meanings | examples | contributors | dates | term |
* -----------------------------------------------------------------------------------------------------
* | 39402849302 | 0 | base64 | base64 | base64 | base64 | miku |
* -----------------------------------------------------------------------------------------------------
*/
//todo: javadocs
public boolean initDb()
{
List<String> newTables = new ArrayList<>();
newTables.add("CREATE TABLE IF NOT EXISTS pending_disabled_messages (" +
"guild_id TEXT NOT NULL, " +
"channel_id TEXT NOT NULL," +
"message_id TEXT NOT NULL," +
"expiry_timestamp TEXT NOT NULL " +
");");
newTables.add("CREATE TABLE IF NOT EXISTS command_runners (" +
"guild_id TEXT NOT NULL, " +
"channel_id TEXT NOT NULL," + // channel the command was run in
"message_id TEXT NOT NULL," + // message id of the bot's response
"user_id TEXT NOT NULL, " + // user who ran the command
"channel_type TEXT NOT NULL" + // channel type (PRIVATE, FORUM, ...)
");");
newTables.add("CREATE TABLE IF NOT EXISTS urban_dictionary (" +
"message_id TEXT NOT NULL, " + // message id of the bot's response
"page INTEGER NOT NULL," + // page that we are currently on
"meanings TEXT NOT NULL," + // list of all meanings, serialized and encoded
"examples TEXT NOT NULL, " + // list of all examples, serialized and encoded
"contributors TEXT NOT NULL, " + // list of all contributors, serialized and encoded
"dates TEXT NOT NULL, " + // list of all submission dates, serialized and encoded
"term TEXT NOT NULL" + // the term that was searched
");");
for(String sql : newTables)
{
try (Statement stmt = dbConnection.createStatement()) {
// execute the statement
stmt.execute(sql);
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
return true;
}
public boolean trackRanCommandReply(Message message, User user)
{
String userId = user.getId();
String guildId;
ChannelType channelType = message.getChannelType();
if(!(channelType.isGuild()))
{
guildId = userId;
} else {
guildId = message.getGuild().getId();
}
String channelId = message.getChannel().getId();
String messageId = message.getId();
String query = "INSERT INTO command_runners " +
"(guild_id, channel_id, message_id, user_id, channel_type) VALUES " +
" (?, ?, ?, ?, ?);";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, guildId);
preparedStatement.setString(2, channelId);
preparedStatement.setString(3, messageId);
preparedStatement.setString(4, userId);
preparedStatement.setString(5, channelType.name());
preparedStatement.executeUpdate();
return true;
} catch (SQLException e)
{
e.printStackTrace();
}
return false;
}
public boolean isUserTrackedFor(String userId, String messageId)
{
String trackedUserId = getTrackedReplyUserId(messageId);
if(trackedUserId == null) return false;
return userId.equals(trackedUserId);
}
public ChannelType getTrackedMessageChannelType(String messageId)
{
String query = "SELECT channel_type " +
"FROM command_runners " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
String channelTypeName = resultSet.getString("channel_type");
return ChannelType.valueOf(channelTypeName);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getTrackedReplyUserId(String messageId)
{
String query = "SELECT user_id " +
"FROM command_runners " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("user_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public boolean queueDisabling(Message message)
{
String messageId = message.getId();
String channelId = message.getChannel().getId();
String guildId;
ChannelType channelType = message.getChannelType();
if(!(channelType.isGuild()))
{
guildId = "PRIVATE";
} else {
guildId = message.getGuild().getId();
}
LocalDateTime expiryTime = LocalDateTime.now().plusSeconds(Cache.getExpiryTimeSeconds());
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Cache.getExpiryTimestampFormat());
String expiryTimeFormatted = dateTimeFormatter.format(expiryTime);
String query = "INSERT INTO pending_disabled_messages " +
"(guild_id, channel_id, message_id, expiry_timestamp) VALUES " +
" (?, ?, ?, ?);";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, guildId);
preparedStatement.setString(2, channelId);
preparedStatement.setString(3, messageId);
preparedStatement.setString(4, expiryTimeFormatted);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e)
{
e.printStackTrace();
}
return false;
}
public List<String> getQueuedExpiringMessages()
{
List<String> messages = new ArrayList<>();
String query = "SELECT message_id " +
"FROM pending_disabled_messages ";
try (Statement statement = dbConnection.createStatement())
{
ResultSet resultSet = statement.executeQuery(query);
if(resultSet.isClosed()) return messages;
while(resultSet.next())
{
messages.add(resultSet.getString("message_id"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return messages;
}
public boolean untrackExpiredMessage(String messageId)
{
String query = "DELETE FROM pending_disabled_messages WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
preparedStatement.execute();
} catch (SQLException e)
{
e.printStackTrace();
return false;
}
query = "DELETE FROM command_runners WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
preparedStatement.execute();
} catch (SQLException e)
{
e.printStackTrace();
return false;
}
query = "DELETE FROM urban_dictionary WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
preparedStatement.execute();
} catch (SQLException e)
{
e.printStackTrace();
return false;
}
return true;
}
public String getQueuedExpiringMessageExpiryDate(String messageId)
{
String query = "SELECT expiry_timestamp " +
"FROM pending_disabled_messages " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("expiry_timestamp");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getQueuedExpiringMessageChannel(String messageId)
{
String query = "SELECT channel_id " +
"FROM pending_disabled_messages " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("channel_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getQueuedExpiringMessageGuild(String messageId)
{
String query = "SELECT guild_id " +
"FROM pending_disabled_messages " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("guild_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public boolean trackUrban(String meanings, String examples,
String contributors, String dates,
Message message, String term)
{
String query = "INSERT INTO urban_dictionary " +
"(message_id, page, meanings, examples, contributors, dates, term) VALUES " +
" (?, ?, ?, ?, ?, ?, ?);";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, message.getId());
preparedStatement.setInt(2, 0);
preparedStatement.setString(3, meanings);
preparedStatement.setString(4, examples);
preparedStatement.setString(5, contributors);
preparedStatement.setString(6, dates);
preparedStatement.setString(7, term);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e)
{
e.printStackTrace();
}
return false;
}
public int getUrbanPage(String messageId)
{
String query = "SELECT page " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return 0;
while(resultSet.next())
{
return resultSet.getInt("page");
}
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
}
public String getUrbanMeanings(String messageId)
{
String query = "SELECT meanings " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("meanings");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanExamples(String messageId)
{
String query = "SELECT examples " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("examples");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanContributors(String messageId)
{
String query = "SELECT contributors " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("contributors");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanDates(String messageId)
{
String query = "SELECT dates " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("dates");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public String getUrbanTerm(String messageId)
{
String query = "SELECT term " +
"FROM urban_dictionary " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, messageId);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.isClosed()) return null;
while(resultSet.next())
{
return resultSet.getString("term");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public boolean setUrbanPage(String messageId, int page)
{
String query = "UPDATE urban_dictionary " +
"SET page = ? " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setInt(1, page);
preparedStatement.setString(2, messageId);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public boolean resetExpiryTimestamp(String messageId)
{
LocalDateTime expiryTime = LocalDateTime.now().plusSeconds(Cache.getExpiryTimeSeconds());
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Cache.getExpiryTimestampFormat());
String expiryTimeFormatted = dateTimeFormatter.format(expiryTime);
String query = "UPDATE pending_disabled_messages " +
"SET expiry_timestamp = ? " +
"WHERE message_id = ?;";
try(PreparedStatement preparedStatement = dbConnection.prepareStatement(query))
{
preparedStatement.setString(1, expiryTimeFormatted);
preparedStatement.setString(2, messageId);
preparedStatement.executeUpdate();
return true;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
}
@@ -1,7 +1,8 @@
package wtf.beatrice.hidekobot.datasources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.Logger;
import java.io.IOException;
import java.io.InputStream;
@@ -12,7 +13,7 @@ public class PropertiesSource
private Properties properties = null;
private final String fileName = "default.properties";
private final Logger logger = new Logger(getClass());
private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesSource.class);
public void load()
{
@@ -24,14 +25,16 @@ public class PropertiesSource
{
properties.load(internalPropertiesStream);
}
catch (IOException e) {
logger.log(e.getMessage());
} catch (IOException e)
{
LOGGER.error(e.getMessage());
HidekoBot.shutdown();
return;
}
}
public String getProperty(String property)
{ return properties == null ? "" : properties.getProperty(property); }
{
return properties == null ? "" : properties.getProperty(property);
}
}
@@ -0,0 +1,77 @@
package wtf.beatrice.hidekobot.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "command_runners")
public class CommandRunner
{
@Id
@Column(name = "message_id", nullable = false)
private String messageId;
@Column(name = "guild_id", nullable = false)
private String guildId;
@Column(name = "channel_id", nullable = false)
private String channelId;
@Column(name = "user_id", nullable = false)
private String userId;
@Column(name = "channel_type", nullable = false)
private String channelType; // store JDA enum name
public String getMessageId()
{
return messageId;
}
public void setMessageId(String messageId)
{
this.messageId = messageId;
}
public String getGuildId()
{
return guildId;
}
public void setGuildId(String guildId)
{
this.guildId = guildId;
}
public String getChannelId()
{
return channelId;
}
public void setChannelId(String channelId)
{
this.channelId = channelId;
}
public String getUserId()
{
return userId;
}
public void setUserId(String userId)
{
this.userId = userId;
}
public String getChannelType()
{
return channelType;
}
public void setChannelType(String channelType)
{
this.channelType = channelType;
}
}
@@ -0,0 +1,64 @@
package wtf.beatrice.hidekobot.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "pending_disabled_messages")
public class PendingDisabledMessage
{
@Id
@Column(name = "message_id", nullable = false)
private String messageId;
@Column(name = "guild_id", nullable = false)
private String guildId;
@Column(name = "channel_id", nullable = false)
private String channelId;
@Column(name = "expiry_timestamp", nullable = false)
private String expiryTimestamp; // keep as String to match your format for now
public String getMessageId()
{
return messageId;
}
public void setMessageId(String messageId)
{
this.messageId = messageId;
}
public String getGuildId()
{
return guildId;
}
public void setGuildId(String guildId)
{
this.guildId = guildId;
}
public String getChannelId()
{
return channelId;
}
public void setChannelId(String channelId)
{
this.channelId = channelId;
}
public String getExpiryTimestamp()
{
return expiryTimestamp;
}
public void setExpiryTimestamp(String expiryTimestamp)
{
this.expiryTimestamp = expiryTimestamp;
}
}
@@ -0,0 +1,103 @@
package wtf.beatrice.hidekobot.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "urban_dictionary")
public class UrbanDictionaryEntry
{
@Id
@Column(name = "message_id", nullable = false)
private String messageId;
@Column(name = "page", nullable = false)
private Integer page;
@Column(name = "meanings", nullable = false, columnDefinition = "TEXT")
private String meanings;
@Column(name = "examples", nullable = false, columnDefinition = "TEXT")
private String examples;
@Column(name = "contributors", nullable = false, columnDefinition = "TEXT")
private String contributors;
@Column(name = "dates", nullable = false, columnDefinition = "TEXT")
private String dates;
@Column(name = "term", nullable = false)
private String term;
public String getMessageId()
{
return messageId;
}
public void setMessageId(String messageId)
{
this.messageId = messageId;
}
public Integer getPage()
{
return page;
}
public void setPage(Integer page)
{
this.page = page;
}
public String getMeanings()
{
return meanings;
}
public void setMeanings(String meanings)
{
this.meanings = meanings;
}
public String getExamples()
{
return examples;
}
public void setExamples(String examples)
{
this.examples = examples;
}
public String getContributors()
{
return contributors;
}
public void setContributors(String contributors)
{
this.contributors = contributors;
}
public String getDates()
{
return dates;
}
public void setDates(String dates)
{
this.dates = dates;
}
public String getTerm()
{
return term;
}
public void setTerm(String term)
{
this.term = term;
}
}
@@ -2,25 +2,42 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.commands.base.UrbanDictionary;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.services.CommandService;
@Component
public class ButtonInteractionListener extends ListenerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(ButtonInteractionListener.class);
private final CommandService commandService;
private final CoinFlip coinFlip;
public ButtonInteractionListener(@Autowired CommandService commandService,
@Autowired CoinFlip coinFlip)
{
this.commandService = commandService;
this.coinFlip = coinFlip;
}
@Override
public void onButtonInteraction(ButtonInteractionEvent event)
{
switch (event.getComponentId().toLowerCase()) {
switch (event.getComponentId().toLowerCase())
{
// coinflip
case "coinflip_reflip" -> CoinFlip.buttonReFlip(event);
case "coinflip_reflip" -> coinFlip.buttonReFlip(event);
// generic dismiss button
case "generic_dismiss" -> CommandUtil.delete(event);
case "generic_dismiss" -> commandService.deleteUserLinkedMessage(event);
// urban dictionary navigation
case "urban_nextpage" -> UrbanDictionary.changePage(event, UrbanDictionary.ChangeType.NEXT);
@@ -31,6 +48,10 @@ public class ButtonInteractionListener extends ListenerAdapter
case "trivia_wrong_1", "trivia_wrong_2", "trivia_wrong_3" ->
Trivia.handleAnswer(event, Trivia.AnswerType.WRONG);
// error handling
default -> LOGGER.warn("Received unhandled {}", event.getClass().getSimpleName());
}
}
@@ -7,26 +7,27 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import wtf.beatrice.hidekobot.objects.comparators.MessageCommandAliasesComparator;
import wtf.beatrice.hidekobot.util.Logger;
import java.util.*;
@Component
public class MessageCommandListener extends ListenerAdapter
{
// map storing command labels and command object alphabetically.
private final TreeMap<LinkedList<String>, MessageCommand> registeredCommands =
new TreeMap<LinkedList<String>, MessageCommand>(new MessageCommandAliasesComparator());
new TreeMap<>(new MessageCommandAliasesComparator());
// map commands and their categories.
// this is not strictly needed but it's better to have it so we avoid looping every time we need to check the cat.
LinkedHashMap<CommandCategory, LinkedList<MessageCommand>> commandCategories = new LinkedHashMap<>();
private final String commandRegex = "(?i)^(hideko|hde)\\b";
private static final String COMMAND_REGEX = "(?i)^(hideko|hde)\\b";
// (?i) -> case insensitive flag
// ^ -> start of string (not in middle of a sentence)
// \b -> the word has to end here
@@ -39,12 +40,16 @@ public class MessageCommandListener extends ListenerAdapter
public MessageCommand getRegisteredCommand(String label)
{
for(LinkedList<String> aliases : registeredCommands.keySet())
for (Map.Entry<LinkedList<String>, MessageCommand> entry : registeredCommands.entrySet())
{
LinkedList<String> aliases = entry.getKey();
for (String currentAlias : aliases)
{
if (label.equals(currentAlias))
{ return registeredCommands.get(aliases); }
{
return entry.getValue();
}
}
}
@@ -52,10 +57,9 @@ public class MessageCommandListener extends ListenerAdapter
}
public LinkedList<MessageCommand> getRegisteredCommands()
{ return new LinkedList<>(registeredCommands.values()); }
private final Logger logger = new Logger(MessageCommandListener.class);
{
return new LinkedList<>(registeredCommands.values());
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
@@ -67,11 +71,11 @@ public class MessageCommandListener extends ListenerAdapter
String eventMessage = event.getMessage().getContentRaw();
// check if the sent message matches the bot activation regex (prefix, name, ...)
if(!eventMessage.toLowerCase().matches(commandRegex + "((.|\\n)*)"))
if (!eventMessage.toLowerCase().matches("(?s)" + COMMAND_REGEX + ".*"))
return;
// generate args from the string
String argsString = eventMessage.replaceAll(commandRegex + "\\s*", "");
String argsString = eventMessage.replaceAll(COMMAND_REGEX + "\\s*", "");
// if no args were specified apart from the bot prefix
@@ -115,16 +119,13 @@ public class MessageCommandListener extends ListenerAdapter
{
Member member = event.getMember();
GuildChannel channel = event.getGuildChannel(); //todo: what about forum post
if(member != null)
{
if(!member.hasPermission(channel, requiredPermissions))
if (member != null && !member.hasPermission(channel, requiredPermissions))
{
event.getMessage()
.reply("You do not have permissions to run this command!")
.queue(); // todo prettier
// todo: queue message deletion in 15 seconds or so
return;
}
}
}
@@ -138,8 +139,7 @@ public class MessageCommandListener extends ListenerAdapter
argsString = argsString.replaceAll("^[\\S]+\\s*", "");
// pass all other arguments as a single argument as the first array element
commandArgs = new String[]{argsString};
}
else
} else
{
// copy all split arguments to the array, except from the command label
commandArgs = Arrays.copyOfRange(argsRaw, 1, argsRaw.length);
@@ -6,49 +6,51 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.util.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class MessageLogger extends ListenerAdapter
{
// this class only gets loaded as a listener if verbosity is set to true on startup.
private final static String guildChannelFormat = "[%guild%] [#%channel%] %user%: %message%";
private final static String dmFormat = "[DM] %user%: %message%";
private static final String GUILD_MESSAGE_LOG_FORMAT = "[%guild%] [#%channel%] %user%: %message%";
private static final String DIRECT_MESSAGE_LOG_FORMAT = "[DM] %user%: %message%";
private final Logger logger = new Logger(MessageLogger.class);
private static final Logger LOGGER = LoggerFactory.getLogger(MessageLogger.class);
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
String toLog = "";
String userName = event.getAuthor().getAsTag();
String userName = event.getAuthor().getName();
String message = event.getMessage().getContentDisplay();
if(event.getChannel() instanceof TextChannel)
if (event.getChannel() instanceof TextChannel channel)
{
String guildName = ((TextChannel) event.getChannel()).getGuild().getName();
String guildName = channel.getGuild().getName();
String channelName = event.getChannel().getName();
toLog = guildChannelFormat
toLog = GUILD_MESSAGE_LOG_FORMAT
.replace("%guild%", guildName)
.replace("%channel%", channelName);
}
else if(event.getChannel() instanceof PrivateChannel)
} else if (event.getChannel() instanceof PrivateChannel)
{
toLog = dmFormat;
toLog = DIRECT_MESSAGE_LOG_FORMAT;
}
toLog = toLog
.replace("%user%", userName)
.replace("%message%", message);
logger.log(toLog);
LOGGER.info(toLog);
if (!event.getMessage().getAttachments().isEmpty())
{
for (Message.Attachment atch : event.getMessage().getAttachments())
{
logger.log(atch.getUrl());
LOGGER.info(atch.getUrl());
}
}
}
@@ -2,18 +2,28 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.commands.base.Trivia;
@Component
public class SelectMenuInteractionListener extends ListenerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(SelectMenuInteractionListener.class);
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event)
{
switch (event.getComponentId().toLowerCase()) {
switch (event.getComponentId().toLowerCase())
{
// trivia
case "trivia_categories" -> Trivia.handleMenuSelection(event);
// error handling
default -> LOGGER.warn("Received unhandled {}", event.getClass().getSimpleName());
}
}
}
@@ -2,11 +2,13 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.SlashArgumentsCompleter;
import java.util.LinkedList;
import java.util.TreeMap;
@Component
public class SlashCommandCompletionListener extends ListenerAdapter
{
@@ -21,10 +23,15 @@ public class SlashCommandCompletionListener extends ListenerAdapter
}
public SlashArgumentsCompleter getRegisteredCompleter(String label)
{ return registeredCompleters.get(label); }
{
return registeredCompleters.get(label);
}
public LinkedList<SlashArgumentsCompleter> getRegisteredCompleters()
{ return new LinkedList<>(registeredCompleters.values()); }
{
return new LinkedList<>(registeredCompleters.values());
}
@Override
public void onCommandAutoCompleteInteraction(CommandAutoCompleteInteractionEvent event)
{
@@ -3,11 +3,13 @@ package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.LinkedList;
import java.util.TreeMap;
@Component
public class SlashCommandListener extends ListenerAdapter
{
@@ -21,10 +23,14 @@ public class SlashCommandListener extends ListenerAdapter
}
public SlashCommand getRegisteredCommand(String label)
{ return registeredCommands.get(label); }
{
return registeredCommands.get(label);
}
public LinkedList<SlashCommand> getRegisteredCommands()
{ return new LinkedList<>(registeredCommands.values()); }
{
return new LinkedList<>(registeredCommands.values());
}
@Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event)
@@ -4,8 +4,41 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Objects;
public record MessageResponse(@Nullable String content,
@Nullable MessageEmbed embed,
@Nullable ItemComponent... components) {
@Nullable ItemComponent... components)
{
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MessageResponse response = (MessageResponse) o;
return Objects.equals(content, response.content) &&
Objects.equals(embed, response.embed) &&
Arrays.equals(components, response.components);
}
@Override
public int hashCode()
{
int result = Objects.hash(content, embed);
result = 31 * result + Arrays.hashCode(components);
return result;
}
@Override
public String toString()
{
return "MessageResponse{" +
"content=" + content +
", embed=" + embed +
", components=" + Arrays.toString(components) +
'}';
}
}
@@ -9,10 +9,14 @@ public enum CommandCategory
;
private String emoji;
CommandCategory(String emoji)
{
this.emoji = emoji;
}
public String getEmoji() { return emoji; }
public String getEmoji()
{
return emoji;
}
}
@@ -70,9 +70,7 @@ public interface MessageCommand
*
* @param event the received message event. It should not be used for parsing message contents data as
* the arguments already account for it in a better way.
*
* @param label the command label that was used, taken from all available command aliases.
*
* @param args a pre-formatted list of arguments, excluding the bot prefix and the command name.
* This is useful because command logic won't have to change in case the bot prefix is changed,
* removed, or we switch to another method of triggering commands (ping, trigger words, ...).
@@ -12,6 +12,7 @@ public interface SlashArgumentsCompleter
* @return the command object.
*/
SlashCommand getCommand();
/**
* Run the argument-completion logic by parsing the event and replying accordingly.
*
@@ -6,13 +6,16 @@ import org.jetbrains.annotations.NotNull;
public class SlashArgumentsCompleterImpl implements SlashArgumentsCompleter
{
private final SlashCommand parentCommand;
public SlashArgumentsCompleterImpl(SlashCommand parentCommand)
{
this.parentCommand = parentCommand;
}
public SlashCommand getCommand()
{ return parentCommand; }
{
return parentCommand;
}
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event)
{
@@ -21,6 +21,7 @@ public interface SlashCommand
* @return the command data object.
*/
CommandData getSlashCommandData();
/**
* Run the command logic by parsing the event and replying accordingly.
*
@@ -8,17 +8,20 @@ public class SlashCommandImpl implements SlashCommand
{
@Override
public String getCommandName() {
public String getCommandName()
{
return getSlashCommandData().getName();
}
@Override
public CommandData getSlashCommandData() {
public CommandData getSlashCommandData()
{
return null;
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event) {
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.reply("Base command implementation").queue();
}
}
@@ -6,10 +6,12 @@ import java.util.LinkedList;
/**
* This class gets two linked lists, and compares their first value alphabetically.
*/
public class MessageCommandAliasesComparator implements Comparator<LinkedList<String>> {
public class MessageCommandAliasesComparator implements Comparator<LinkedList<String>>
{
@Override
public int compare(LinkedList<String> linkedList, LinkedList<String> t1) {
public int compare(LinkedList<String> linkedList, LinkedList<String> t1)
{
if (linkedList.isEmpty()) return 0;
if (t1.isEmpty()) return 0;
@@ -7,10 +7,12 @@ import java.util.Comparator;
/**
* This class gets two trivia categories, and compares them by their name.
*/
public class TriviaCategoryComparator implements Comparator<TriviaCategory> {
public class TriviaCategoryComparator implements Comparator<TriviaCategory>
{
@Override
public int compare(TriviaCategory o1, TriviaCategory o2) {
public int compare(TriviaCategory o1, TriviaCategory o2)
{
return CharSequence.compare(o1.categoryName(), o2.categoryName());
}
}
@@ -7,10 +7,12 @@ import java.util.Comparator;
/**
* This class gets two trivia scores, and compares their score.
*/
public class TriviaScoreComparator implements Comparator<TriviaScore> {
public class TriviaScoreComparator implements Comparator<TriviaScore>
{
@Override
public int compare(TriviaScore o1, TriviaScore o2) {
public int compare(TriviaScore o1, TriviaScore o2)
{
return Integer.compare(o2.getScore(), o1.getScore()); // inverted, because higher number should come first
}
}
@@ -1,5 +1,6 @@
package wtf.beatrice.hidekobot.objects.fun;
public record TriviaCategory(String categoryName, int categoryId) {
public record TriviaCategory(String categoryName, int categoryId)
{
}
@@ -3,6 +3,7 @@ package wtf.beatrice.hidekobot.objects.fun;
import java.util.List;
public record TriviaQuestion(String question, String correctAnswer,
List<String> wrongAnswers) {
List<String> wrongAnswers)
{
}
@@ -18,14 +18,20 @@ public class TriviaScore
score += add;
}
public int getScore() { return score; }
public int getScore()
{
return score;
}
public User getUser() { return user; }
public User getUser()
{
return user;
}
@Override
public String toString()
{
return "[" + user.getAsTag() + "," + score + "]";
return "[" + user.getName() + "," + score + "]";
}
}
@@ -0,0 +1,8 @@
package wtf.beatrice.hidekobot.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import wtf.beatrice.hidekobot.entities.CommandRunner;
public interface CommandRunnerRepository extends JpaRepository<CommandRunner, String>
{
}
@@ -0,0 +1,8 @@
package wtf.beatrice.hidekobot.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import wtf.beatrice.hidekobot.entities.PendingDisabledMessage;
public interface PendingDisabledMessageRepository extends JpaRepository<PendingDisabledMessage, String>
{
}
@@ -0,0 +1,8 @@
package wtf.beatrice.hidekobot.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import wtf.beatrice.hidekobot.entities.UrbanDictionaryEntry;
public interface UrbanDictionaryRepository extends JpaRepository<UrbanDictionaryEntry, String>
{
}
@@ -1,37 +1,40 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.util.Logger;
import wtf.beatrice.hidekobot.services.CommandService;
import wtf.beatrice.hidekobot.services.DatabaseService;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class ExpiredMessageTask implements Runnable {
public class ExpiredMessageTask implements Runnable
{
private final DatabaseService databaseService;
private final CommandService commandService;
private final DateTimeFormatter formatter;
private final Logger logger;
private DatabaseSource databaseSource;
private static final Logger LOGGER = LoggerFactory.getLogger(ExpiredMessageTask.class);
public ExpiredMessageTask()
public ExpiredMessageTask(DatabaseService databaseService,
CommandService commandService)
{
this.databaseService = databaseService;
this.commandService = commandService;
String format = Cache.getExpiryTimestampFormat();
formatter = DateTimeFormatter.ofPattern(format);
databaseSource = Cache.getDatabaseSource();
logger = new Logger(getClass());
}
@Override
public void run() {
public void run()
{
databaseSource = Cache.getDatabaseSource();
if(databaseSource == null) return;
List<String> expiringMessages = Cache.getDatabaseSource().getQueuedExpiringMessages();
List<String> expiringMessages = databaseService.getQueuedExpiringMessages();
if (expiringMessages == null || expiringMessages.isEmpty()) return;
LocalDateTime now = LocalDateTime.now();
@@ -39,13 +42,13 @@ public class ExpiredMessageTask implements Runnable {
for (String messageId : expiringMessages)
{
if(Cache.isVerbose()) logger.log("expired check: " + messageId);
if (Cache.isVerbose()) LOGGER.info("expired check: {}", messageId);
String expiryTimestamp = databaseSource.getQueuedExpiringMessageExpiryDate(messageId);
if(expiryTimestamp == null || expiryTimestamp.equals("")) // if missing timestamp
String expiryTimestamp = databaseService.getQueuedExpiringMessageExpiryDate(messageId);
if (expiryTimestamp == null || expiryTimestamp.isEmpty()) // if missing timestamp
{
// count it as already expired
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
// move on to next message
continue;
}
@@ -54,8 +57,8 @@ public class ExpiredMessageTask implements Runnable {
LocalDateTime expiryDate = LocalDateTime.parse(expiryTimestamp, formatter);
if (now.isAfter(expiryDate))
{
if(Cache.isVerbose()) logger.log("expired: " + messageId);
CommandUtil.disableExpired(messageId);
if (Cache.isVerbose()) LOGGER.info("expired: {}", messageId);
commandService.disableExpired(messageId);
}
}
@@ -1,7 +1,8 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.Logger;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -9,14 +10,7 @@ import java.net.URL;
public class HeartBeatTask implements Runnable
{
private final Logger logger;
public HeartBeatTask()
{
logger = new Logger(getClass());
}
private static final Logger LOGGER = LoggerFactory.getLogger(HeartBeatTask.class);
@Override
public void run()
@@ -24,7 +18,8 @@ public class HeartBeatTask implements Runnable
String urlString = Cache.getFullHeartBeatLink();
if (urlString == null || urlString.isEmpty()) return;
try {
try
{
URL heartbeatUrl = new URL(urlString);
@@ -37,15 +32,15 @@ public class HeartBeatTask implements Runnable
if (200 <= responseCode && responseCode < 300)
{
// only log ok response codes when verbosity is enabled
if(Cache.isVerbose()) logger.log("Heartbeat response code: " + responseCode);
}
else
if (Cache.isVerbose()) LOGGER.info("Heartbeat response code: {}", responseCode);
} else
{
logger.log("Heartbeat returned problematic response code: " + responseCode);
LOGGER.error("Heartbeat returned problematic response code: {}", responseCode);
}
} catch (IOException e) {
logger.log("Error while trying to push heartbeat: " + e.getMessage());
} catch (IOException e)
{
LOGGER.error("Error while trying to push heartbeat", e);
}
}
@@ -0,0 +1,30 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
/**
* This runnable pulls a random seed from random.org and used it to feed a SecureRandom,
* if the integration is enabled.
* <br/>
* This is necessary since we are not directly accessing random.org for each random number we
* need, and thus, only the seed is random - not the algorithm applied to it to compute the numbers.
*/
public class RandomOrgSeedTask implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(RandomOrgSeedTask.class);
@Override
public void run()
{
if (RandomUtil.isRandomOrgKeyValid())
{
if (Cache.isVerbose()) LOGGER.info("Updating Random seed from random.org...");
RandomUtil.initRandomOrg();
if (Cache.isVerbose()) LOGGER.info("Random.org seed updated!");
}
}
}
@@ -1,12 +0,0 @@
package wtf.beatrice.hidekobot.runnables;
import wtf.beatrice.hidekobot.Cache;
public class RandomSeedTask implements Runnable
{
@Override
public void run() {
Cache.setRandomSeed(System.currentTimeMillis());
}
}
@@ -22,7 +22,8 @@ public class StatusUpdateTask implements Runnable
);
@Override
public void run() {
public void run()
{
int randomPos = RandomUtil.getRandomNumber(0, statuses.size() - 1);
String status = statuses.get(randomPos) + " | " + Cache.getBotPrefix() + " help";
HidekoBot.getAPI().getPresence().setActivity(Activity.playing(status));
@@ -13,13 +13,18 @@ import wtf.beatrice.hidekobot.objects.comparators.TriviaScoreComparator;
import wtf.beatrice.hidekobot.objects.fun.TriviaCategory;
import wtf.beatrice.hidekobot.objects.fun.TriviaQuestion;
import wtf.beatrice.hidekobot.objects.fun.TriviaScore;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.services.CommandService;
import wtf.beatrice.hidekobot.services.DatabaseService;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
public class TriviaTask implements Runnable
{
private final DatabaseService databaseService;
private final CommandService commandService;
private final User author;
private final MessageChannel channel;
@@ -33,11 +38,17 @@ public class TriviaTask implements Runnable
private int iteration = 0;
public TriviaTask(User author, MessageChannel channel, TriviaCategory category)
public TriviaTask(User author,
MessageChannel channel,
TriviaCategory category,
DatabaseService databaseService,
CommandService commandService)
{
this.author = author;
this.channel = channel;
this.category = category;
this.databaseService = databaseService;
this.commandService = commandService;
triviaJson = Trivia.fetchJson(Trivia.getTriviaLink(category.categoryId()));
questions = Trivia.parseQuestions(triviaJson); //todo: null check, rate limiting...
@@ -55,7 +66,7 @@ public class TriviaTask implements Runnable
if (previousMessage != null)
{
// todo: we shouldn't use this method, since it messes with the database... look at coin reflip
CommandUtil.disableExpired(previousMessage.getId());
commandService.disableExpired(previousMessage.getId());
String previousCorrectAnswer = questions.get(iteration - 1).correctAnswer();
@@ -93,12 +104,14 @@ public class TriviaTask implements Runnable
previousScore = score;
topScore = score;
pos = 1;
} else {
} else
{
if (score != previousScore) pos++;
}
if (pos == 1) winners.add(user);
else {
else
{
othersBuilder.append("\n").append(pos)
.append(" | ").append(user)
.append(": ").append(score).append(" points");
@@ -113,7 +126,8 @@ public class TriviaTask implements Runnable
if (i + 1 != winners.size())
{
winnersBuilder.append(", "); // separate with comma except on last run
} else {
} else
{
winnersBuilder.append(": ").append(topScore).append(" points \uD83C\uDF89");
}
}
@@ -190,12 +204,12 @@ public class TriviaTask implements Runnable
.complete();
Cache.getDatabaseSource().trackRanCommandReply(previousMessage, author);
databaseService.trackRanCommandReply(previousMessage, author);
// todo: ^ we should get rid of this tracking, since we don't need to know who started the trivia.
// todo: however, for now, that's the only way to avoid a thread-locking scenario as some data is
// todo: only stored in that table. this should be solved when we merge / fix the two main tables.
// todo: then, we can remove this instruction.
Cache.getDatabaseSource().queueDisabling(previousMessage);
databaseService.queueDisabling(previousMessage);
iteration++;
}
@@ -1,4 +1,5 @@
package wtf.beatrice.hidekobot.util;
package wtf.beatrice.hidekobot.services;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
@@ -11,18 +12,29 @@ import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
import net.dv8tion.jda.api.requests.RestAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.datasources.DatabaseSource;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.ArrayList;
import java.util.List;
public class CommandUtil
@Component
public class CommandService
{
private static final Logger logger = new Logger(CommandUtil.class);
private static final Logger LOGGER = LoggerFactory.getLogger(CommandService.class);
private final DatabaseService databaseService;
public CommandService(@Autowired DatabaseService databaseService)
{
this.databaseService = databaseService;
}
/**
* Function to delete a message when a user clicks the "delete" button attached to that message.
@@ -30,17 +42,37 @@ public class CommandUtil
*
* @param event the button interaction event.
*/
public static void delete(ButtonInteractionEvent event)
public void deleteUserLinkedMessage(ButtonInteractionEvent event)
{
// check if the user interacting is the same one who ran the command
if (!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId()))) {
if (!databaseService.isUserTrackedFor(event.getUser().getId(), event.getMessageId()))
{
event.reply("❌ You did not run this command!").setEphemeral(true).queue();
return;
}
// delete the message
event.getInteraction().getMessage().delete().queue();
// no need to manually untrack it from database, it will be purged on the next planned check.
// Acknowledge immediately so the interaction token stays valid
event.deferEdit().queue(hook -> {
// Try deleting via the interaction webhook (works for original interaction responses)
hook.deleteOriginal().queue(
success -> { /* optional: databaseService.untrackExpiredMessage(event.getMessageId()); */ },
failure -> {
// Fallback to channel delete (works even if webhook token expired)
event.getChannel().deleteMessageById(event.getMessageId()).queue(
null,
__ -> { /* ignore if already deleted */ }
);
}
);
},
failure -> {
// If we failed to acknowledge (interaction already expired), try channel delete anyway
event.getChannel().deleteMessageById(event.getMessageId()).queue(
null,
__ -> { /* ignore if already deleted */ }
);
}
);
}
@@ -51,13 +83,15 @@ public class CommandUtil
*
* @param force a boolean specifying if the update should be forced even if no differences were found.
*/
public static void updateSlashCommands(boolean force)
public void updateSlashCommands(boolean force)
{
// populate commands list from registered commands
List<CommandData> allCommands = new ArrayList<>();
for (SlashCommand cmd : Cache.getSlashCommandListener().getRegisteredCommands())
{ allCommands.add(cmd.getSlashCommandData()); }
{
allCommands.add(cmd.getSlashCommandData());
}
JDA jdaInstance = HidekoBot.getAPI();
@@ -133,13 +167,13 @@ public class CommandUtil
}
}
logger.log("Found " + registeredCommands.size() + " commands.");
LOGGER.info("Found {} commands.", registeredCommands.size());
if (update)
{
// send updated command list.
jdaInstance.updateCommands().addCommands(allCommands).queue();
logger.log("Commands updated. New total: " + allCommands.size() + ".");
LOGGER.info("Commands updated. New total: {}.", allCommands.size());
}
}
@@ -149,49 +183,46 @@ public class CommandUtil
*
* @param messageId the message id to disable.
*/
public static void disableExpired(String messageId)
public void disableExpired(String messageId)
{
DatabaseSource databaseSource = Cache.getDatabaseSource();
String channelId = databaseSource.getQueuedExpiringMessageChannel(messageId);
String channelId = databaseService.getQueuedExpiringMessageChannel(messageId);
// todo: warning, the following method + related if check are thread-locking.
// todo: we should probably merge the two tables somehow, since they have redundant information.
ChannelType msgChannelType = databaseSource.getTrackedMessageChannelType(messageId);
ChannelType msgChannelType = databaseService.getTrackedMessageChannelType(messageId);
MessageChannel textChannel = null;
// this should never happen, but only message channels are supported.
if (!msgChannelType.isMessage())
{
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
return;
}
// if this is a DM
if (!(msgChannelType.isGuild()))
{
String userId = databaseSource.getTrackedReplyUserId(messageId);
String userId = databaseService.getTrackedReplyUserId(messageId);
User user = userId == null ? null : HidekoBot.getAPI().retrieveUserById(userId).complete();
if (user == null)
{
// if user is not found, consider it expired
// (deleted profile, or blocked the bot)
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
return;
}
textChannel = user.openPrivateChannel().complete();
}
else
} else
{
String guildId = databaseSource.getQueuedExpiringMessageGuild(messageId);
String guildId = databaseService.getQueuedExpiringMessageGuild(messageId);
Guild guild = guildId == null ? null : HidekoBot.getAPI().getGuildById(guildId);
if (guild == null)
{
// if guild is not found, consider it expired
// (server was deleted or bot was kicked)
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
return;
}
textChannel = guild.getTextChannelById(channelId);
@@ -201,20 +232,20 @@ public class CommandUtil
{
// if channel is not found, count it as expired
// (channel was deleted or bot permissions restricted)
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
return;
}
RestAction<Message> retrieveAction = textChannel.retrieveMessageById(messageId);
if(Cache.isVerbose()) logger.log("cleaning up: " + messageId);
if (Cache.isVerbose()) LOGGER.info("cleaning up: {}", messageId);
retrieveAction.queue(
message -> {
if (message == null)
{
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
return;
}
@@ -227,9 +258,9 @@ public class CommandUtil
}
message.editMessageComponents(newComponents).queue();
databaseSource.untrackExpiredMessage(messageId);
databaseService.untrackExpiredMessage(messageId);
},
error -> databaseSource.untrackExpiredMessage(messageId));
error -> databaseService.untrackExpiredMessage(messageId));
}
}
@@ -0,0 +1,183 @@
package wtf.beatrice.hidekobot.services;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.entities.CommandRunner;
import wtf.beatrice.hidekobot.entities.PendingDisabledMessage;
import wtf.beatrice.hidekobot.entities.UrbanDictionaryEntry;
import wtf.beatrice.hidekobot.repositories.CommandRunnerRepository;
import wtf.beatrice.hidekobot.repositories.PendingDisabledMessageRepository;
import wtf.beatrice.hidekobot.repositories.UrbanDictionaryRepository;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
@Transactional
public class DatabaseService
{
private final PendingDisabledMessageRepository pendingRepo;
private final CommandRunnerRepository runnerRepo;
private final UrbanDictionaryRepository urbanRepo;
public DatabaseService(PendingDisabledMessageRepository p, CommandRunnerRepository c, UrbanDictionaryRepository u)
{
this.pendingRepo = p;
this.runnerRepo = c;
this.urbanRepo = u;
}
// trackRanCommandReply
public void trackRanCommandReply(Message message, User user)
{
String userId = user.getId();
String guildId = message.getChannelType().isGuild() ? message.getGuild().getId() : userId;
CommandRunner row = new CommandRunner();
row.setMessageId(message.getId());
row.setGuildId(guildId);
row.setChannelId(message.getChannel().getId());
row.setUserId(userId);
row.setChannelType(message.getChannelType().name());
runnerRepo.save(row);
}
public boolean isUserTrackedFor(String userId, String messageId)
{
return runnerRepo.findById(messageId)
.map(r -> userId.equals(r.getUserId()))
.orElse(false);
}
public ChannelType getTrackedMessageChannelType(String messageId)
{
return runnerRepo.findById(messageId)
.map(r -> ChannelType.valueOf(r.getChannelType()))
.orElse(null);
}
public String getTrackedReplyUserId(String messageId)
{
return runnerRepo.findById(messageId)
.map(CommandRunner::getUserId)
.orElse(null);
}
public void queueDisabling(Message message)
{
String guildId = message.getChannelType().isGuild() ? message.getGuild().getId() : "PRIVATE";
LocalDateTime expiry = LocalDateTime.now().plusSeconds(Cache.getExpiryTimeSeconds());
String formatted = DateTimeFormatter.ofPattern(Cache.getExpiryTimestampFormat()).format(expiry);
PendingDisabledMessage row = new PendingDisabledMessage();
row.setMessageId(message.getId());
row.setChannelId(message.getChannel().getId());
row.setGuildId(guildId);
row.setExpiryTimestamp(formatted);
pendingRepo.save(row);
}
public List<String> getQueuedExpiringMessages()
{
return pendingRepo.findAll()
.stream()
.map(PendingDisabledMessage::getMessageId)
.toList();
}
public void untrackExpiredMessage(String messageId)
{
pendingRepo.deleteById(messageId);
runnerRepo.deleteById(messageId);
urbanRepo.deleteById(messageId);
}
public String getQueuedExpiringMessageExpiryDate(String messageId)
{
return pendingRepo.findById(messageId).map(PendingDisabledMessage::getExpiryTimestamp).orElse(null);
}
public String getQueuedExpiringMessageChannel(String messageId)
{
return pendingRepo.findById(messageId).map(PendingDisabledMessage::getChannelId).orElse(null);
}
public String getQueuedExpiringMessageGuild(String messageId)
{
return pendingRepo.findById(messageId).map(PendingDisabledMessage::getGuildId).orElse(null);
}
public void trackUrban(String meanings, String examples, String contributors, String dates, Message message, String term)
{
UrbanDictionaryEntry e = new UrbanDictionaryEntry();
e.setMessageId(message.getId());
e.setPage(0);
e.setMeanings(meanings);
e.setExamples(examples);
e.setContributors(contributors);
e.setDates(dates);
e.setTerm(term);
urbanRepo.save(e);
}
public int getUrbanPage(String messageId)
{
return urbanRepo.findById(messageId)
.map(UrbanDictionaryEntry::getPage)
.orElse(0);
}
public String getUrbanMeanings(String messageId)
{
return urbanRepo.findById(messageId).map(UrbanDictionaryEntry::getMeanings).orElse(null);
}
public String getUrbanExamples(String messageId)
{
return urbanRepo.findById(messageId).map(UrbanDictionaryEntry::getExamples).orElse(null);
}
public String getUrbanContributors(String messageId)
{
return urbanRepo.findById(messageId).map(UrbanDictionaryEntry::getContributors).orElse(null);
}
public String getUrbanDates(String messageId)
{
return urbanRepo.findById(messageId).map(UrbanDictionaryEntry::getDates).orElse(null);
}
public String getUrbanTerm(String messageId)
{
return urbanRepo.findById(messageId).map(UrbanDictionaryEntry::getTerm).orElse(null);
}
public void setUrbanPage(String messageId, int page)
{
urbanRepo.findById(messageId).ifPresent(e -> {
e.setPage(page);
urbanRepo.save(e);
});
}
public void resetExpiryTimestamp(String messageId)
{
pendingRepo.findById(messageId).ifPresent(row -> {
String formatted = DateTimeFormatter
.ofPattern(Cache.getExpiryTimestampFormat())
.format(LocalDateTime.now().plusSeconds(Cache.getExpiryTimeSeconds()));
row.setExpiryTimestamp(formatted);
pendingRepo.save(row);
});
}
}
@@ -4,18 +4,41 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class FormatUtil
{
private FormatUtil()
{
throw new IllegalStateException("Utility class");
}
// cosmetic string to print on startup.
private static final String LOGO = """
\s
██╗░░██╗██╗██████╗░███████╗██╗░░██╗░█████╗░
██║░░██║██║██╔══██╗██╔════╝██║░██╔╝██╔══██╗
███████║██║██║░░██║█████╗░░█████═╝░██║░░██║
██╔══██║██║██║░░██║██╔══╝░░██╔═██╗░██║░░██║
██║░░██║██║██████╔╝███████╗██║░╚██╗╚█████╔╝
╚═╝░░╚═╝╚═╝╚═════╝░╚══════╝╚═╝░░╚═╝░╚════╝░
\s""";
/**
* Returns ASCII art saying the bot name.
*
* @return a String containing the logo
*/
public static String getLogo()
{
return LOGO;
}
/**
* Generate a nicely formatted time-diff String that omits unnecessary data
* (e.g. 0 days, 0 hours, 4 minutes, 32 seconds -> 4m 32s)
@@ -40,32 +63,28 @@ public class FormatUtil
*/
public static String getNiceDuration(Duration duration)
{
long seconds = duration.toSecondsPart();
long minutes = duration.toMinutesPart();
long hours = duration.toHoursPart();
long days = duration.toDays();
long hours = duration.toHoursPart();
long minutes = duration.toMinutesPart();
long seconds = duration.toSecondsPart();
StringBuilder uptimeStringBuilder = new StringBuilder();
if(days == 0)
StringBuilder sb = new StringBuilder();
if (days > 0)
{
if(hours == 0)
sb.append(days).append("d ");
sb.append(hours).append("h ");
sb.append(minutes).append("m ");
} else if (hours > 0)
{
if(minutes == 0)
{} else { // i know this if has an empty body but i feel like this reads better
uptimeStringBuilder.append(minutes).append("m ");
sb.append(hours).append("h ");
sb.append(minutes).append("m ");
} else if (minutes > 0)
{
sb.append(minutes).append("m ");
}
} else {
uptimeStringBuilder.append(hours).append("h ");
uptimeStringBuilder.append(minutes).append("m ");
}
} else {
uptimeStringBuilder.append(days).append("d ");
uptimeStringBuilder.append(hours).append("h ");
uptimeStringBuilder.append(minutes).append("m ");
}
uptimeStringBuilder.append(seconds).append("s ");
sb.append(seconds).append("s");
return uptimeStringBuilder.toString();
return sb.toString();
}
/**
@@ -87,8 +106,11 @@ public class FormatUtil
eg: 3d, 33hours, 32048dojg, 3d2h5m22s.
it does not match if the [digits and characters] blocks are missing.
eg: 33, asd, 3g5hj, 4 asd.
{1,10} is used to limit the size of the input to parse, to avoid stack overflows.
no one should be typing more than 10 arguments, or more than 10 digits for a single argument anyway.
*/
if(!duration.matches("(\\d+[a-zA-Z]+)+"))
if (!duration.matches("(\\d{1,10}[a-zA-Z]{1,10}){1,10}"))
return null;
String[] durationTimes = duration.split("[a-zA-Z]+");
@@ -121,7 +143,7 @@ public class FormatUtil
// we won't do any sanitization, because this is a private method, and
// we are only accessing it with things that we know for sure are already sanitized.
unitName = unitName.toLowerCase();
TemporalUnit timeUnit = null;
TemporalUnit timeUnit;
/*
parsing table
@@ -139,6 +161,7 @@ public class FormatUtil
case "m", "mi", "min", "minute", "minutes" -> timeUnit = ChronoUnit.MINUTES;
case "h", "ho", "hr", "hour", "hours" -> timeUnit = ChronoUnit.HOURS;
case "d", "day", "days" -> timeUnit = ChronoUnit.DAYS;
default -> timeUnit = null;
}
return timeUnit;
@@ -1,84 +0,0 @@
package wtf.beatrice.hidekobot.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Logger
{
// cosmetic string to print on startup.
private String logo =
"██╗░░██╗██╗██████╗░███████╗██╗░░██╗░█████╗░\n" +
"██║░░██║██║██╔══██╗██╔════╝██║░██╔╝██╔══██╗\n" +
"███████║██║██║░░██║█████╗░░█████═╝░██║░░██║\n" +
"██╔══██║██║██║░░██║██╔══╝░░██╔═██╗░██║░░██║\n" +
"██║░░██║██║██████╔╝███████╗██║░╚██╗╚█████╔╝\n" +
"╚═╝░░╚═╝╚═╝╚═════╝░╚══════╝╚═╝░░╚═╝░╚════╝░";
// objects that we need to have for a properly formatted message
private String className;
private final String format = "[%date% %time%] [%class%] %message%";
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd");
private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// when initializing a new logger, save variables in that instance
public Logger(Class logClass)
{
className = logClass.getSimpleName();
}
/**
* Logs a message to console, following a specific format.
*
* @param message the message to log
*/
public void log(String message)
{
LocalDateTime now = LocalDateTime.now();
String currentDate = dateFormatter.format(now);
String currentTime = timeFormatter.format(now);
logRaw(format
.replace("%date%", currentDate)
.replace("%time%", currentTime)
.replace("%class%", className)
.replace("%message%", message));
}
/**
* Logs a message to console, after delaying it.
*
* @param message the message to log
* @param delay the time to wait before logging, in seconds
*/
public void log(String message, int delay)
{
// create a new scheduled executor with an anonymous runnable...
//... after waiting <delay> seconds.
Executors.newSingleThreadScheduledExecutor().schedule(() -> log(message), delay, TimeUnit.SECONDS);
}
/**
* Prints a message to console without any formatting.
*
* @param message the message to log
*/
public void logRaw(String message)
{
System.out.println(message);
}
/**
* Returns ASCII art saying the bot name.
*
* @return a String containing the logo
*/
public String getLogo()
{
return logo;
}
}
@@ -1,9 +1,24 @@
package wtf.beatrice.hidekobot.util;
import org.random.util.RandomOrgRandom;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.datasources.ConfigurationEntry;
import java.security.SecureRandom;
import java.util.Random;
public class RandomUtil
{
private RandomUtil()
{
throw new IllegalStateException("Utility class");
}
// the Random instance that we should always use when looking for an RNG based thing.
// the seed is updated periodically, if the random.org integration is enabled.
private static Random randomInstance = new SecureRandom();
/**
* Returns a random integer picked in a range.
*
@@ -26,9 +41,44 @@ public class RandomUtil
int difference = (max - min) + 1;
// find a number between 0 and our range (eg. 5 -> 8 = 0 -> 3)
int randomTemp = Cache.getRandom().nextInt(difference);
int randomTemp = getRandom().nextInt(difference);
// add the minimum value, so we are sure to be in the original range (0->5, 1->6, 2->7, 3->8)
return randomTemp + min;
}
public static Random getRandom()
{
return randomInstance;
}
public static void initRandomOrg()
{
/* we use the random.org instance to generate 160 random bytes.
then, we're feeding those 160 bytes as a seed for a SecureRandom.
this is preferred to calling the RandomOrgRandom directly every time,
because it has to query the api and (1) takes a long time, especially with big
dice rolls, and (2) you'd run in the limits extremely quickly if the bot
was run publicly for everyone to use.
*/
String apiKey = Cache.getRandomOrgApiKey();
RandomOrgRandom randomOrg = new RandomOrgRandom(apiKey);
byte[] randomBytes = new byte[160];
randomOrg.nextBytes(randomBytes);
randomInstance = new SecureRandom(randomBytes);
}
public static boolean isRandomOrgKeyValid()
{
String apiKey = Cache.getRandomOrgApiKey();
return apiKey != null &&
!apiKey.isEmpty() &&
!apiKey.equals(ConfigurationEntry.RANDOM_ORG_API_KEY.getDefaultValue());
}
}
@@ -10,29 +10,38 @@ import java.util.List;
public class SerializationUtil
{
public static String serializeBase64(List dataList) {
private SerializationUtil()
{
throw new IllegalStateException("Utility class");
}
public static <T> String serializeBase64(List<T> dataList)
{
try (ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream so = new ObjectOutputStream(bo)) {
ObjectOutputStream so = new ObjectOutputStream(bo))
{
so.writeObject(dataList);
so.flush();
return Base64.getEncoder().encodeToString(bo.toByteArray());
}
catch (IOException e) {
} catch (IOException e)
{
throw new SerializationException("Error during serialization", e);
}
}
public static LinkedList deserializeBase64(String dataStr) {
public static <T> LinkedList<T> deserializeBase64(String dataStr)
{
byte[] b = Base64.getDecoder().decode(dataStr);
ByteArrayInputStream bi = new ByteArrayInputStream(b);
ObjectInputStream si;
try {
try
{
si = new ObjectInputStream(bi);
return LinkedList.class.cast(si.readObject());
}
catch (IOException | ClassNotFoundException e) {
} catch (IOException | ClassNotFoundException e)
{
throw new SerializationException("Error during deserialization", e);
}
}

Some files were not shown because too many files have changed in this diff Show More