435 Commits

Author SHA1 Message Date
f3ec956c7a Merge pull request 'Update dependency org.yaml:snakeyaml to v2.5' (#59) from renovate/org.yaml-snakeyaml-2.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #59
2025-11-07 11:19:32 +01:00
0fc8beacf8 Merge pull request 'Update dependency org.junit.jupiter:junit-jupiter-api to v6' (#60) from renovate/org.junit.jupiter-junit-jupiter-api-6.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #60
2025-11-07 11:19:24 +01:00
ecbf4080ae Update dependency org.junit.jupiter:junit-jupiter-api to v6
All checks were successful
continuous-integration/drone/pr Build is passing
2025-11-06 17:01:08 +00:00
e1215cfa25 Update dependency org.yaml:snakeyaml to v2.5
All checks were successful
continuous-integration/drone/pr Build is passing
2025-11-06 17:01:03 +00:00
6f2dd2a220 Merge pull request 'Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.12.0' (#57) from renovate/org.apache.maven.plugins-maven-javadoc-plugin-3.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #57
2025-11-06 16:41:54 +01:00
ff8efba025 Merge pull request 'Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5.2.0.4988' (#58) from renovate/org.sonarsource.scanner.maven-sonar-maven-plugin-5.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #58
2025-11-06 16:41:27 +01:00
d8c1435416 Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5.2.0.4988
All checks were successful
continuous-integration/drone/pr Build is passing
2025-11-06 15:37:33 +00:00
eb3178a706 Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.12.0
All checks were successful
continuous-integration/drone/pr Build is passing
2025-11-06 15:37:30 +00:00
b431ca4eae Merge pull request 'Update dependency org.owasp:dependency-check-maven to v12.1.8' (#49) from renovate/org.owasp-dependency-check-maven-12.x into main
Reviewed-on: #49
2025-11-06 16:17:11 +01:00
ff9a6d9e14 Merge pull request 'Update dependency com.google.protobuf:protobuf-java to v4.33.0' (#55) from renovate/com.google.protobuf-protobuf-java-4.x into main
Reviewed-on: #55
2025-11-06 16:16:35 +01:00
ed49a6de32 Merge pull request 'Update dependency commons-codec:commons-codec to v1.20.0' (#53) from renovate/commons-codec-commons-codec-1.x into main
Reviewed-on: #53
2025-11-06 16:16:24 +01:00
31c2f22c40 Merge pull request 'Update dependency net.dv8tion:JDA to v5.6.1' (#50) from renovate/net.dv8tion-jda-5.x into main
Reviewed-on: #50
2025-11-06 16:15:28 +01:00
475864389c Merge pull request 'Update dependency org.xerial:sqlite-jdbc to v3.51.0.0' (#51) from renovate/org.xerial-sqlite-jdbc-3.x into main
Reviewed-on: #51
2025-11-06 16:15:17 +01:00
195400fae8 Merge pull request 'Update dependency org.junit.jupiter:junit-jupiter-api to v5.14.1' (#48) from renovate/org.junit.jupiter-junit-jupiter-api-5.x into main
Reviewed-on: #48
2025-11-06 16:14:56 +01:00
1227fff1b1 Merge pull request 'Update dependency org.jsoup:jsoup to v1.21.2' (#44) from renovate/org.jsoup-jsoup-1.x into main
Reviewed-on: #44
2025-11-06 16:13:57 +01:00
8800a0fea4 Merge pull request 'Update dependency com.google.code.gson:gson to v2.13.2' (#54) from renovate/com.google.code.gson-gson-2.x into main
Reviewed-on: #54
2025-11-06 16:13:16 +01:00
884d1376e1 Merge pull request 'Update dependency org.apache.commons:commons-text to v1.14.0' (#56) from renovate/org.apache.commons-commons-text-1.x into main
Reviewed-on: #56
2025-11-06 16:04:17 +01:00
89bf70c388 Update dependency org.apache.commons:commons-text to v1.14.0 2025-11-06 14:03:41 +00:00
0499a24ae7 Update dependency org.xerial:sqlite-jdbc to v3.51.0.0 2025-11-06 13:44:19 +00:00
e54ce77e43 Update dependency org.junit.jupiter:junit-jupiter-api to v5.14.1 2025-11-06 13:44:17 +00:00
2df821b28b Update dependency org.jsoup:jsoup to v1.21.2 2025-11-06 13:44:15 +00:00
a23b198531 Update dependency commons-codec:commons-codec to v1.20.0 2025-11-06 13:44:12 +00:00
ba369a9ea1 Update dependency com.google.protobuf:protobuf-java to v4.33.0 2025-11-06 13:44:10 +00:00
1f40450920 Update dependency org.owasp:dependency-check-maven to v12.1.8 2025-11-06 13:44:08 +00:00
51f5b57f47 Update dependency com.google.code.gson:gson to v2.13.2 2025-11-06 13:44:06 +00:00
fd2970fa59 cleanup and reformat
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-05 00:06:35 +02:00
14b54501fd update drone
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-04 23:42:30 +02:00
1928cfe858 update readme
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-04 23:41:54 +02:00
0b7880af88 fix drone
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-04 23:36:49 +02:00
fc00467059 Update dependency net.dv8tion:JDA to v5.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
2025-06-08 19:00:28 +00:00
f1969c2043 Update README.MD
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-01 21:02:56 +02:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #47
2025-06-01 20:25:10 +02:00
4ce9acd428 Merge pull request 'Update dependency org.json:json to v20250517' (#46) from renovate/org.json-json-20250517.x into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #46
2025-06-01 20:25:04 +02:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #45
2025-06-01 20:24:56 +02:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #43
2025-06-01 20:24:46 +02:00
f87854459f Merge pull request 'Update dependency net.dv8tion:JDA to v5.5.1' (#42) from renovate/net.dv8tion-jda-5.x into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #42
2025-06-01 20:24:31 +02:00
72c4ae2133 Update dependency org.junit.jupiter:junit-jupiter-api to v5.13.0
All checks were successful
continuous-integration/drone/pr Build is passing
2025-05-30 11:00:27 +00:00
7f4ca6aa8e Update dependency com.google.protobuf:protobuf-java to v4.31.1
All checks were successful
continuous-integration/drone/pr Build is passing
2025-05-28 20:00:24 +00:00
c3793aa159 Update dependency org.json:json to v20250517
All checks were successful
continuous-integration/drone/pr Build is passing
2025-05-17 14:00:27 +00:00
63fc1feaea Update dependency net.dv8tion:JDA to v5.5.1
All checks were successful
continuous-integration/drone/pr Build is passing
2025-05-03 12:00:25 +00:00
71d646ff69 Update dependency com.google.code.gson:gson to v2.13.1
All checks were successful
continuous-integration/drone/pr Build is passing
2025-04-24 02:00:23 +00:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #41
2025-04-13 00:55:18 +02:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #40
2025-04-13 00:54:58 +02:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #39
2025-04-13 00:54:46 +02:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #38
2025-04-13 00:54:33 +02:00
b0dd1c21b2 Merge pull request 'Update dependency net.dv8tion:JDA to v5.3.2' (#37) from renovate/net.dv8tion-jda-5.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #37
2025-04-13 00:54:12 +02:00
d46368f0ce Update dependency com.google.code.gson:gson to v2.13.0
Some checks failed
continuous-integration/drone/pr Build is failing
2025-04-11 15:00:26 +00:00
2032dc1d0e Update dependency org.junit.jupiter:junit-jupiter-api to v5.12.2
Some checks failed
continuous-integration/drone/pr Build is failing
2025-04-11 15:00:24 +00:00
9c099230c9 Update dependency org.apache.commons:commons-text to v1.13.1
All checks were successful
continuous-integration/drone/pr Build is passing
2025-04-10 23:00:22 +00:00
f2134cbdb9 Update dependency org.owasp:dependency-check-maven to v12.1.1
Some checks failed
continuous-integration/drone/pr Build is failing
2025-04-05 13:00:31 +00:00
97e846c3dc Update dependency net.dv8tion:JDA to v5.3.2
All checks were successful
continuous-integration/drone/pr Build is passing
2025-04-05 11:00:29 +00:00
7f16a011b3 update pipeline
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-03-29 16:45:14 +01:00
b32ece3f88 Update .drone.yml
Some checks failed
continuous-integration/drone/push Build was killed
2025-03-29 13:02:17 +01:00
681785ef0d Merge pull request 'Update dependency net.dv8tion:JDA to v5.3.1' (#36) from renovate/net.dv8tion-jda-5.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #36
2025-03-28 14:07:57 +01:00
a72b6f690b Update dependency net.dv8tion:JDA to v5.3.1
All checks were successful
continuous-integration/drone/pr Build is passing
2025-03-27 18:00:34 +00:00
645fb5f4a6 build on arm
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-27 00:37:54 +01:00
a228067cce update drone
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-27 00:36:31 +01:00
4f0eb7ce74 Update .drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-27 00:22:01 +01:00
a7e36299b6 Update .drone.yml
Some checks failed
continuous-integration/drone/push Build was killed
2025-03-27 00:21:29 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2025-03-26 23:21:50 +01:00
2a4a80cc3f Update dependency com.google.protobuf:protobuf-java to v4.30.2
All checks were successful
continuous-integration/drone/pr Build is passing
2025-03-26 20:00:37 +00:00
a50e8c050b update pull requests pipe
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-25 18:26:00 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2025-03-25 14:06:04 +01:00
884814064c Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5.1.0.4751
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-25 11:00:43 +00:00
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
All checks were successful
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
aabfbd3020 Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v5
Some checks failed
continuous-integration/drone/pr Build was killed
2025-03-23 11:28:03 +00:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #25
2025-03-23 11:33:45 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #26
2025-03-23 11:33:36 +01:00
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
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #27
2025-03-23 11:33:29 +01:00
edf896efb0 Merge pull request 'Update dependency org.yaml:snakeyaml to v2.4' (#31) from renovate/org.yaml-snakeyaml-2.x into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #31
2025-03-23 11:33:20 +01:00
bd7355add9 Merge pull request 'Update dependency org.json:json to v20250107' (#32) from renovate/org.json-json-20250107.x into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #32
2025-03-23 11:33:05 +01:00
6d210551af Update dependency org.json:json to v20250107
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:08 +00:00
abd3c02be6 Update dependency org.yaml:snakeyaml to v2.4
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:06 +00:00
50749f2108 Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.2
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:04 +00:00
fc7b6d54d1 Update dependency org.apache.commons:commons-text to v1.13.0
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 10:25:01 +00:00
f5d684c5a4 Update dependency commons-codec:commons-codec to v1.18.0
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 10:24:59 +00:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #29
2025-03-23 10:38:52 +01:00
5c4f7e4252 Merge pull request 'Update dependency org.jsoup:jsoup to v1.19.1' (#28) from renovate/org.jsoup-jsoup-1.x into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2025-03-23 10:38:35 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #24
2025-03-23 10:38:05 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
2025-03-23 10:37:32 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2025-03-23 10:31:39 +01:00
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
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #22
2025-03-23 10:31:28 +01:00
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
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #21
2025-03-23 10:31:14 +01:00
1ed389c18b Update dependency org.sonarsource.scanner.maven:sonar-maven-plugin to v3.11.0.3922
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 08:25:02 +00:00
4320c9698a Update dependency org.junit.jupiter:junit-jupiter-api to v5.12.1
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 08:25:00 +00:00
3a8044dda1 Update dependency org.jsoup:jsoup to v1.19.1
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 07:25:07 +00:00
96ca58de12 Update dependency com.google.code.gson:gson to v2.12.1
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 05:24:54 +00:00
5ed444ab92 Update dependency org.slf4j:slf4j-simple to v2.0.17
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 05:24:51 +00:00
19a5583594 Update dependency org.slf4j:slf4j-api to v2.0.17
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 04:27:18 +00:00
a39bcf68cb Update dependency com.google.protobuf:protobuf-java to v4.30.1
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-23 04:27:16 +00:00
eb08f0bcab Merge pull request 'Configure Renovate' (#20) from renovate/configure into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2025-03-23 05:08:56 +01:00
87eadb88ef Add renovate.json
Some checks failed
continuous-integration/drone/pr Build is failing
2025-03-22 23:11:34 +00:00
4f74541d1d move to dev version
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-09 12:00:37 +01:00
86f370643e bump to stable 0.6.2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-03-09 12:00:28 +01:00
27133b8e3c update protobuf dep
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2025-03-09 12:00:01 +01:00
54fee8a010 move to dev version
Some checks failed
continuous-integration/drone/push Build was killed
2025-03-09 11:54:25 +01:00
9e3289c616 bump version to 0.6.1
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 11:54:09 +01:00
a5d647e6ba fix code smells
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 11:52:26 +01:00
44add27d5e move to dev version
Some checks failed
continuous-integration/drone/push Build was killed
2025-03-09 11:46:49 +01:00
fa17bf8ef6 bump version to 0.6.0
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 11:46:28 +01:00
90e576923d update jda version
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 11:42:06 +01:00
ea31746442 move to dev build
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-09 11:37:06 +01:00
3c522149c3 make stable version with updated security 2025-03-09 11:36:55 +01:00
ed1b25b403 update sonar token
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-09 11:31:27 +01:00
04d93dd7a5 fix autocloseable executors
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-09 11:30:59 +01:00
f523e6cd92 add api key
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-09 11:17:30 +01:00
35f52ec583 fix build
Some checks failed
continuous-integration/drone/push Build was killed
2025-03-09 11:07:00 +01:00
549f8bb48b update jdk and deps-ckeck plugin
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 11:03:01 +01:00
be9bd1a068 update vulnerable deps
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 11:01:18 +01:00
2a7be8868c update drone
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-09 00:45:02 +01:00
e51646ace4 simplify method 2025-03-09 00:14:07 +01:00
4abd3d6179 Suppress unneeded security warning
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-11 17:10:50 +01:00
0f2e2f876d Revert "Add .deepsource.toml"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit b174c581e9.
2023-01-25 20:42:11 +01:00
06b28aac70 Fix flawed logic in verbosity enable method
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-24 14:45:44 +01:00
9f52e8747c Fix first arg not getting recognized in some cases (fixes #18)
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-24 14:41:32 +01:00
829e19fac3 Prevent trying to delete "say" message in DMs
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 12:42:49 +01:00
ff323b9d8b Update JDA version and fix related CVEs
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 17:23:31 +01:00
5aa99ae4bf Make builds fail on CVSS >= 8
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-16 08:21:35 +01:00
96465af441 Update suppressions
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 08:20:43 +01:00
f4fc8811ef Suppress SnakeYaml detections
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 08:14:26 +01:00
3abb48ba60 Revert to base Maven image due to missing arm64 binaries
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 08:11:55 +01:00
d58cc08082 Move to different dependency check container
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-16 08:03:01 +01:00
1675b62967 Use prebuilt dependencies database
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-16 07:52:45 +01:00
cc29b63d78 Fix suppression files entry
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 07:47:46 +01:00
d816a1f1d9 Fix suppression files entry
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 07:42:30 +01:00
d788070eb8 Ignore CVE-2022-1471
All checks were successful
continuous-integration/drone/push Build is passing
It doesn't affect the project
2023-01-16 07:40:03 +01:00
627f6deb97 Add default cases to switches
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 07:20:49 +01:00
668375367a Use Java 16 "instanceof" pattern matching
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 07:15:51 +01:00
980cf5eef3 Prevent instantiating utility classes
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 07:07:42 +01:00
ccf69a2903 Update dependency checker configuration
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 05:48:27 +01:00
6c6cdab9f4 Switch to XML dependency report
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 05:39:51 +01:00
05efe6c0d3 Try to rely only on JSON dependency report
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 05:34:04 +01:00
4f615378a6 Implement Maven dependency checker
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 05:24:40 +01:00
118979bde4 Fix corrupted database sql statements
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 05:00:18 +01:00
009fec3be3 Reformat database class and queries using text blocks
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 04:57:25 +01:00
546637c188 Improve various small code quality issues
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 03:53:51 +01:00
d315b3f38a Bump to development version
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 02:32:46 +01:00
94037b252f Improve final fields naming
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 02:31:11 +01:00
d62d6bdfdd Bump to stable version 0.5.19
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-01-16 02:18:24 +01:00
7f73d4fb23 Fix shutdown method no longer shutting down when invocated directly
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 02:11:16 +01:00
383b09a53e Register shutdown hook without relying on Sun proprietary method
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 02:07:27 +01:00
c9d69c512c Enable arm64 for Drone CI
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 01:47:58 +01:00
8b1c1b4d04 Make repository URL a property
Some checks failed
continuous-integration/drone/push Build was killed
2023-01-16 00:41:45 +01:00
a28781b806 Update repo URLs
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-16 00:33:52 +01:00
78bdadad06 Add info about random.org
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 23:02:41 +01:00
a5b9f9d993 Add basic development info to readme 2023-01-15 23:02:13 +01:00
64f0b611ca Bump to stable version 0.5.18
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-01-15 22:21:20 +01:00
f87d461b89 Update README.md
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 22:14:08 +01:00
4f71b5f599 Add direct download link for always up-to-date JAR file
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 22:08:45 +01:00
12e3c5fc2f Remove maven reposity upload in normal commit builds
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 21:56:24 +01:00
bec705ea6c Finish implementing functional Maven build system
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 21:54:03 +01:00
3acab18ff2 Update DroneCI configuration, include settings.xml
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:38:50 +01:00
99a92badc2 Update DroneCI configuration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:32:15 +01:00
f24c93acc0 Update DroneCI configuration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:30:44 +01:00
3cc37b4669 Update DroneCI configuration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:26:55 +01:00
5a74401f74 Update DroneCI configuration
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 21:26:35 +01:00
db151a3f66 Update DroneCI configuration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:25:08 +01:00
c1e995225f Update DroneCI configuration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:23:54 +01:00
ae1313459d Fix typo
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:21:59 +01:00
ea29241d1d Attempt to fix Maven DoneCI integration 2023-01-15 21:21:26 +01:00
ecff8ee31b Update temporary DroneCI configuration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:18:10 +01:00
9578fcdbc2 Fix SonarQube integration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2023-01-15 21:09:03 +01:00
944b4cf905 Update .drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-15 21:07:41 +01:00
21a5e07b3e Attempt to implement Nexus Maven repo deployment
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-15 21:06:01 +01:00
32a5a46565 Update badges, remove SFTP DroneCI integration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-01-15 20:21:04 +01:00
5dcfd7339e Update DroneCI integration to allow artifact uploading
Some checks failed
continuous-integration/drone/push Build is running
continuous-integration/drone Build is failing
2023-01-15 17:53:07 +01:00
3bb006e0c6 Update badges
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 05:32:11 +01:00
0cc30ddf29 Implement MessageRespone base functions
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 05:29:57 +01:00
b55a27fdfb Optimize regex expressions
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 05:22:22 +01:00
ee4c5155fa Improve exception handling
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 05:00:44 +01:00
d2abeb35fc Implement various null checks
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 04:51:30 +01:00
546eb49144 Add empty line to logo
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 04:42:43 +01:00
6b97e0bb7e Remove unused variable
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 04:39:59 +01:00
4df2429b09 Move random methods to random util class, fix footer
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 04:34:39 +01:00
7de23d8207 Bump version to 0.5.17
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 04:26:43 +01:00
d7aa5d75eb Implement random.org API integration with random seed updater
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 04:26:06 +01:00
b81a7e65d2 Disable random seed update because SecureRandom is self-seeding with a better algorithm
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 03:34:34 +01:00
14d2505dac Use Java 15's new text blocks for Unicode logo
All checks were successful
continuous-integration/drone/push Build is passing
Looks way better than String concatenation
2023-01-15 02:13:35 +01:00
528940a9d1 Disable JavaDoc generation in DroneCI pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 02:06:57 +01:00
4c653fc93c Complete moving to SFL4J
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 02:05:23 +01:00
6bbaf3fe7e Deprecate logger and start moving to SLF4J
All checks were successful
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
95b4f81235 Switch to SecureRandom class
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 01:07:31 +01:00
dee00e6814 Update badges
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 01:04:55 +01:00
d6ef0da167 Make multiple small improvements
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 01:00:44 +01:00
fb752fb9a9 Fix code style error
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 00:40:19 +01:00
0d862da9ec Update badges in readme
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 00:08:31 +01:00
57a5972d59 Add badges to readme
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 00:07:07 +01:00
374f979ae3 Fix synchronized mismatch
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-14 23:52:12 +01:00
53abab6bf3 Update DroneCI config
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-14 23:49:20 +01:00
5591b8abab Make method synchronized
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-14 23:30:57 +01:00
5afb398299 Update DroneCI config 2023-01-14 23:27:50 +01:00
0d8c3e2be3 Implement SonarQube support
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-14 21:49:47 +01:00
818a25346b Optimize imports
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-14 20:22:37 +01:00
28286f5389 Fix date parser
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-14 20:20:59 +01:00
DeepSource Bot
b174c581e9 Add .deepsource.toml
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-14 19:16:58 +00:00
c843d2cd61 Update instructions order when registering listeners
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-28 05:41:24 +01:00
3bd09d2867 Update a small comment 2022-12-28 05:41:24 +01:00
c077b92f93 Update 'README.MD' 2022-12-28 05:41:24 +01:00
ecff77867c Update 'README.MD' 2022-12-28 05:41:24 +01:00
5e4e79d505 Bump version to 0.5.16 2022-12-28 05:41:24 +01:00
e1c22ac059 Implement ban, kick, timeout slash commands 2022-12-28 05:41:24 +01:00
3676e9d5ad Make trivia command defer reply 2022-12-28 05:41:24 +01:00
5acd900857 Rework time parsing utils 2022-12-28 05:41:24 +01:00
d2f00c781e Implement basic timeout command 2022-12-28 05:41:24 +01:00
35b2c8fb42 Implement basic ban command 2022-12-28 05:41:23 +01:00
420666eab1 Implement basic kick command 2022-12-28 05:41:23 +01:00
52fe279f35 Add bot age info 2022-12-28 05:41:23 +01:00
7ac72f9e38 Bump version to 0.5.15 2022-12-28 05:41:23 +01:00
00c46c1396 Implement profile banner grabber command 2022-12-28 05:41:23 +01:00
7f7ada9b9e Fix minetest's name in random statuses 2022-12-28 05:41:23 +01:00
47bd16fd13 Fix missing JSON dependency 2022-12-28 05:41:23 +01:00
f5a8c2af48 Mitigate potential RCE from SnakeYaml (CVE-2022-1471)
This vulnerability is very unlikely to ever happen, since the only way to modify the YAML file is to edit it yourself, and it would be useless for a bot owner to RCE their own bot. No other person can edit the configuration file remotely (eg. with bot commands), so realistically, this could not happen.
2022-12-28 05:41:23 +01:00
50196bb8f9 Make trivia support slash commands too 2022-12-28 05:41:23 +01:00
26f1c880ea Handle trivia edge cases without hanging 2022-12-28 05:41:23 +01:00
09c53c07a1 Bump version to 0.5.14 2022-12-28 05:41:23 +01:00
1c19f3c07f Implement trivia welcome screen with category picker 2022-12-28 05:41:23 +01:00
d4c3afbddd Bump version to 0.5.13 2022-12-28 05:41:23 +01:00
96f298b654 Make trivia have a functional scoreboard 2022-12-28 05:41:23 +01:00
48fdb32e15 Make trivia functional 2022-12-28 05:41:23 +01:00
eb93362d16 Add emojis to trivia buttons 2022-12-28 05:41:23 +01:00
9f147cee65 Make trivia announce correct answer 2022-12-28 05:41:23 +01:00
b0d234a454 Raise trivia timeout to 15s 2022-12-28 05:41:23 +01:00
c9082e84cc Make trivia loop through all questions 2022-12-28 05:41:23 +01:00
0be4389448 Disable trivia in dms 2022-12-28 05:41:23 +01:00
c1059bb937 Raise interaction expiration time to 30s 2022-12-28 05:41:23 +01:00
78f62b5f8d Make trivia functional for a single question 2022-12-28 05:41:23 +01:00
3a8a44adf0 Only fetch multiple-answer trivia for now 2022-12-28 05:41:23 +01:00
cfa7aef333 Fix build errors 2022-12-28 05:41:23 +01:00
0e256e4cb5 Remove unneeded methods from MessageResponse 2022-12-28 05:41:23 +01:00
b0622f36aa Start implementing trivia command 2022-12-28 05:41:23 +01:00
e451f59199 Remove double space on urban footer 2022-12-28 05:41:23 +01:00
f3cc9a2d75 Add emojis to urban dictionary 2022-12-28 05:41:23 +01:00
8f5c29aa95 Convert message response to immutable record 2022-12-28 05:41:23 +01:00
df1e2426e3 Bump version to 0.5.12 2022-12-28 05:41:23 +01:00
91261f04e5 Make dice roll support slash commands too 2022-12-28 05:41:23 +01:00
bc0463dd38 Add a MessageResponse class for mixed-type content 2022-12-28 05:41:23 +01:00
161c91b45d Rename method 2022-12-28 05:41:23 +01:00
d09c59996b Cache love calculator results in RAM 2022-12-28 05:41:23 +01:00
1c82d2b53b Make love calculator also support slash commands 2022-12-28 05:41:23 +01:00
174b78704f Optimize imports 2022-12-28 05:41:23 +01:00
ba64c02049 Increase randomness by updating the random's seed every minute 2022-12-28 05:41:23 +01:00
264a94fe0d Bump version to 0.5.11 2022-12-28 05:41:23 +01:00
9e1888611a Make random statuses update automatically 2022-12-28 05:41:23 +01:00
b1b62bab9f Fix invite command being categorized as fun 2022-12-28 05:41:23 +01:00
de34caa513 Improve help command title 2022-12-28 05:41:23 +01:00
215e597a4d Make permissions bold instead of code-wrapped 2022-12-28 05:41:23 +01:00
217022faca Bump version to 0.5.10 2022-12-28 05:41:23 +01:00
34c100acde Fix nothing being rolles if no arg was specified 2022-12-28 05:41:22 +01:00
496304c2c3 Make help command use descriptions and usages 2022-12-28 05:41:22 +01:00
e08fefbda3 Fix spacing 2022-12-28 05:41:22 +01:00
6c4d362ca4 Improve default responses 2022-12-28 05:41:22 +01:00
0e1a7d3459 Bump version to 0.5.9 2022-12-28 05:41:22 +01:00
e2c84f62c3 Make command category not null 2022-12-28 05:41:22 +01:00
b20fb73371 Implement alias command 2022-12-28 05:41:22 +01:00
61745c36d0 Implement alias command 2022-12-28 05:41:22 +01:00
480b8b5eda Add help command and command categories 2022-12-28 05:41:22 +01:00
f9fe12a248 Remove deprecated and unused private method 2022-12-28 05:41:22 +01:00
28d7ff18ba Throw exception in case of serialization issue 2022-12-28 05:41:22 +01:00
1644a4b07d Make serialization util class 2022-12-28 05:41:22 +01:00
68dceaff13 Use enum instead of boolean for page switching
This is useless but looks better
2022-12-28 05:41:22 +01:00
7dcdf9dbde Remove duplication 2022-12-28 05:41:22 +01:00
c4d81fb0e4 Add javadoc comment 2022-12-28 05:41:22 +01:00
24a55e14fd Merge two classes 2022-12-28 05:41:22 +01:00
cce57b8108 Optimize imports 2022-12-28 05:41:22 +01:00
fc846fa901 Remove duplicated method 2022-12-28 05:41:22 +01:00
4476dd2f7b Fix small emoji translation issue 2022-12-28 05:41:22 +01:00
2d7cadea02 Optimize imports 2022-12-28 05:41:22 +01:00
b4c80fe56a Make urban command support slash too 2022-12-28 05:41:22 +01:00
60ee5f2ae2 Allow sender to delete their own urban command results 2022-12-28 05:41:22 +01:00
8ca70dac78 Fix urban dictionary term not getting parsed correctly for url 2022-12-28 05:41:22 +01:00
d412801758 Bump version to 0.5.8 2022-12-28 05:41:22 +01:00
4ef42ffa9e Make urban command support multiple entries 2022-12-28 05:41:22 +01:00
0f54211ecd Improve urban dictionary parsing 2022-12-28 05:41:22 +01:00
d5664eb646 Improve urban dictionary parsing 2022-12-28 05:41:22 +01:00
1421d52598 Keep newlines in urban dictionary parser 2022-12-28 05:41:22 +01:00
639c54bc52 Bump version to 0.5.7 2022-12-28 05:41:22 +01:00
28c0f1d750 Implement urban dictionary lookup command 2022-12-28 05:41:22 +01:00
3259a49ace Change a magic ball response 2022-12-28 05:41:22 +01:00
4d888d68b9 Make it send a message instead of responding to 8ball 2022-12-28 05:41:22 +01:00
7959044335 Bump version to 0.5.6 2022-12-28 05:41:22 +01:00
fb3c08fc41 Add basic love calculator message command 2022-12-28 05:41:22 +01:00
09ec600234 Bump version to 0.5.5 2022-12-28 05:41:22 +01:00
c9ff329cbb Make magicball support slash commands too 2022-12-28 05:41:22 +01:00
8f4f341aab Improve magic ball answers 2022-12-28 05:41:22 +01:00
a030821197 Implement magic ball message command 2022-12-28 05:41:22 +01:00
e531eef1d6 Bump version to 0.5.4 2022-12-28 05:41:22 +01:00
b033763704 Improve diceroll looks, implement limits to avoid abuse 2022-12-28 05:41:21 +01:00
982902fc6d Ignore bots interacting with hideko 2022-12-28 05:41:16 +01:00
00441f089f Remove unneeded todo 2022-12-28 05:41:11 +01:00
1f6f23e917 Make clear command also delete the sender's message 2022-12-28 05:41:06 +01:00
1e07ede83e Bump version to 0.5.3 2022-12-28 05:41:01 +01:00
1a8409994c Implement basic functional diceroll command 2022-12-28 05:40:56 +01:00
b0a1381589 Fix command label being passed as arg in case of no args 2022-12-28 05:40:51 +01:00
9504921f27 Fallback to 0 instead of 1 2022-12-28 05:40:45 +01:00
c9528848bc Fix console error when int parsing fails in clear message 2022-12-28 05:40:39 +01:00
2d1f6699ba Re-register accidentally removed invite command 2022-12-28 05:40:34 +01:00
595e81e02c Bump JDA version to more stable beta 2022-12-28 05:40:28 +01:00
764ff23010 Bump version to 0.5.2 2022-12-28 05:40:23 +01:00
32ea099690 Fix messages with newlines not being handled for commands 2022-12-28 05:40:17 +01:00
1410e4e8af Make say support both slash and message commands 2022-12-28 05:40:11 +01:00
81e621ec1a Bump version to 0.5.1 2022-12-28 05:40:01 +01:00
c486630adb Make avatar support both slash and message commands 2022-12-28 05:37:26 +01:00
af16e6d8ac Bump version to 0.5.0
All checks were successful
continuous-integration/drone/push Build is passing
This is a pretty important update since all the basic layout has been completed.
2022-11-23 00:01:38 +01:00
50ccda214f Finish command completion listener implementation
All checks were successful
continuous-integration/drone/push Build is passing
Very similarly to how the slash command interface works, now a slash command auto-completion interface also exists, with its respective listener.
2022-11-23 00:01:05 +01:00
ff084cf8e8 Rename datasource package
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 23:44:34 +01:00
be3895d268 Update Maven dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 23:44:03 +01:00
a045d0cb2d Bump version to 0.4.6
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 23:42:48 +01:00
0016b5de30 Force using config enum class instead of direct entry path
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 23:42:21 +01:00
6480795368 Discontinue config.yml file in favor of class mapping
All checks were successful
continuous-integration/drone/push Build is passing
The configuration entries are now mapped in an enum that transfers very well to SnakeYaml's YAML parsing. This is better because we no longer run the risk of entries getting mistyped in classes, or renamed without replacing them everywhere...
2022-11-22 23:40:44 +01:00
ae6647a51e Bump version to 0.4.3
All checks were successful
continuous-integration/drone/push Build is passing
We are getting closer to a stable version.
2022-11-22 23:29:29 +01:00
40aac28e34 Make bot version consistent with Maven
All checks were successful
continuous-integration/drone/push Build is passing
A new internal properties file has been added. Maven will scan this file and replace any value it finds.
2022-11-22 23:28:59 +01:00
70578d2ffc Bump version to 0.4.0
All checks were successful
continuous-integration/drone/push Build is passing
The new thread update deserves a decent version bump
2022-11-22 22:00:59 +01:00
5f73c4069b Make bot commands run in separate threads by default
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 21:59:58 +01:00
b681acdbca Make bot announce its prefix
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 21:48:13 +01:00
19100758cb Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 21:42:46 +01:00
b2a62d754e Make invite support both slash and message commands
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 21:42:35 +01:00
c186c9c576 Improve bot info page
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 21:32:20 +01:00
c7208eef84 Make botinfo support both slash and message commands
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 21:02:48 +01:00
89fc2aa0a6 Bump version to 0.3.1
All checks were successful
continuous-integration/drone/push Build is passing
The bot is now in a semi-stable state, although still very lacking in terms of features.
2022-11-22 20:51:23 +01:00
96ea29b103 Reduce bot name to just Hideko
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 20:50:53 +01:00
4015aecc99 Make coinflip support both slash and message commands
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 20:50:37 +01:00
ff80e754ff Add comment 2022-11-22 20:42:47 +01:00
3f1835e059 Move clearchat command to base class
All checks were successful
continuous-integration/drone/push Build is passing
The "clear" command now supports both slash commands and message commands, having identical behavior in both situations.
2022-11-22 20:39:55 +01:00
ecdb0c73e8 Add tiny comment
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 17:27:56 +01:00
655840dc82 Implement basic permission check for message commands
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 17:08:31 +01:00
11e4a07698 Refactor objects package
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 16:41:08 +01:00
a7ac446b0b Remove the need to register slash commands separately
All checks were successful
continuous-integration/drone/push Build is passing
We modified the slash command interface to allow getting command data, and created a generic implementation of it that automatically retrieves data from the command data. The interface should not be used now. Instead, extending the implementation is preferred as it provides a semi-working command already.
2022-11-22 16:40:02 +01:00
ee263a1297 Bump version to 0.3.0
All checks were successful
continuous-integration/drone/push Build is passing
We finally reached a good status for the bot's internal structure, and can actually focus on features now. We also dropped the -slash tag because it's not restricted to slash commands anymore.
2022-11-22 16:20:28 +01:00
a9790b3525 Complete message command parser and listener
All checks were successful
continuous-integration/drone/push Build is passing
The message command listener is now completed and the bot now also supports message-based commands with multiple aliases.
2022-11-22 16:19:08 +01:00
501b1bc71c Bump version
All checks were successful
continuous-integration/drone/push Build is passing
Up to 0.2.8 because we are close to 0.3.0 since we made slash commands interfaces and a better command listener. However, we're still missing interface for command auto-completion and we should probably also register commands on discord's api from our interface instead of storing them again in a separate class.
2022-11-22 14:55:47 +01:00
244e8ace76 Remove redundant API command fetcher
All checks were successful
continuous-integration/drone/push Build is passing
We have our own command listener now, so we don't need to rely on Discord's slow API.
2022-11-22 14:53:46 +01:00
526880e1f1 Start implementing message-base commands
All checks were successful
continuous-integration/drone/push Build is passing
Slash commands can't be used for everything, so we need something to fall back on.
2022-11-22 14:40:44 +01:00
882c695484 Make slash commands interface and load them dynamically
All checks were successful
continuous-integration/drone/push Build is passing
Slash commands are now loaded dynamically by implementing a SlashCommand interface and storing them in a loaded commands map.
2022-11-22 14:32:22 +01:00
7ae4790d5c Add bot owner info to botinfo command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 00:45:58 +01:00
51de18206e Refactor clearchat class 2022-11-22 00:35:10 +01:00
d3db53a451 Cleanup imports 2022-11-22 00:34:37 +01:00
656dff4b26 Refactor packages
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 00:31:52 +01:00
8faa9c4677 Refactor datasource classes
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 00:28:33 +01:00
f9e1578899 Fix heartbeat error not always being logged
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 00:12:24 +01:00
72115cbec2 Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-22 00:08:36 +01:00
0b05f2858f Update README with new bot startup guide 2022-11-22 00:08:19 +01:00
843ee43275 Finish implementing configuration file
All checks were successful
continuous-integration/drone/push Build is passing
Configuration file is now fully functional.
Startup arguments for bot token and heartbeat key have now been removed.
2022-11-22 00:04:34 +01:00
b6bf366822 Allow reading values from config
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 23:36:42 +01:00
c650caa090 Finish configuration init method
All checks were successful
continuous-integration/drone/push Build is passing
This method runs every time the bot starts, and ensures that all values are always present in the config file.
2022-11-21 23:28:33 +01:00
f74ae43673 Refactor Config class
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 20:20:11 +01:00
e396ce6417 Start implementing yaml loading
Some checks failed
continuous-integration/drone/push Build is failing
2022-11-21 20:20:03 +01:00
66d27fe1fe Start implementing config file 2022-11-21 20:04:28 +01:00
d9d0ce3236 Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 19:55:04 +01:00
a5ddbf0d2e Implement heartbeat for uptime monitoring
All checks were successful
continuous-integration/drone/push Build is passing
You can now monitor the bot's uptime via any external tool that supports push heartbeats. The bots sends a GET request every 30 seconds to show that it's online. The URL is hardcoded for the moment, but very easy to change.
2022-11-21 19:54:49 +01:00
0bcb5d58f4 Set message expiration time to 15 seconds
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 19:11:18 +01:00
531ff66bae Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 19:07:49 +01:00
da511f2913 Database overhaul to fix #3
All checks were successful
continuous-integration/drone/push Build is passing
We are now tracking whether messages are sent privately on in a guild, and acting accordingly.
2022-11-21 19:07:34 +01:00
0aec543a46 Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 16:28:31 +01:00
326ad68e38 Add emoji to invite command button
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 16:24:21 +01:00
163619a7f8 Improve registered commands caching
All checks were successful
continuous-integration/drone/push Build is passing
Discord's API is slow in updating and registering new commands, so we set up a runnable to periodically check.
2022-11-21 16:24:09 +01:00
b015fddf3c Make invite command nicer
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 16:11:16 +01:00
5e4e438340 Make minimal text changes 2022-11-21 15:59:27 +01:00
b35b962ac6 Implement basic say command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:37:12 +01:00
4382f7d490 Fix issue tracker link
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:17:03 +01:00
3038be9a28 Actually register help command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:14:57 +01:00
24bb560d93 Register help command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:13:32 +01:00
78d5bd6beb Make some messages nicer 2022-11-21 15:07:46 +01:00
3ff154eec6 Rename methods
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:04:12 +01:00
e9f475cb59 Refactor code and packages
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:02:40 +01:00
cdc45d62f2 Move uptime string generator to config class
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 15:00:37 +01:00
6998cc92e5 Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 14:55:01 +01:00
996cedb154 Finish botinfo command
All checks were successful
continuous-integration/drone/push Build is passing
I don't know what else to add, and it looks nice now.
2022-11-21 14:54:45 +01:00
5e08cd748c Update README
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 14:44:54 +01:00
d2caccf080 Finish uptime string generator 2022-11-21 14:44:48 +01:00
79c8f1a95e Start implementing bot info command
Some checks failed
continuous-integration/drone/push Build is failing
2022-11-21 12:19:35 +01:00
6e9291c535 Implement messages error handling in database
All checks were successful
continuous-integration/drone/push Build is passing
Invalid messages will now be purged from database (if bot was kicked from a guild, if a channel was deleted, ...).
2022-11-21 11:23:52 +01:00
083fef3911 Fix expired coinflip channel id bug
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 11:19:23 +01:00
f73b489844 Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 11:15:23 +01:00
0da3eecd29 Restrict reflip button to user who ran the command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 11:14:07 +01:00
7562e956bc Remove leftover debug log
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 00:29:22 +01:00
97980f8ed9 Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 00:28:51 +01:00
0d92921b45 Ignore SQLite database
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-21 00:28:29 +01:00
a9ccfbe0bc Remove SQLite database 2022-11-21 00:28:10 +01:00
5ee7321978 Implement functional reply tracker
Now, with commands that support it, only the user who ran the command/triggered the interaction can use its buttons.
2022-11-21 00:27:57 +01:00
98a162a33b Implement SQLite database solving #1
All checks were successful
continuous-integration/drone/push Build is passing
A new basic database has been laid out, with support for message expiry and disabling buttons for old messages.
2022-11-21 00:14:13 +01:00
7ffd3442c2 Remove code duplication in coinflip command 2022-11-20 22:23:26 +01:00
3a5b2a23c1 Add clearchat dismiss button 2022-11-20 22:23:14 +01:00
3d626bb46f Move command handling out of constructor, add coin reflip command
All checks were successful
continuous-integration/drone/push Build is passing
Having heavy code run in a constructor is bad practice. We made separate methods for command handling.
2022-11-20 22:09:58 +01:00
c44251ddb7 Move avatar resolutions to config class
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 19:00:27 +01:00
913e8e023a Refactor command packages
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 18:56:57 +01:00
3474593dc9 Implement command force-refresh arg
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 18:54:13 +01:00
3578de17c8 Implement avatar resolution autocomplete
Some checks failed
continuous-integration/drone/push Build is failing
2022-11-20 18:53:28 +01:00
679d16e1fa Change avatar embed format
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 18:22:32 +01:00
33d81acc64 Improve internal documentation
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 18:11:00 +01:00
044445890f Improve avatar command
All checks were successful
continuous-integration/drone/push Build is passing
Avatar command now produces an embed with links to all possible resolutions
2022-11-20 18:06:07 +01:00
3e1ba12314 Implement avatar grabber command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 17:19:40 +01:00
4817bacf5c Improve invite command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 16:20:50 +01:00
18db0282d5 Implement invite link command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 16:07:04 +01:00
b14850acaa Bump bot version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 06:15:14 +01:00
e592111d1b Update base JDA version
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 06:13:52 +01:00
cd46d601ec Remove pause feature
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 06:07:25 +01:00
c6ee0f3ae1 Sort commands alphabetically
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 06:05:07 +01:00
f156727413 Optimize imports
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 06:04:34 +01:00
a2c1944a32 Move to class-based command handling
All checks were successful
continuous-integration/drone/push Build is passing
Having everything in a single class is bad practice, so different classes for each command were made.
2022-11-20 06:04:00 +01:00
dd4ffe252e Add command to stop bot process
All checks were successful
continuous-integration/drone/push Build is passing
Previously, anyone could send the keywords in chat and kill the bot. Now, only the set bot owner can run the command.
2022-11-20 05:57:58 +01:00
bd76562bcc Remove "flip a coin" message-command
All checks were successful
continuous-integration/drone/push Build is passing
We have /coinflip now
2022-11-20 05:48:31 +01:00
cc671499cd Fix null argument error in clearchat
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 05:47:29 +01:00
96953bddcb Make messages prettier
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 05:43:17 +01:00
7d9c820243 Implement fully functional clearchat command
All checks were successful
continuous-integration/drone/push Build is passing
The command now supports potentially-infinite message deletion and exception catching (eg messages older than 2 weeks). No longer limited to 100 messages per run.
2022-11-20 05:33:04 +01:00
e1ecc310cc Remove dangerous old clearchat message
All checks were successful
continuous-integration/drone/push Build is passing
Previous clearchat message had no permissions set (it was just a demo) and didn't support slash commands.
2022-11-20 03:53:53 +01:00
fca7c2d26f Fix command registration
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 03:52:15 +01:00
813107a2f9 Attempt to fix commands getting unregistered
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 03:49:53 +01:00
bfb4aae2aa Register clear command 2022-11-20 03:42:59 +01:00
f2dc70569d Implement basic clear-chat slash command 2022-11-20 03:41:51 +01:00
771e115bbd Update startup method
All checks were successful
continuous-integration/drone/push Build is passing
Small changes to improve stability and readability.
2022-11-20 03:25:51 +01:00
fddabae3c3 Make commands util class grab API instance
All checks were successful
continuous-integration/drone/push Build is passing
Instead of passing it as an argument, let the class grab the instance itself.
2022-11-20 03:18:14 +01:00
8b9ce25684 Add shutdown interrupt signal listener
This way, we can nicely close the API connection and perform general cleanup.
2022-11-20 03:17:37 +01:00
b43b882cab Add coinflip slash command
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 03:07:43 +01:00
fb69dcd863 Implement basic slash commands support
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-20 03:01:46 +01:00
add9dc0632 Update a comment
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 20:57:23 +02:00
8b2fee6aec Optimize args extraction
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 20:43:49 +02:00
ff4ffba45d Implement verbosity-changer command at runtime
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 20:43:12 +02:00
d085a671c5 Update pause message
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 20:29:29 +02:00
3cf8a1c92a Lower invite-link logging delay 2022-08-26 20:29:10 +02:00
a18b34b784 Add pause command to halt processing
All checks were successful
continuous-integration/drone/push Build is passing
Useful for now since I have two instances of the bot running in the same servers and I don't want both of them to respond.
2022-08-26 20:27:46 +02:00
2443adfccc Document verbose startup argument
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 16:30:56 +02:00
b3429f9203 Enable JavaDocs generation in Drone CI
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 16:27:04 +02:00
aa223df480 Implement JavaDocs
All checks were successful
continuous-integration/drone/push Build is passing
JavaDocs can be generated with mvn javadoc:javadoc and will be available in target/site/apidocs.
2022-08-26 16:25:41 +02:00
70570624e1 Move shutdown log to delayed actions
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 03:50:43 +02:00
d15132e6d6 Add shutdown event to logs
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 03:48:55 +02:00
3416c13f10 Add small delay before shutdown
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 03:48:15 +02:00
ffab94f525 Add basic shutdown command
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 03:46:36 +02:00
0c09a03255 Improve message deletion command
All checks were successful
continuous-integration/drone/push Build is passing
It's now supported on all TextChannels, not only GuildMessageChannels
2022-08-26 03:39:57 +02:00
e4ecd15867 Add simple greeting message
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 01:47:01 +02:00
63bed66073 Improve message deletion command
All checks were successful
continuous-integration/drone/push Build is passing
Now you can specify how many messages to clear, and we aren't spamming Discord's API.
2022-08-26 01:44:53 +02:00
76c2c9e171 Move logger's anonymous runnable to lambda
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 01:15:57 +02:00
59c5e09f14 Add demo clear-chat command
All checks were successful
continuous-integration/drone/push Build is passing
This command is very unoptimized (spawning 12 threads) and sometimes hits 429 errors, but it works until things get more serious. Also it's hardcoded to only delete 10 messages.
2022-08-26 01:15:10 +02:00
a875053435 Make the bot play Project DIVA
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 00:39:55 +02:00
00e30bd073 Add support for attachments in message logger
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 00:20:29 +02:00
7d068892e2 Include user tag in message logger
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-26 00:15:47 +02:00
99 changed files with 8022 additions and 264 deletions

View File

@@ -1,13 +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 test -B
- develop
build_events:
- push
- pull_request
sonar_project_key: HidekoBot
deploy_targets:
- production

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
target/
.idea/
.idea/
scripts/
*.sqlite

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

View File

@@ -1,16 +1,48 @@
# HidekoBot
# 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
java -jar HidekoBot.jar botToken
java -jar HidekoBot.jar [additional parameters]
```
Where `HidekoBot.jar` is the executable archive and `botToken` is your bot token passed as an argument.
*Note: Java 16 or later is required.*
Where `HidekoBot.jar` is the executable archive and `[additional parameters]` are arguments that you can add to
make the bot change its behavior.
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 21 or later is required.*
## Initial setup
After successfully starting the bot up, it will print an invite-link in your console. Click on the link to add your bot
to any server with the correct permissions already set-up.
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.
Save the file and start the bot again. If there are no issues, everything will load and it will print an
invite-link in your console. Click on the link to add your bot to any server with the correct permissions
already set-up. The bot supports both slash commands and message commands, with prefix `hideko`. Most
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 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.

184
pom.xml
View File

@@ -6,57 +6,183 @@
<groupId>wtf.beatrice.hidekobot</groupId>
<artifactId>HidekoBot</artifactId>
<version>1.0-SNAPSHOT</version>
<version>0.6.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-alpha.18</version>
<version>5.6.1</version>
</dependency>
<!-- JDA depends on SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0</version>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.0</version>
<version>2.0.17</version>
</dependency>
<!-- Dependency used for SQLite database connections-->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.51.0.0</version>
</dependency>
<!-- Dependency used for YAML configuration files -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.5</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.21.2</version>
</dependency>
<!-- Various String manipulation utils -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.14.0</version>
</dependency>
<!-- JSON dependency used for better parsing of JSON files -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<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.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.20.0</version>
</dependency>
<!-- End Random.org dependencies -->
<!-- Unit Tests Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- override dependencies to use newer versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.33.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<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>
</plugin>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<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>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>5.2.0.4988</version>
</plugin>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.1.8</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>
</plugins>
</build>
</project>
<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
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

20
settings.xml Normal file
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>

View File

@@ -0,0 +1,411 @@
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 java.awt.*;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.HashMap;
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 BOT_PREFIX = "hideko";
private static final Logger LOGGER = LoggerFactory.getLogger(Cache.class);
// map to store results of "love calculator", to avoid people re-running the same command until
// they get what they wanted.
// i didn't think this was worthy of a whole database table with a runnable checking for expiration,
// and it will get cleared after a few minutes anyway, so RAM caching is more than good enough.
private static final HashMap<String, Integer> loveCalculatorValues = new HashMap<>();
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 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 static final long EXPIRY_TIME_SECONDS = 30L;
// used to count e.g. uptime
private static LocalDateTime startupTime = null;
// date of when the first bot commit was made (CEST time)
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 static final ScheduledExecutorService taskScheduler = Executors.newSingleThreadScheduledExecutor(); // todo: try-with-resources
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 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 = "";
// discord api returns a broken image if you don't use specific sizes (powers of 2), so we limit it to these
private static final int[] supportedAvatarResolutions = {16, 32, 64, 128, 256, 512, 1024};
/**
* Get an array of all the Discord-supported avatar resolutions.
* Discord's API returns a broken image if you don't use specific sizes (powers of 2).
*
* @return array of supported resolutions.
*/
public static int[] getSupportedAvatarResolutions()
{
return supportedAvatarResolutions;
}
/**
* 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 synchronized boolean isVerbose()
{
return verbose;
}
/**
* Set the bot's verbosity status at runtime.
* This also registers or unregisters the message-logger listener.
*
* @param v the verbosity boolean value
*/
public static synchronized void setVerbose(boolean v)
{
verbose = v;
if (verbosityLogger != null)
{
HidekoBot.getAPI().removeEventListener(verbosityLogger);
verbosityLogger = null;
}
if (v)
{
verbosityLogger = new MessageLogger();
HidekoBot.getAPI().addEventListener(verbosityLogger);
}
}
/**
* Get the bot owner's profile id.
*
* @return a long of the account's id
*/
public static long getBotOwnerId()
{
return configurationSource == null ? 0L : (Long) configurationSource.getConfigValue(ConfigurationEntry.BOT_OWNER_ID);
}
/**
* Get the bot's token.
*
* @return a String of the bot's token.
*/
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 BOT_MAINTAINER_ID;
}
/**
* Set the bot's application id.
*
* @param id the bot's application id
*/
public static void setBotApplicationId(String id)
{
botApplicationId = id;
}
/**
* Get the bot's application id
*
* @return a string of the bot's application id
*/
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 DEFAULT_INVITE_LINK.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)
{
databaseSource = databaseSourceInstance;
}
/**
* 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.
*
* @param propertiesSourceInstance the properties source instance.
*/
public static void setPropertiesSourceInstance(PropertiesSource propertiesSourceInstance)
{
propertiesSource = propertiesSourceInstance;
}
/**
* Get the DateTimeFormatter string for parsing the expired messages timestamp.
*
* @return the String of the DateTimeFormatter format.
*/
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 EXPIRY_TIME_SECONDS;
}
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");
}
/**
* 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 + "/";
}
/**
* Get the bot's global color.
*
* @return the Color object.
*/
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
{
Field field = Color.class.getField(colorName);
color = (Color) field.get(null);
} catch (RuntimeException | NoSuchFieldException | IllegalAccessException e)
{
LOGGER.error("Unknown color: {}", colorName);
}
return color == null ? defaultColor : color;
}
//todo javadocs
public static void setSlashCommandListener(SlashCommandListener commandListener)
{
slashCommandListener = commandListener;
}
public static SlashCommandListener getSlashCommandListener()
{
return slashCommandListener;
}
public static void setSlashCommandCompletionListener(SlashCommandCompletionListener commandCompletionListener)
{
slashCommandCompletionListener = commandCompletionListener;
}
public static SlashCommandCompletionListener getSlashCommandCompletionListener()
{
return slashCommandCompletionListener;
}
public static void setMessageCommandListener(MessageCommandListener commandListener)
{
messageCommandListener = commandListener;
}
public static MessageCommandListener getMessageCommandListener()
{
return messageCommandListener;
}
/**
* Set the bot's startup time. Generally only used at boot time.
*
* @param time a LocalDateTime of the startup moment.
*/
public static void setStartupTime(LocalDateTime time)
{
startupTime = time;
}
/**
* Get the time of when the bot was started up.
*
* @return a LocalDateTime object of the startup instant.
*/
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 String getFullHeartBeatLink()
{
return configurationSource == null ? null : (String) configurationSource.getConfigValue(ConfigurationEntry.HEARTBEAT_LINK);
}
//todo javadocs
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;
}
/**
* Get the bot's prefix
*
* @return a String of the bot's prefix.
*/
public static String getBotPrefix()
{
return BOT_PREFIX;
}
public static void cacheLoveCalculatorValue(String userId1, String userId2, int value)
{
String merged = userId1 + "|" + userId2;
loveCalculatorValues.put(merged, value);
}
@Nullable
public static Integer getLoveCalculatorValue(String userId1, String userId2)
{
String merged1 = userId1 + "|" + userId2;
String merged2 = userId2 + "|" + userId1;
Integer value = null;
value = loveCalculatorValues.get(merged1);
if (value == null) value = loveCalculatorValues.get(merged2);
return value;
}
public static void removeLoveCalculatorValue(String userId1, String userId2)
{
loveCalculatorValues.remove(userId1 + "|" + userId2);
loveCalculatorValues.remove(userId2 + "|" + userId1);
}
public static ScheduledExecutorService getTaskScheduler()
{
return taskScheduler;
}
}

View File

@@ -1,15 +0,0 @@
package wtf.beatrice.hidekobot;
public class Configuration
{
private static boolean verbose = false;
public static boolean isVerbose() { return verbose; }
// WARNING: verbosity spams the logs a LOT!
public static void setVerbose(boolean v) { verbose = v; }
}

View File

@@ -2,56 +2,71 @@ package wtf.beatrice.hidekobot;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.requests.GatewayIntent;
import wtf.beatrice.hidekobot.listeners.MessageListener;
import wtf.beatrice.hidekobot.listeners.MessageLogger;
import wtf.beatrice.hidekobot.utils.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.commands.completer.ProfileImageCommandCompleter;
import wtf.beatrice.hidekobot.commands.message.HelloCommand;
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.RandomOrgSeedTask;
import wtf.beatrice.hidekobot.runnables.StatusUpdateTask;
import wtf.beatrice.hidekobot.util.CommandUtil;
import wtf.beatrice.hidekobot.util.FormatUtil;
import wtf.beatrice.hidekobot.util.RandomUtil;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class HidekoBot
{
private static String botToken;
private static String standardInviteLink = "https://discord.com/oauth2/authorize?client_id=%userid%&scope=bot&permissions=8";
private static String botUserId;
private static final String version = "0.0.1"; // we should probably find a way to make this consistent with Maven
private static JDA jda;
// create a logger instance for ease of use
private static final Logger logger = new Logger(HidekoBot.class);
private static final Logger LOGGER = LoggerFactory.getLogger(HidekoBot.class);
public static void main(String[] args)
{
// check if bot token was specified as a startup argument
if(args.length < 1)
// load configuration
LOGGER.info("Loading configuration...");
String configFilePath = Cache.getExecPath() + File.separator + "config.yml";
ConfigurationSource configurationSource = new ConfigurationSource(configFilePath);
configurationSource.initConfig();
Cache.setConfigurationSource(configurationSource);
LOGGER.info("Configuration loaded!");
// load properties
LOGGER.info("Loading properties...");
PropertiesSource propertiesSource = new PropertiesSource();
propertiesSource.load();
Cache.setPropertiesSourceInstance(propertiesSource);
LOGGER.info("Properties loaded!");
// check loaded bot token
String botToken = Cache.getBotToken();
if (botToken == null || botToken.isEmpty())
{
logger.log("Please specify your bot token!");
LOGGER.error("Invalid bot token!");
shutdown();
return;
}
// load token from args
botToken = args[0];
// if there are more than 1 args, then iterate through them because we have additional things to do
if(args.length > 1) {
List<String> argsList = new ArrayList<>();
for(int i = 1; i < args.length; i++)
{ argsList.add(args[i]); }
if(argsList.contains("verbose")) Configuration.setVerbose(true);
}
try
{
// try to create the bot object and authenticate it with discord.
JDABuilder jdaBuilder = JDABuilder.createDefault(botToken);
jdaBuilder.setActivity(Activity.playing("the piano"));
// enable necessary intents.
jdaBuilder.enableIntents(
@@ -61,31 +76,189 @@ public class HidekoBot
);
jda = jdaBuilder.build().awaitReady();
} catch (LoginException | InterruptedException e)
} catch (InterruptedException e)
{
logger.log(e.getMessage()); // print the error message, omit the stack trace.
return; // if we failed connecting and authenticating, then quit.
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.error(e.getMessage()); // print the error message, omit the stack trace.
shutdown(); // if we failed connecting and authenticating, then quit.
}
// find the bot's user id and generate an invite-link.
botUserId = jda.getSelfUser().getId();
standardInviteLink = standardInviteLink.replace("%userid%", botUserId);
// find the bot's user/application id
String botUserId = jda.getSelfUser().getId();
Cache.setBotApplicationId(botUserId);
// store if we have to force refresh commands despite no apparent changes.
boolean forceUpdateCommands = false;
// 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 > 0)
{
List<String> argsList = new ArrayList<>(Arrays.asList(args));
// NOTE: do not replace with enhanced for, since we might need
// to know what position we're at or do further elaboration of the string.
// we were using this for api key parsing in the past.
for (int i = 0; i < argsList.size(); i++)
{
String arg = argsList.get(i);
if (arg.equals("verbose")) Cache.setVerbose(true);
if (arg.equals("refresh")) forceUpdateCommands = true;
}
}
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);
slashCommandCompletionListener.registerCommandCompleter(avatarCommandCompleter);
slashCommandListener.registerCommand(new BanCommand());
BannerCommand bannerCommand = new BannerCommand();
ProfileImageCommandCompleter bannerCommandCompleter = new ProfileImageCommandCompleter(bannerCommand);
slashCommandListener.registerCommand(bannerCommand);
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());
// 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());
// register listeners
jda.addEventListener(new MessageListener());
if(Configuration.isVerbose()) jda.addEventListener(new MessageLogger());
Cache.setSlashCommandListener(slashCommandListener);
Cache.setSlashCommandCompletionListener(slashCommandCompletionListener);
Cache.setMessageCommandListener(messageCommandListener);
jda.addEventListener(messageCommandListener);
jda.addEventListener(slashCommandListener);
jda.addEventListener(slashCommandCompletionListener);
jda.addEventListener(new ButtonInteractionListener());
jda.addEventListener(new SelectMenuInteractionListener());
// update slash commands (delayed)
final boolean finalForceUpdateCommands = forceUpdateCommands;
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.schedule(() -> CommandUtil.updateSlashCommands(finalForceUpdateCommands),
1, TimeUnit.SECONDS);
}
// set the bot's status
jda.getPresence().setStatus(OnlineStatus.ONLINE);
// connect to database
LOGGER.info("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.info("Database connection initialized!");
Cache.setDatabaseSourceInstance(databaseSource);
// load data here...
LOGGER.info("Database data loaded into memory!");
} else
{
LOGGER.error("Error initializing database connection!");
}
// start scheduled runnables
ScheduledExecutorService scheduler = Cache.getTaskScheduler();
ExpiredMessageTask expiredMessageTask = new ExpiredMessageTask();
scheduler.scheduleAtFixedRate(expiredMessageTask, 5L, 5L, TimeUnit.SECONDS); //every 5 seconds
HeartBeatTask heartBeatTask = new HeartBeatTask();
scheduler.scheduleAtFixedRate(heartBeatTask, 10L, 30L, TimeUnit.SECONDS); //every 30 seconds
StatusUpdateTask statusUpdateTask = new StatusUpdateTask();
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.
Runtime.getRuntime().addShutdownHook(new Thread(HidekoBot::preShutdown));
// set startup time.
Cache.setStartupTime(LocalDateTime.now());
// print the bot logo.
logger.log("Ready!\n\n" + logger.getLogo() + "\nv" + version + " - 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, 4);
logger.log("Invite Link: " + standardInviteLink, 5);
LOGGER.info("Bot User ID: {}", botUserId);
LOGGER.info("Invite Link: {}", Cache.getInviteUrl());
}
public static JDA getAPI()
{
return jda;
}
public static void shutdown()
{
preShutdown();
System.exit(0);
}
private static void preShutdown()
{
LOGGER.warn("WARNING! Shutting down!");
if (jda != null) jda.shutdown();
}
}

View File

@@ -0,0 +1,28 @@
package wtf.beatrice.hidekobot.commands.base;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
public class Alias
{
private Alias()
{
throw new IllegalStateException("Utility class");
}
public static String generateNiceAliases(MessageCommand command)
{
LinkedList<String> aliases = command.getCommandLabels();
StringBuilder aliasesStringBuilder = new StringBuilder();
for (int i = 0; i < aliases.size(); i++)
{
aliasesStringBuilder.append("`").append(aliases.get(i)).append("`");
if (i + 1 != aliases.size())
aliasesStringBuilder.append(", "); // separate with comma except on last iteration
}
return aliasesStringBuilder.toString();
}
}

View File

@@ -0,0 +1,128 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
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;
public class BotInfo
{
private BotInfo()
{
throw new IllegalStateException("Utility class");
}
public static MessageEmbed generateEmbed(List<String> commandLabels)
{
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle(Cache.getBotName());
// thumbnail
String botAvatarUrl = HidekoBot.getAPI().getSelfUser().getAvatarUrl();
if (botAvatarUrl != null) embedBuilder.setThumbnail(botAvatarUrl);
// help field
long ownerId = Cache.getBotOwnerId();
String prefix = Cache.getBotPrefix();
embedBuilder.addField("Getting started",
"This instance is run by <@" + ownerId + ">.\n" +
"Type `/help` for help! The bot prefix is `" + prefix + "`.",
false);
// type-specific commands list field
StringBuilder commandsListBuilder = new StringBuilder();
commandsListBuilder.append(commandLabels.size()).append(" total - ");
for (int i = 0; i < commandLabels.size(); i++)
{
commandsListBuilder.append("`").append(commandLabels.get(i)).append("`");
if (i + 1 != commandLabels.size()) // don't add comma in last iteration
{
commandsListBuilder.append(", ");
}
}
embedBuilder.addField("Type commands", commandsListBuilder.toString(), false);
// keep track of how many total commands we have
int commandsCount = 0;
// message commands info field
String messageCommandsInfo;
if (Cache.getMessageCommandListener() == null)
messageCommandsInfo = "❌ disabled";
else
{
messageCommandsInfo = "✅ available";
commandsCount += Cache.getMessageCommandListener().getRegisteredCommands().size();
}
embedBuilder.addField("Message commands", messageCommandsInfo, true);
// slash commands info field
String slashCommandsInfo;
if (Cache.getMessageCommandListener() == null)
slashCommandsInfo = "❌ disabled";
else
{
slashCommandsInfo = "✅ available";
commandsCount += Cache.getSlashCommandListener().getRegisteredCommands().size();
}
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);
// version field
embedBuilder.addField("Version", "v" + Cache.getBotVersion(), true);
// jvm version field
String jvmVersion = ManagementFactory.getRuntimeMXBean().getVmVersion();
// only keep the important part "v19.0.1" and omit "v19.0.1+10"
jvmVersion = jvmVersion.replaceAll("\\+.*", "");
embedBuilder.addField("JVM Version", "v" + jvmVersion, true);
// used ram field
long usedRamBytes = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
double usedRamMB = usedRamBytes / 1024.0 / 1024.0; // bytes -> kB -> MB
DecimalFormat ramMBFormatter = new DecimalFormat("#.##");
embedBuilder.addField("RAM Usage", ramMBFormatter.format(usedRamMB) + " MB", true);
// developer field
String developerMention = "<@" + Cache.getBotMaintainerId() + ">";
embedBuilder.addField("Maintainer", developerMention, true);
// uptime field
embedBuilder.addField("Uptime", FormatUtil.getNiceTimeDiff(Cache.getStartupTime()), true);
// issue tracker field
String link = "[Issue tracker](" + Cache.getRepositoryUrl() + "issues)";
embedBuilder.addField("Support",
link, true);
// bot birthday field
embedBuilder.addField("Bot age",
Cache.getBotName() + " was created " + FormatUtil.getNiceTimeDiff(Cache.getBotBirthDate()) + "ago!",
false);
return embedBuilder.build();
}
}

View File

@@ -0,0 +1,188 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageHistory;
import net.dv8tion.jda.api.entities.channel.Channel;
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 java.util.ArrayList;
import java.util.List;
public class ClearChat
{
private ClearChat()
{
throw new IllegalStateException("Utility class");
}
public static String getLabel()
{
return "clear";
}
public static String getDescription()
{
return "Clear the current channel's chat.";
}
public static Permission getPermission()
{
return Permission.MESSAGE_MANAGE;
}
public static String checkDMs(Channel channel)
{
if (!(channel instanceof TextChannel))
{
return "\uD83D\uDE22 Sorry! I can't delete messages here.";
}
return null;
}
public static String checkDeleteAmount(int toDeleteAmount)
{
if (toDeleteAmount <= 0)
{
return "\uD83D\uDE22 Sorry, I can't delete that amount of messages!";
}
return null;
}
public static int delete(int toDeleteAmount,
long startingMessageId,
MessageChannel channel)
{
// int to keep track of how many messages we actually deleted.
int deleted = 0;
int limit = 95; //discord limits this method to only 2<x<100 deletions per run.
// we set this slightly lower to be safe, and iterate as needed.
// increase the count by 1, because we technically aren't clearing the first ID ever
// which is actually the slash command's ID and not a message.
toDeleteAmount++;
// count how many times we have to iterate this to delete the full <toDeleteAmount> messages.
int iterations = toDeleteAmount / limit;
//if there are some messages left, but less than <limit>, we need one more iterations.
int remainder = toDeleteAmount % limit;
if (remainder != 0) iterations++;
// set the starting point.
long messageId = startingMessageId;
// boolean to see if we're trying to delete more messages than possible.
boolean outOfBounds = false;
// do iterate.
for (int iteration = 0; iteration < iterations; iteration++)
{
if (outOfBounds) break;
// 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... check if we have <limit> or fewer messages to delete
if (iteration + 1 == iterations && remainder != 0)
{
iterationSize = remainder;
}
if (iterationSize == 1)
{
// grab the message
Message toDelete = channel.retrieveMessageById(messageId).complete();
//only delete one message
if (toDelete != null) toDelete.delete().queue();
else outOfBounds = true;
// increase deleted counter by 1
deleted++;
} 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.
List<Message> messages = new ArrayList<>();
// (we are skipping first iteration since it would return an error, given that the id is the slash command and not a message)
if (iteration != 0) messages.add(channel.retrieveMessageById(messageId).complete());
messages.addAll(action.complete().getRetrievedHistory());
// check if we only have one or zero messages left (trying to delete more than possible)
if (messages.size() <= 1)
{
outOfBounds = true;
} else
{
// before deleting, we need to grab the <previous to the oldest> message's id for next iteration.
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.getFirst().getIdLong();
else outOfBounds = true;
}
// queue messages for deletion
if (messages.size() == 1)
{
messages.getFirst().delete().queue();
} else if (!messages.isEmpty())
{
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
which are restricted by discord, and thus has to use
a less efficient way that triggers rate-limiting very quickly. */
} catch (RuntimeException ignored)
{
return -1;
}
}
// increase deleted counter by <list size>
deleted += messages.size();
}
}
return deleted;
}
public static Button getDismissButton()
{
return Button.primary("generic_dismiss", "Dismiss")
.withEmoji(Emoji.fromUnicode(""));
}
public static String parseAmount(int deleted)
{
if (deleted < 1)
{
return "\uD83D\uDE22 Couldn't clear any message!";
} else if (deleted == 1)
{
return "✂ Cleared 1 message!";
} else
{
return "✂ Cleared " + deleted + " messages!";
}
}
// cap the amount to avoid abuse.
public static int getMaxAmount()
{
return 1000;
}
}

View File

@@ -0,0 +1,74 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
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 wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.List;
public class CoinFlip
{
private CoinFlip()
{
throw new IllegalStateException("Utility class");
}
public static Button getReflipButton()
{
return Button.primary("coinflip_reflip", "Flip again")
.withEmoji(Emoji.fromUnicode("\uD83E\uDE99"));
}
public static String genRandom()
{
int rand = RandomUtil.getRandomNumber(0, 1);
String msg;
if (rand == 1)
{
msg = ":coin: It's **Heads**!";
} else
{
msg = "It's **Tails**! :coin:";
}
return msg;
}
public static 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())))
{
event.reply("❌ 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();
// perform coin flip
event.getHook().sendMessage(genRandom())
.addActionRow(getReflipButton())
.queue((message) ->
{
// 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)
{
Cache.getDatabaseSource().queueDisabling(replyMessage);
Cache.getDatabaseSource().trackRanCommandReply(replyMessage, user);
}
}

View File

@@ -0,0 +1,174 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.User;
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;
public class DiceRoll
{
private DiceRoll()
{
throw new IllegalStateException("Utility class");
}
public static MessageResponse buildResponse(User author, String[] args)
{
LinkedHashMap<Dice, Integer> dicesToRoll = new LinkedHashMap<>();
String diceRegex = "d\\d+";
String amountRegex = "\\d+";
Dice currentDice = null;
int currentAmount;
UUID lastPushedDice = null;
int totalRolls = 0;
for (String arg : args)
{
if (totalRolls > 200)
{
return new MessageResponse("Too many total rolls!", null);
}
if (arg.matches(amountRegex))
{
currentAmount = Integer.parseInt(arg);
if (currentDice == null)
{
currentDice = new Dice(6);
} else
{
currentDice = new Dice(currentDice);
}
if (currentAmount > 100)
{
return new MessageResponse("Too many rolls (`" + currentAmount + "`)!", null);
}
lastPushedDice = currentDice.getUUID();
dicesToRoll.put(currentDice, currentAmount);
totalRolls += currentAmount;
} else if (arg.matches(diceRegex))
{
int sides = Integer.parseInt(arg.substring(1));
if (sides > 10000)
{
return new MessageResponse("Too many sides (`" + sides + "`)!", null);
}
if (args.length == 1)
{
dicesToRoll.put(new Dice(sides), 1);
totalRolls++;
} else
{
if (currentDice != null)
{
if (lastPushedDice == null || !lastPushedDice.equals(currentDice.getUUID()))
{
dicesToRoll.put(currentDice, 1);
lastPushedDice = currentDice.getUUID();
totalRolls++;
}
}
currentDice = new Dice(sides);
}
}
}
if (lastPushedDice == null)
{
if (currentDice != null)
{
dicesToRoll.put(currentDice, 1);
totalRolls++;
}
} else
{
if (!lastPushedDice.equals(currentDice.getUUID()))
{
dicesToRoll.put(new Dice(currentDice), 1);
totalRolls++;
}
}
LinkedList<Dice> rolledDices = new LinkedList<>();
// in case no dice was specified (or invalid), roll a standard 6-sided dice.
if (dicesToRoll.isEmpty())
{
Dice standardDice = new Dice(6);
dicesToRoll.put(standardDice, 1);
totalRolls = 1;
}
for (Map.Entry<Dice, Integer> entry : dicesToRoll.entrySet())
{
Dice dice = entry.getKey();
Integer rollsToMake = entry.getValue();
for (int roll = 0; roll < rollsToMake; roll++)
{
dice.roll();
rolledDices.add(new Dice(dice));
}
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getAsTag(), 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)
{
int diceSize = dice.getSides();
if (previousDiceSides != diceSize)
{
message.append("\nd").append(diceSize).append(": ");
previousDiceSides = diceSize;
} else if (previousDiceSides != 0)
{
message.append(", ");
}
message.append("`").append(dice.getValue()).append("`");
total += dice.getValue();
}
// discord doesn't allow embed fields to be longer than 1024 and errors out
if (message.length() > 1024)
{
return new MessageResponse("Too many rolls!", null);
}
embedBuilder.addField("\uD83C\uDFB2 Rolls", message.toString(), false);
String rolls = totalRolls == 1 ? "roll" : "rolls";
embedBuilder.addField("✨ Total", totalRolls + " " + rolls + ": " + total, false);
return new MessageResponse(null, embedBuilder.build());
}
}

View File

@@ -0,0 +1,42 @@
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.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
public class Invite
{
private Invite()
{
throw new IllegalStateException("Utility class");
}
public static MessageEmbed generateEmbed()
{
EmbedBuilder embedBuilder = new EmbedBuilder();
//embed processing
{
embedBuilder.setColor(Cache.getBotColor());
String avatarUrl = HidekoBot.getAPI().getSelfUser().getAvatarUrl();
if (avatarUrl != null) embedBuilder.setThumbnail(avatarUrl);
embedBuilder.setTitle("Invite");
embedBuilder.appendDescription("Click on the button below to invite " +
Cache.getBotName() +
" to your server!");
}
return embedBuilder.build();
}
public static Button getInviteButton()
{
String inviteUrl = Cache.getInviteUrl();
return Button.link(inviteUrl, "Invite " + Cache.getBotName())
.withEmoji(Emoji.fromUnicode("\uD83C\uDF1F"));
}
}

View File

@@ -0,0 +1,54 @@
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 wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.concurrent.TimeUnit;
public class LoveCalculator
{
private LoveCalculator()
{
throw new IllegalStateException("Utility class");
}
public static MessageEmbed buildEmbedAndCacheResult(User author, User user1, User user2)
{
String userId1 = user1.getId();
String userId2 = user2.getId();
Integer loveAmount = Cache.getLoveCalculatorValue(userId1, userId2);
if (loveAmount == null)
{
loveAmount = RandomUtil.getRandomNumber(0, 100);
Cache.cacheLoveCalculatorValue(userId1, userId2, loveAmount);
Cache.getTaskScheduler().schedule(() ->
Cache.removeLoveCalculatorValue(userId1, userId2), 10, TimeUnit.MINUTES);
}
String formattedAmount = loveAmount + "%";
if (loveAmount <= 30) formattedAmount += "... \uD83D\uDE22";
else if (loveAmount < 60) formattedAmount += "! \uD83E\uDDD0";
else if (loveAmount < 75) formattedAmount += "!!! \uD83E\uDD73";
else formattedAmount = "" + formattedAmount + "!!! \uD83D\uDE0D\uD83D\uDCA5";
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setTitle("Love Calculator");
embedBuilder.addField("\uD83D\uDC65 People",
user1.getAsMention() + " & " + user2.getAsMention(),
false);
embedBuilder.addField("❤️\u200D\uD83D\uDD25 Match",
formattedAmount,
false);
return embedBuilder.build();
}
}

View File

@@ -0,0 +1,72 @@
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 wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class MagicBall
{
private MagicBall()
{
throw new IllegalStateException("Utility class");
}
public static LinkedList<String> getLabels()
{
return new LinkedList<>(Arrays.asList("8ball", "8b", "eightball", "magicball"));
}
private static final List<String> answers = new ArrayList<>(
Arrays.asList("It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes, definitely.",
"That would be a yes.",
"As I see it, yes.",
"Most likely.",
"Looks like it.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Seems uncertain.",
"Concentrate and ask again.",
"Don't count on it.",
"My answer is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful."));
public static String getRandomAnswer()
{
int answerPos = RandomUtil.getRandomNumber(0, answers.size() - 1);
return answers.get(answerPos);
}
public static MessageEmbed generateEmbed(String question, User author)
{
// add a question mark at the end, if missing.
// this might not always apply but it's fun
if (!question.endsWith("?")) question += "?";
String answer = getRandomAnswer();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setTitle("Magic Ball");
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.addField("❓ Question", question, false);
embedBuilder.addField("\uD83C\uDFB1 Answer", answer, false);
return embedBuilder.build();
}
}

View File

@@ -0,0 +1,113 @@
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 wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageResponse;
public class ProfileImage
{
private ProfileImage()
{
throw new IllegalStateException("Utility class");
}
public static 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++)
{
int cdistance = Math.abs(acceptedSizes[c] - resolution);
if (cdistance < distance)
{
idx = c;
distance = cdistance;
}
}
return acceptedSizes[idx];
}
public static MessageResponse buildResponse(int resolution, User user, ImageType imageType)
{
String imageTypeName = imageType.name().toLowerCase();
String resolutionString;
String imageLink = null;
User.Profile userProfile = user.retrieveProfile().complete();
ImageProxy bannerProxy = userProfile.getBanner();
if (imageType == ImageType.AVATAR)
{
resolutionString = resolution + " × " + resolution;
imageLink = user.getEffectiveAvatar().getUrl(resolution);
} else
{
int verticalRes = 361 * resolution / 1024;
resolutionString = resolution + " × " + verticalRes;
if (bannerProxy != null)
imageLink = bannerProxy.getUrl(resolution);
}
int[] acceptedSizes = Cache.getSupportedAvatarResolutions();
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("Profile " + imageTypeName);
embedBuilder.addField("User", user.getAsMention(), false);
embedBuilder.addField("Current resolution", resolutionString, false);
// string builder to create a string that links to all available resolutions
StringBuilder links = new StringBuilder();
for (int pos = 0; pos < acceptedSizes.length; pos++)
{
int currSize = acceptedSizes[pos];
String currLink;
if (imageType == ImageType.AVATAR)
{
currLink = user.getEffectiveAvatar().getUrl(currSize);
} else
{
if (bannerProxy == null) break;
currLink = bannerProxy.getUrl(currSize);
}
links.append("**[").append(currSize).append("px](").append(currLink).append(")**");
if (pos + 1 != acceptedSizes.length) // don't add a separator on the last iteration
{
links.append(" | ");
}
}
embedBuilder.addField("Available resolutions", links.toString(), false);
if (imageLink != null)
embedBuilder.setImage(imageLink);
if (imageLink == null)
{
String error = "I couldn't find " + user.getAsMention() + "'s " + imageTypeName + "!";
return new MessageResponse(error, null);
} else
{
return new MessageResponse(null, embedBuilder.build());
}
}
public enum ImageType
{
AVATAR, BANNER;
}
}

View File

@@ -0,0 +1,17 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.Permission;
public class Say
{
private Say()
{
throw new IllegalStateException("Utility class");
}
public static Permission getPermission()
{
return Permission.MESSAGE_MANAGE;
}
}

View File

@@ -0,0 +1,312 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
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;
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;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class Trivia
{
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 = new ArrayList<>();
// first string is the channelId, the list contain all users who responded there
public static HashMap<String, List<String>> channelAndWhoResponded = new HashMap<>();
// 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 String getTriviaLink(int categoryId)
{
return TRIVIA_API_LINK + categoryId;
}
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()
{
// todo nicer looking
return "Trivia is already running here!";
}
public static MessageResponse generateMainScreen()
{
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();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("\uD83C\uDFB2 Trivia");
embedBuilder.addField("\uD83D\uDCD6 Begin here",
"Select a category from the dropdown menu to start a match!",
false);
embedBuilder.addField("❓ How to play",
"A new question gets posted every few seconds." +
"\nIf you get it right, you earn points!" +
"\nIf you choose a wrong answer, you lose points." +
"\nIf you are unsure, you can wait without answering and your score won't change!",
false);
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("trivia_categories");
for (TriviaCategory category : categories)
{
String name = category.categoryName();
int id = category.categoryId();
menuBuilder.addOption(name, String.valueOf(id));
}
return new MessageResponse(null, embedBuilder.build(), menuBuilder.build());
}
public static JSONObject fetchJson(String link)
{
try
{
URL url = new URL(link);
URLConnection connection = url.openConnection();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String currentChar;
StringBuilder jsonStrBuilder = new StringBuilder();
while ((currentChar = bufferedReader.readLine()) != null)
{
jsonStrBuilder.append(currentChar);
}
bufferedReader.close();
return new JSONObject(jsonStrBuilder.toString());
} catch (IOException e)
{
LOGGER.error("JSON Parsing Exception", e);
}
return null;
}
public static List<TriviaQuestion> parseQuestions(JSONObject jsonObject)
{
List<TriviaQuestion> questions = new ArrayList<>();
JSONArray results = jsonObject.getJSONArray("results");
for (Object currentQuestionGeneric : results)
{
JSONObject questionJson = (JSONObject) currentQuestionGeneric;
String question = StringEscapeUtils.unescapeHtml4(questionJson.getString("question"));
String correctAnswer = StringEscapeUtils.unescapeHtml4(questionJson.getString("correct_answer"));
List<String> incorrectAnswersList = new ArrayList<>();
JSONArray incorrectAnswers = questionJson.getJSONArray("incorrect_answers");
for (Object incorrectAnswerGeneric : incorrectAnswers)
{
String incorrectAnswer = (String) incorrectAnswerGeneric;
incorrectAnswersList.add(StringEscapeUtils.unescapeHtml4(incorrectAnswer));
}
TriviaQuestion triviaQuestion = new TriviaQuestion(question, correctAnswer, incorrectAnswersList);
questions.add(triviaQuestion);
}
return questions;
}
public static List<TriviaCategory> parseCategories(JSONObject jsonObject)
{
List<TriviaCategory> categories = new ArrayList<>();
JSONArray categoriesArray = jsonObject.getJSONArray("trivia_categories");
for (Object categoryObject : categoriesArray)
{
JSONObject categoryJson = (JSONObject) categoryObject;
String name = categoryJson.getString("name");
int id = categoryJson.getInt("id");
categories.add(new TriviaCategory(name, id));
}
return categories;
}
public static void handleAnswer(ButtonInteractionEvent event, AnswerType answerType)
{
User user = event.getUser();
String channelId = event.getChannel().getId();
if (trackResponse(user, event.getChannel()))
{
LinkedList<TriviaScore> scores = channelAndScores.get(channelId);
if (scores == null) scores = new LinkedList<>();
TriviaScore currentUserScore = null;
for (TriviaScore score : scores)
{
if (score.getUser().equals(user))
{
currentUserScore = score;
scores.remove(score);
break;
}
}
if (currentUserScore == null)
{
currentUserScore = new TriviaScore(user);
}
if (answerType.equals(AnswerType.CORRECT))
{
event.reply(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();
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));
}
}
private static boolean trackResponse(User user, MessageChannel channel)
{
String userId = user.getId();
String channelId = channel.getId();
List<String> responders = channelAndWhoResponded.get(channelId);
if (responders == null)
{
responders = new ArrayList<>();
}
if (responders.isEmpty() || !responders.contains(userId))
{
responders.add(userId);
channelAndWhoResponded.put(channelId, responders);
return true; // response was successfully tracked
} else
{
return false; // response wasn't tracked because there already was an entry
}
}
public static void handleMenuSelection(StringSelectInteractionEvent event)
{
// check if the user interacting is the same one who ran the command
if (!(Cache.getDatabaseSource().isUserTrackedFor(event.getUser().getId(), event.getMessageId())))
{
event.reply("❌ 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());
SelectOption pickedOption = event.getInteraction().getSelectedOptions().get(0);
String categoryName = pickedOption.getLabel();
String categoryIdString = pickedOption.getValue();
Integer categoryId = Integer.parseInt(categoryIdString);
TriviaCategory category = new TriviaCategory(categoryName, categoryId);
startTrivia(event, category);
}
public static void startTrivia(StringSelectInteractionEvent event, TriviaCategory category)
{
User author = event.getUser();
Message message = event.getMessage();
MessageChannel channel = message.getChannel();
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);
return;
} else
{
// todo nicer looking
event.reply("Starting new Trivia session!").queue();
}
TriviaTask triviaTask = new TriviaTask(author, channel, category);
ScheduledFuture<?> future =
Cache.getTaskScheduler().scheduleAtFixedRate(triviaTask,
0,
15,
TimeUnit.SECONDS);
triviaTask.setScheduledFuture(future);
Trivia.channelsRunningTrivia.add(channel.getId());
}
public enum AnswerType
{
CORRECT, WRONG
}
}

View File

@@ -0,0 +1,344 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
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.ItemComponent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.apache.commons.text.StringEscapeUtils;
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.util.SerializationUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
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"));
}
public static String getBaseUrl()
{
return "https://www.urbandictionary.com/define.php?term=";
}
public static Button getPreviousPageButton()
{
return Button.primary("urban_previouspage", "Back")
.withEmoji(Emoji.fromFormatted("⬅️"));
}
public static Button getNextPageButton()
{
return Button.primary("urban_nextpage", "Next")
.withEmoji(Emoji.fromFormatted("➡️"));
}
public static Button getDeleteButton()
{
return Button.danger("generic_dismiss", "Delete")
.withEmoji(Emoji.fromFormatted("\uD83D\uDDD1"));
}
public static String getNoArgsError()
{
return "\uD83D\uDE22 I need to know what to search for!";
}
public static String sanitizeArgs(String term, boolean forUrl)
{
term = term.replaceAll("[^\\w\\s]", ""); // only keep letters, numbers and spaces
term = WordUtils.capitalizeFully(term); // Make Every Word Start With A Capital Letter
if (forUrl) term = term.replaceAll("\\s+", "+"); // replace all whitespaces with + for the url
if (term.length() > 64) term = term.substring(0, 64); // cut it to length to avoid abuse
return term;
}
public static String generateUrl(String term)
{
return getBaseUrl() + sanitizeArgs(term, true);
}
public static MessageEmbed buildEmbed(String term,
String url,
User author,
UrbanSearch search,
int page)
{
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle(term + ", on Urban Dictionary", url);
embedBuilder.setAuthor(author.getAsTag(), 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",
"*Entry " + (page + 1) + " | Sent by " + search.getContributorsNames().get(page) +
" on" + search.getSubmissionDates().get(page) + "*",
false);
return embedBuilder.build();
}
public static String getTermNotFoundError()
{
return "\uD83D\uDE22 I couldn't find that term!";
}
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(),
search.getSerializedExamples(),
search.getSerializedContributors(),
search.getSerializedDates(),
message,
sanitizedTerm);
}
public static void changePage(ButtonInteractionEvent event, ChangeType changeType)
{
String messageId = event.getMessageId();
DatabaseSource database = Cache.getDatabaseSource();
// check if the user interacting is the same one who ran the command
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);
String term = database.getUrbanTerm(messageId);
String url = generateUrl(term);
// get serialized parameters
String serializedMeanings = database.getUrbanMeanings(messageId);
String serializedExamples = database.getUrbanExamples(messageId);
String serializedContributors = database.getUrbanContributors(messageId);
String serializedDates = database.getUrbanDates(messageId);
// construct object by passing serialized parameters
UrbanSearch search = new UrbanSearch(serializedMeanings,
serializedExamples, serializedContributors, serializedDates);
// move to new page
if (changeType == ChangeType.NEXT)
page++;
else if (changeType == ChangeType.PREVIOUS)
page--;
term = UrbanDictionary.sanitizeArgs(term, false);
// generate embed with new results
MessageEmbed updatedEmbed = UrbanDictionary.buildEmbed(term, url, event.getUser(), search, page);
// get all attached components and check which ones need to be enabled or disabled
List<ItemComponent> components = new ArrayList<>();
if (page > 0)
{
components.add(UrbanDictionary.getPreviousPageButton().asEnabled());
} else
{
components.add(UrbanDictionary.getPreviousPageButton().asDisabled());
}
if (page + 1 == search.getPages())
{
components.add(UrbanDictionary.getNextPageButton().asDisabled());
} else
{
components.add(UrbanDictionary.getNextPageButton().asEnabled());
}
// update the components on the object
components.add(UrbanDictionary.getDeleteButton());
ActionRow currentRow = ActionRow.of(components);
// update the message
event.editComponents(currentRow).setEmbeds(updatedEmbed).queue();
database.setUrbanPage(messageId, page);
database.resetExpiryTimestamp(messageId);
}
public static class UrbanSearch
{
final LinkedList<String> plaintextMeanings;
final LinkedList<String> plaintextExamples;
final LinkedList<String> contributorsNames;
final LinkedList<String> submissionDates;
final String serializedMeanings;
final String serializedExamples;
final String serializedContributors;
final String serializedDates;
final int pages;
public UrbanSearch(String serializedMeanings,
String serializedExamples,
String serializedContributors,
String serializedDates)
{
this.serializedMeanings = serializedMeanings;
this.serializedExamples = serializedExamples;
this.serializedContributors = serializedContributors;
this.serializedDates = serializedDates;
this.plaintextMeanings = SerializationUtil.deserializeBase64(serializedMeanings);
this.plaintextExamples = SerializationUtil.deserializeBase64(serializedExamples);
this.contributorsNames = SerializationUtil.deserializeBase64(serializedContributors);
this.submissionDates = SerializationUtil.deserializeBase64(serializedDates);
this.pages = submissionDates.size();
}
public UrbanSearch(Elements definitions)
{
plaintextMeanings = new LinkedList<>();
plaintextExamples = new LinkedList<>();
contributorsNames = new LinkedList<>();
submissionDates = new LinkedList<>();
for (Element definition : definitions)
{
Elements meaningSingleton = definition.getElementsByClass("meaning");
if (meaningSingleton.isEmpty())
{
plaintextMeanings.add(" ");
} else
{
Element meaning = meaningSingleton.get(0);
String text = meaning.html()
.replaceAll("<br\\s*?>", "\n") // keep newlines
.replaceAll("<.*?>", ""); // remove all other html tags
// this is used to fix eg. &amp; being shown literally instead of being parsed
text = StringEscapeUtils.unescapeHtml4(text);
// discord only allows 1024 characters for embed fields
if (text.length() > 1024) text = text.substring(0, 1020) + "...";
plaintextMeanings.add(text);
}
Elements exampleSingleton = definition.getElementsByClass("example");
if (exampleSingleton.isEmpty())
{
plaintextExamples.add(" ");
} else
{
Element example = exampleSingleton.get(0);
String text = example.html()
.replaceAll("<br\\s*?>", "\n") // keep newlines
.replaceAll("<.*?>", ""); // remove all other html tags
// this is used to fix eg. &amp; being shown literally instead of being parsed
text = StringEscapeUtils.unescapeHtml4(text);
// discord only allows 1024 characters for embed fields
if (text.length() > 1024) text = text.substring(0, 1020) + "...";
plaintextExamples.add(text);
}
Elements contributorSingleton = definition.getElementsByClass("contributor");
if (contributorSingleton.isEmpty())
{
contributorsNames.add("Unknown");
} else
{
Element contributor = contributorSingleton.get(0);
String htmlContributor = contributor.html();
String htmlContributorName = contributor.select("a").html();
String htmlSubmitDate = htmlContributor.substring(
htmlContributor.indexOf("</a>") + 4);
contributorsNames.add(htmlContributorName
.replaceAll("<.*?>", "")); // remove all html tags
submissionDates.add(htmlSubmitDate
.replaceAll("<.*?>", "")); // remove all html tags
}
}
serializedMeanings = SerializationUtil.serializeBase64(plaintextMeanings);
serializedExamples = SerializationUtil.serializeBase64(plaintextExamples);
serializedContributors = SerializationUtil.serializeBase64(contributorsNames);
serializedDates = SerializationUtil.serializeBase64(submissionDates);
pages = submissionDates.size();
}
public List<String> getPlaintextMeanings()
{
return this.plaintextMeanings;
}
public List<String> getPlaintextExamples()
{
return this.plaintextExamples;
}
public List<String> getContributorsNames()
{
return this.contributorsNames;
}
public List<String> getSubmissionDates()
{
return this.submissionDates;
}
public String getSerializedMeanings()
{
return serializedMeanings;
}
public String getSerializedExamples()
{
return serializedExamples;
}
public String getSerializedContributors()
{
return serializedContributors;
}
public String getSerializedDates()
{
return serializedDates;
}
public int getPages()
{
return pages;
}
}
public enum ChangeType
{
NEXT,
PREVIOUS;
}
}

View File

@@ -0,0 +1,279 @@
package wtf.beatrice.hidekobot.commands.base;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
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.entities.channel.unions.MessageChannelUnion;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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 wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.util.FormatUtil;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class UserPunishment
{
private UserPunishment()
{
throw new IllegalStateException("Utility class");
}
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)
{
// this might take a sec
event.deferReply().queue();
User targetUser = null;
OptionMapping targetUserArg = event.getOption("target");
if (targetUserArg != null)
{
targetUser = targetUserArg.getAsUser();
}
List<IMentionable> mentions = null;
if (targetUser != null) mentions = new ArrayList<>(Collections.singletonList(targetUser));
String reason = null;
OptionMapping reasonArg = event.getOption("reason");
if (reasonArg != null)
{
reason = reasonArg.getAsString();
}
String timeDiff = null;
OptionMapping timeDiffArg = event.getOption("duration");
if (timeDiffArg != null)
{
timeDiff = timeDiffArg.getAsString();
}
// todo: the following code is not great, because we are making an array and then
// we are also recreating the string later in code. this is useless and a bit hacked on,
// but works for now. this happened because the function was NOT written with slash commands
// in mind, but with message commands, that send every word as a separate argument.
// we should probably rework the it so that it works better in both scenarios.
String[] reasonSplit = null;
// generate the arguments array by splitting the string
if (reason != null) reasonSplit = reason.split("\\s+");
//prepend timediff at index 0
if (timeDiff != null) reasonSplit = ArrayUtils.insert(0, reasonSplit, timeDiff);
// in message-commands, the first arg would contain the user mention. since we have no one mentioned here,
// because it's in its own argument, we just prepend an empty string. note that this makes relying on the
// first argument BAD, because it is no longer ensured that it contains the user mention.
if (timeDiff != null) reasonSplit = ArrayUtils.insert(0, reasonSplit, "");
MessageResponse response = getResponse(event.getUser(),
punishmentType,
event.getChannel(),
mentions,
reasonSplit);
if (response.embed() != null)
event.getHook().editOriginalEmbeds(response.embed()).queue();
else if (response.content() != null)
event.getHook().editOriginal(response.content()).queue();
}
public static void handle(MessageReceivedEvent event, String[] args, PunishmentType punishmentType)
{
Mentions msgMentions = event.getMessage().getMentions();
List<IMentionable> mentions = msgMentions.getMentions();
MessageResponse response = getResponse(event.getAuthor(),
punishmentType,
event.getChannel(),
mentions,
args);
if (response.embed() != null)
event.getMessage().replyEmbeds(response.embed()).queue();
else if (response.content() != null)
event.getMessage().reply(response.content()).queue();
}
public static MessageResponse getResponse(User author,
PunishmentType punishmentType,
MessageChannelUnion channel,
List<IMentionable> mentions,
String[] args)
{
String punishmentTypeName = punishmentType.name().toLowerCase();
if (!(channel instanceof TextChannel))
{
// todo nicer looking with emojis
return new MessageResponse("Sorry! I can't " + punishmentTypeName + " people in DMs.", null);
}
if (mentions == null || mentions.isEmpty())
{
// todo nicer looking with emojis
return new MessageResponse("You have to tell me who to " + punishmentTypeName + "!", null);
}
String mentionedId = mentions.get(0).getId();
User mentioned = null;
try
{
mentioned = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
} catch (RuntimeException ignored)
{
// todo nicer looking with emojis
return new MessageResponse("I can't " + punishmentTypeName + " that user!", null);
}
StringBuilder reasonBuilder = new StringBuilder();
String reason = "";
// some commands require an additional parameter before the reason, so in that case, we should start at 2.
int startingPoint = punishmentType == PunishmentType.TIMEOUT ? 2 : 1;
if (args != null && args.length > startingPoint)
{
for (int i = startingPoint; i < args.length; i++)
{
String arg = args[i];
reasonBuilder.append(arg);
if (i + 1 != arg.length())
reasonBuilder.append(" "); // separate args with a space except on last iteration.
}
reason = reasonBuilder.toString();
}
if (mentioned == null)
{
// todo nicer looking with emojis
return new MessageResponse("I can't " + punishmentTypeName + " that user!", null);
}
Guild guild = ((TextChannel) channel).getGuild();
Duration duration = null;
AuditableRestAction<Void> punishmentAction = null;
boolean impossible = false;
try
{
switch (punishmentType)
{
case BAN -> punishmentAction = guild.ban(mentioned, 0, TimeUnit.SECONDS);
case KICK -> punishmentAction = guild.kick(mentioned);
case TIMEOUT ->
{
if (args != null)
{
String durationStr = args[1];
duration = FormatUtil.parseDuration(durationStr);
}
boolean isDurationValid = true;
if (duration == null) isDurationValid = false;
else
{
if (duration.compareTo(maxTimeoutDuration) > 0) isDurationValid = false;
if (minTimeoutDuration.compareTo(duration) > 0) isDurationValid = false;
}
if (duration == null || !isDurationValid)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, but the specified duration is invalid!", null);
}
punishmentAction = guild.timeoutFor(mentioned, duration);
}
}
} 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);
try
{
punishmentAction.complete();
} catch (RuntimeException ignored)
{
// todo nicer looking with emojis
return new MessageResponse("Sorry, I couldn't " + punishmentTypeName + " " + mentioned.getAsMention() + "!",
null);
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("User " + punishmentType.getPastTense());
embedBuilder.addField("\uD83D\uDC64 User", mentioned.getAsMention(), false);
embedBuilder.addField("✂️ By", author.getAsMention(), false);
if (duration != null)
embedBuilder.addField("⏱️ Duration", FormatUtil.getNiceDuration(duration), false);
if (reason.isEmpty())
reason = "*No reason specified*";
embedBuilder.addField("\uD83D\uDCD6 Reason", reason, false);
return new MessageResponse(null, embedBuilder.build());
}
public enum PunishmentType
{
KICK("kicked"),
BAN("banned"),
TIMEOUT("timed out"),
;
private final String pastTense;
PunishmentType(String pastTense)
{
this.pastTense = pastTense;
}
public String getPastTense()
{
return pastTense;
}
}
}

View File

@@ -0,0 +1,41 @@
package wtf.beatrice.hidekobot.commands.completer;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.Command;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.commands.SlashArgumentsCompleterImpl;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.ArrayList;
import java.util.List;
public class ProfileImageCommandCompleter extends SlashArgumentsCompleterImpl
{
public ProfileImageCommandCompleter(SlashCommand parentCommand)
{
super(parentCommand);
}
@Override
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event)
{
if (event.getFocusedOption().getName().equals("size"))
{
List<Command.Choice> options = new ArrayList<>();
for (int res : Cache.getSupportedAvatarResolutions())
{
String resString = String.valueOf(res);
String userInput = event.getFocusedOption().getValue();
if (resString.startsWith(userInput))
options.add(new Command.Choice(resString, res));
}
event.replyChoices(options).queue();
}
}
}

View File

@@ -0,0 +1,84 @@
package wtf.beatrice.hidekobot.commands.message;
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 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.Arrays;
import java.util.LinkedList;
import java.util.List;
public class AliasCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("alias", "aliases"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@NotNull
@Override
public String getDescription()
{
return "See other command aliases.";
}
@Nullable
@Override
public String getUsage()
{
return "<command>";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
if (args.length == 0)
{
event.getMessage().reply("\uD83D\uDE20 Hey, you have to specify a command!").queue();
return;
}
String commandLabel = args[0].toLowerCase();
MessageCommand command = Cache.getMessageCommandListener().getRegisteredCommand(commandLabel);
if (command == null)
{
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
return;
}
String aliases = Alias.generateNiceAliases(command);
aliases = "Aliases for **" + command.getCommandLabels().get(0) + "**: " + aliases;
event.getMessage()
.reply(aliases)
.queue();
}
}

View File

@@ -0,0 +1,113 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Mentions;
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.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class AvatarCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("avatar"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Get someone's avatar, or your own. You can additionally specify a resolution.";
}
@Nullable
@Override
public String getUsage()
{
return "[mentioned user] [resolution]";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
User user;
int resolution = -1;
// we have no specific order for user and resolution, so let's try parsing any arg as resolution
// (mentions are handled differently by a specific method)
boolean resFound = false;
for (String arg : args)
{
try
{
int givenRes = Integer.parseInt(arg);
resolution = ProfileImage.parseResolution(givenRes);
resFound = true;
break;
} 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);
// check if someone is mentioned
Mentions mentions = event.getMessage().getMentions();
if (mentions.getMentions().isEmpty())
{
user = event.getAuthor();
} else
{
String mentionedId = mentions.getMentions().get(0).getId();
user = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
}
// in case of issues, fallback to the sender
if (user == null) user = event.getAuthor();
// send a response
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if (response.content() != null)
{
event.getMessage().reply(response.content()).queue();
} else if (response.embed() != null)
{
event.getMessage().replyEmbeds(response.embed()).queue();
}
}
}

View File

@@ -0,0 +1,64 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BanCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("ban"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return new ArrayList<Permission>(Collections.singletonList(Permission.BAN_MEMBERS));
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription()
{
return "Ban the mentioned user.";
}
@Nullable
@Override
public String getUsage()
{
return "<mentioned user> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.BAN);
}
}

View File

@@ -0,0 +1,113 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Mentions;
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.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class BannerCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("banner"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Get someone's profile banner, or your own.";
}
@Nullable
@Override
public String getUsage()
{
return "[mentioned user] [resolution]";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
User user;
int resolution = -1;
// we have no specific order for user and resolution, so let's try parsing any arg as resolution
// (mentions are handled differently by a specific method)
boolean resFound = false;
for (String arg : args)
{
try
{
int givenRes = Integer.parseInt(arg);
resolution = ProfileImage.parseResolution(givenRes);
resFound = true;
break;
} 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);
// check if someone is mentioned
Mentions mentions = event.getMessage().getMentions();
if (mentions.getMentions().isEmpty())
{
user = event.getAuthor();
} else
{
String mentionedId = mentions.getMentions().get(0).getId();
user = HidekoBot.getAPI().retrieveUserById(mentionedId).complete();
}
// in case of issues, fallback to the sender
if (user == null) user = event.getAuthor();
// send a response
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
if (response.content() != null)
{
event.getMessage().reply(response.content()).queue();
} else if (response.embed() != null)
{
event.getMessage().replyEmbeds(response.embed()).queue();
}
}
}

View File

@@ -0,0 +1,76 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
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 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.LinkedList;
import java.util.List;
public class BotInfoCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("botinfo", "info"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Get general info about the bot.";
}
@Nullable
@Override
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
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)
{
commandNames.add(command.getCommandLabels().get(0));
}
// send the list
MessageEmbed embed = BotInfo.generateEmbed(commandNames);
event.getMessage().replyEmbeds(embed).queue();
}
}

View File

@@ -0,0 +1,121 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
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 wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class ClearCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList(ClearChat.getLabel()));
}
@Override
public List<Permission> getPermissions()
{
return Collections.singletonList(ClearChat.getPermission());
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription()
{
return "Clear the current channel's chat history.";
}
@Nullable
@Override
public String getUsage()
{
return "[amount]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
// check if user is trying to run command in dms.
String error = ClearChat.checkDMs(event.getChannel());
if (error != null)
{
event.getMessage().reply(error).queue();
return;
}
// get the amount from the command args.
Integer toDeleteAmount;
if (args.length == 0) toDeleteAmount = 1;
else
{
try
{
toDeleteAmount = Integer.parseInt(args[0]);
} catch (NumberFormatException e)
{
toDeleteAmount = 0;
}
}
// cap the amount to avoid abuse.
if (toDeleteAmount > ClearChat.getMaxAmount()) toDeleteAmount = 0;
error = ClearChat.checkDeleteAmount(toDeleteAmount);
if (error != null)
{
event.getMessage().reply(error).queue();
return;
}
// answer by saying that the operation has begun.
String content = "\uD83D\uDEA7 Clearing...";
Message botMessage = event.getMessage().reply(content).complete();
int deleted = ClearChat.delete(toDeleteAmount,
event.getMessageIdLong(),
event.getChannel());
// get a nicely formatted message that logs the deletion of messages.
content = ClearChat.parseAmount(deleted);
// edit the message text and attach a button.
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());
// delete the sender's message.
event.getMessage().delete().queue();
// delete the "clearing" info message.
botMessage.delete().queue();
}
}

View File

@@ -0,0 +1,72 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.commands.base.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class CoinFlipCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("coinflip", "flip", "flipcoin"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // null because it can be used anywhere
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Flip a coin.";
}
@Nullable
@Override
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
// perform coin flip
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) -> {
});
}
}

View File

@@ -0,0 +1,81 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.commands.base.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class DiceRollCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("diceroll", "droll", "roll"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
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()
{
return "[dice size] [rolls]";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
MessageResponse response = DiceRoll.buildResponse(event.getAuthor(), args);
if (response.content() != null)
{
event.getMessage().reply(response.content()).queue();
} else if (response.embed() != null)
{
event.getMessage().replyEmbeds(response.embed()).queue();
}
}
}

View File

@@ -0,0 +1,63 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class HelloCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("hi", "hello", "heya"));
}
@Override
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Get pinged by the bot.";
}
@Nullable
@Override
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String sender = event.getMessage().getAuthor().getAsMention();
event.getMessage().reply("Hi, " + sender + "! :sparkles:").queue();
}
}

View File

@@ -0,0 +1,165 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
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 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.*;
public class HelpCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("help"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
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()
{
return "[command]";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
LinkedHashMap<CommandCategory, LinkedList<MessageCommand>> commandCategories = new LinkedHashMap<>();
if (args.length == 0)
{
for (CommandCategory category : CommandCategory.values())
{
LinkedList<MessageCommand> commandsOfThisCategory = new LinkedList<>();
for (MessageCommand command : Cache.getMessageCommandListener().getRegisteredCommands())
{
if (command.getCategory().equals(category))
{
commandsOfThisCategory.add(command);
}
}
commandCategories.put(category, commandsOfThisCategory);
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("Bot Help");
embedBuilder.addField("General Help",
"Type `" + Cache.getBotPrefix() + " help [command]` to get help on a specific command." +
"\nYou will find a list of commands organized in categories below.",
false);
for (Map.Entry<CommandCategory, LinkedList<MessageCommand>> entry : commandCategories.entrySet())
{
StringBuilder commandsList = new StringBuilder();
CommandCategory category = entry.getKey();
LinkedList<MessageCommand> commandsOfThisCategory = entry.getValue();
for (int pos = 0; pos < commandsOfThisCategory.size(); pos++)
{
MessageCommand command = commandsOfThisCategory.get(pos);
commandsList.append("`").append(command.getCommandLabels().get(0)).append("`");
if (pos + 1 != commandsOfThisCategory.size())
commandsList.append(", "); // separate with comma except on last run
}
String niceCategoryName = category.name().replace("_", " ");
niceCategoryName = WordUtils.capitalizeFully(niceCategoryName);
niceCategoryName = category.getEmoji() + " " + niceCategoryName;
embedBuilder.addField(niceCategoryName, commandsList.toString(), false);
}
event.getMessage().replyEmbeds(embedBuilder.build()).queue();
} else
{
String commandLabel = args[0].toLowerCase();
MessageCommand command = Cache.getMessageCommandListener().getRegisteredCommand(commandLabel);
if (command == null)
{
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
return;
}
commandLabel = command.getCommandLabels().get(0);
String usage = "`" + Cache.getBotPrefix() + " " + commandLabel;
String internalUsage = command.getUsage();
if (internalUsage != null) usage += " " + internalUsage;
usage += "`";
String aliases = Alias.generateNiceAliases(command);
List<Permission> permissions = command.getPermissions();
StringBuilder permissionsStringBuilder = new StringBuilder();
if (permissions == null)
{
permissionsStringBuilder = new StringBuilder("Available to everyone");
} else
{
for (int i = 0; i < permissions.size(); i++)
{
Permission permission = permissions.get(i);
permissionsStringBuilder.append("**").append(permission.getName()).append("**");
if (i + 1 != permissions.size())
permissionsStringBuilder.append(", "); // separate with comma expect on last iteration
}
}
String title = command.getCategory().getEmoji() +
" \"" + WordUtils.capitalizeFully(commandLabel + "\" help");
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle(title);
embedBuilder.addField("Description", command.getDescription(), false);
embedBuilder.addField("Usage", usage, false);
embedBuilder.addField("Aliases", aliases, false);
embedBuilder.addField("Permissions", permissionsStringBuilder.toString(), false);
event.getMessage().replyEmbeds(embedBuilder.build()).queue();
}
}
}

View File

@@ -0,0 +1,88 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.emoji.Emoji;
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 wtf.beatrice.hidekobot.commands.base.Invite;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class InviteCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("invite"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Get the bot's invite link.";
}
@Nullable
@Override
public String getUsage()
{
return null;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
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())
{
event.getAuthor().openPrivateChannel().queue(privateChannel ->
{
privateChannel.sendMessageEmbeds(inviteEmbed)
.addActionRow(inviteButton)
.queue();
event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
}, error -> event.getMessage().addReaction(Emoji.fromUnicode("")).queue());
} else
{
event.getMessage()
.replyEmbeds(inviteEmbed)
.addActionRow(inviteButton)
.queue();
}
}
}

View File

@@ -0,0 +1,64 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class KickCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("kick"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return new ArrayList<Permission>(Collections.singletonList(Permission.KICK_MEMBERS));
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription()
{
return "Kick the mentioned user from the guild.";
}
@Nullable
@Override
public String getUsage()
{
return "<mentioned user> [reason]";
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
UserPunishment.handle(event, args, UserPunishment.PunishmentType.KICK);
}
}

View File

@@ -0,0 +1,98 @@
package wtf.beatrice.hidekobot.commands.message;
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.MessageEmbed;
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.HidekoBot;
import wtf.beatrice.hidekobot.commands.base.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class LoveCalculatorCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Arrays.asList("lovecalc", "lovecalculator", "lc"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; //anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
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()
{
return "<person 1> [person 2]";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
Mentions mentionsObj = event.getMessage().getMentions();
List<IMentionable> mentions = mentionsObj.getMentions();
if (args.length == 0 || mentions.isEmpty())
{
event.getMessage()
.reply("\uD83D\uDE22 I need to know who to check! Please mention them.")
.queue();
return;
}
User user1, user2;
String mentionedUserId = mentions.get(0).getId();
user1 = HidekoBot.getAPI().retrieveUserById(mentionedUserId).complete();
if (mentions.size() == 1)
{
user2 = event.getAuthor();
} else
{
mentionedUserId = mentions.get(1).getId();
user2 = HidekoBot.getAPI().retrieveUserById(mentionedUserId).complete();
}
MessageEmbed embed = LoveCalculator.buildEmbedAndCacheResult(event.getAuthor(), user1, user2);
event.getChannel().sendMessageEmbeds(embed).queue();
}
}

View File

@@ -0,0 +1,80 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.commands.base.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.LinkedList;
import java.util.List;
public class MagicBallCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return MagicBall.getLabels();
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; // anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Ask a question to the Magic Ball.";
}
@Nullable
@Override
public String getUsage()
{
return "<question>";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
if (args.length == 0)
{
event.getMessage().reply("You need to specify a question!").queue();
return;
}
StringBuilder questionBuilder = new StringBuilder();
for (int i = 0; i < args.length; i++)
{
String arg = args[i];
questionBuilder.append(arg);
if (i + 1 != args.length) // don't add a separator on the last iteration
questionBuilder.append(" ");
}
String question = questionBuilder.toString();
event.getChannel().sendMessageEmbeds(MagicBall.generateEmbed(question, event.getAuthor())).queue();
}
}

View File

@@ -0,0 +1,87 @@
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 wtf.beatrice.hidekobot.commands.base.Say;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class SayCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("say"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return Collections.singletonList(Say.getPermission());
}
@Override
public boolean passRawArgs()
{
return true;
}
@NotNull
@Override
public String getDescription()
{
return "Make the bot say something for you.";
}
@Nullable
@Override
public String getUsage()
{
return "<text>";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.TOOLS;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String messageContent;
if (args.length != 0 && !args[0].isEmpty())
{
messageContent = args[0];
} else
{
event.getMessage().reply("\uD83D\uDE20 Hey, you have to tell me what to say!")
.queue();
return;
}
event.getChannel().sendMessage(messageContent).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
});
}
}
}

View File

@@ -0,0 +1,64 @@
package wtf.beatrice.hidekobot.commands.message;
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 wtf.beatrice.hidekobot.commands.base.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class TimeoutCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("timeout"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return new ArrayList<Permission>(Collections.singletonList(Permission.MODERATE_MEMBERS));
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.MODERATION;
}
@NotNull
@Override
public String getDescription()
{
return "Timeout the mentioned user.";
}
@Nullable
@Override
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);
}
}

View File

@@ -0,0 +1,104 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
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;
import wtf.beatrice.hidekobot.commands.base.Trivia;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class TriviaCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return new LinkedList<>(Collections.singletonList("trivia"));
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null;
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@NotNull
@Override
public String getDescription()
{
return "Start a Trivia session and play with others!";
}
@Nullable
@Override
public String getUsage()
{
return null;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
MessageChannel channel = event.getChannel();
if (!(channel instanceof TextChannel))
{
channel.sendMessage(Trivia.getNoDMsError()).queue();
return;
}
if (Trivia.channelsRunningTrivia.contains(channel.getId()))
{
// todo: also what if the bot stops (database...?)
// todo: also what if the message is already deleted
Message err = event.getMessage().reply(Trivia.getTriviaAlreadyRunningError()).complete();
Cache.getTaskScheduler().schedule(() -> err.delete().queue(), 10, TimeUnit.SECONDS);
return;
}
MessageResponse response = Trivia.generateMainScreen();
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)
{
if (response.components() != null) responseAction = responseAction.addActionRow(response.components());
responseAction.queue(message -> {
Cache.getDatabaseSource().trackRanCommandReply(message, event.getAuthor());
Cache.getDatabaseSource().queueDisabling(message);
});
}
}
}

View File

@@ -0,0 +1,115 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.MessageEmbed;
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.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import wtf.beatrice.hidekobot.commands.base.UrbanDictionary;
import wtf.beatrice.hidekobot.objects.commands.CommandCategory;
import wtf.beatrice.hidekobot.objects.commands.MessageCommand;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class UrbanDictionaryCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels()
{
return UrbanDictionary.getCommandLabels();
}
@Nullable
@Override
public List<Permission> getPermissions()
{
return null; //anyone can use it
}
@Override
public boolean passRawArgs()
{
return false;
}
@NotNull
@Override
public String getDescription()
{
return "Look something up in the Urban Dictionary.";
}
@Nullable
@Override
public String getUsage()
{
return "<query>";
}
@NotNull
@Override
public CommandCategory getCategory()
{
return CommandCategory.FUN;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
if (args.length == 0)
{
event.getMessage().reply(UrbanDictionary.getNoArgsError()).queue();
return;
}
// 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++)
{
String arg = args[i];
termBuilder.append(arg);
if (i + 1 != args.length) // add spaces between args, but not on the last run
termBuilder.append(" ");
}
String term = UrbanDictionary.sanitizeArgs(termBuilder.toString(), false);
String url = UrbanDictionary.generateUrl(term);
Document doc;
try
{
doc = Jsoup.connect(url).get();
} catch (IOException e)
{
event.getMessage().reply(UrbanDictionary.getTermNotFoundError()).queue();
return;
}
Elements definitions = doc.getElementsByClass("definition");
UrbanDictionary.UrbanSearch search = new UrbanDictionary.UrbanSearch(definitions);
MessageEmbed embed = UrbanDictionary.buildEmbed(term, url, event.getAuthor(), search, 0);
// disable next page if we only have one result
Button nextPageBtnLocal = UrbanDictionary.getNextPageButton();
if (search.getPages() == 1) nextPageBtnLocal = nextPageBtnLocal.asDisabled();
event.getChannel()
.sendMessageEmbeds(embed)
.addActionRow(UrbanDictionary.getPreviousPageButton().asDisabled(),
//disabled by default because we're on page 0
nextPageBtnLocal,
UrbanDictionary.getDeleteButton())
.queue(message -> UrbanDictionary.track(message, event.getAuthor(), search, term));
}
}

View File

@@ -0,0 +1,62 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class AvatarCommand extends SlashCommandImpl
{
@Override
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.",
false,
true);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// defer reply because this might take a moment
event.deferReply().queue();
User user;
int resolution;
OptionMapping userArg = event.getOption("user");
if (userArg != null)
{
user = userArg.getAsUser();
} else
{
user = event.getUser();
}
OptionMapping sizeArg = event.getOption("size");
if (sizeArg != null)
{
resolution = ProfileImage.parseResolution(sizeArg.getAsInt());
} else
{
resolution = ProfileImage.parseResolution(512);
}
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.AVATAR);
if (response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
} else if (response.embed() != null)
{
event.getHook().editOriginalEmbeds(response.embed()).queue();
}
}
}

View File

@@ -0,0 +1,36 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
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.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class BanCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("ban", "Ban someone from the guild.")
.addOption(OptionType.MENTIONABLE, "target",
"The member user to ban.",
true,
false)
.addOption(OptionType.STRING, "reason",
"The reason for the punishment.",
false,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.BAN);
}
}

View File

@@ -0,0 +1,62 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.ProfileImage;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class BannerCommand extends SlashCommandImpl
{
@Override
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.",
false,
true);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// defer reply because this might take a moment
event.deferReply().queue();
User user;
int resolution;
OptionMapping userArg = event.getOption("user");
if (userArg != null)
{
user = userArg.getAsUser();
} else
{
user = event.getUser();
}
OptionMapping sizeArg = event.getOption("size");
if (sizeArg != null)
{
resolution = ProfileImage.parseResolution(sizeArg.getAsInt());
} else
{
resolution = ProfileImage.parseResolution(512);
}
MessageResponse response = ProfileImage.buildResponse(resolution, user, ProfileImage.ImageType.BANNER);
if (response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
} else if (response.embed() != null)
{
event.getHook().editOriginalEmbeds(response.embed()).queue();
}
}
}

View File

@@ -0,0 +1,43 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.Cache;
import wtf.beatrice.hidekobot.commands.base.BotInfo;
import wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.util.LinkedList;
import java.util.List;
public class BotInfoCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("botinfo", "Get info about the bot.");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// defer reply because this might take a moment
event.deferReply().queue();
// get a list of slash commands
List<SlashCommand> registeredCommands = Cache.getSlashCommandListener().getRegisteredCommands();
LinkedList<String> registeredCommandNames = new LinkedList<>();
for (SlashCommand command : registeredCommands)
{
// node: adding slash so people realize that this is specific about slash commands.
registeredCommandNames.add("/" + command.getCommandName());
}
// send the list
MessageEmbed embed = BotInfo.generateEmbed(registeredCommandNames);
event.getHook().editOriginalEmbeds(embed).queue();
}
}

View File

@@ -0,0 +1,79 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.Message;
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 net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.ClearChat;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class ClearCommand extends SlashCommandImpl
{
@Override
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()));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.deferReply().queue();
// check if user is trying to run command in dms.
String error = ClearChat.checkDMs(event.getChannel());
if (error != null)
{
event.getHook().editOriginal(error).queue();
return;
}
/* get the amount from the command args.
NULL should not be possible because we specified them as mandatory,
but apparently the mobile app doesn't care and still sends the command if you omit the args. */
OptionMapping amountOption = event.getOption("amount");
int toDeleteAmount = amountOption == null ? 1 : amountOption.getAsInt();
// cap the amount to avoid abuse.
if (toDeleteAmount > ClearChat.getMaxAmount()) toDeleteAmount = 0;
error = ClearChat.checkDeleteAmount(toDeleteAmount);
if (error != null)
{
event.getHook().editOriginal(error).queue();
return;
}
// answer by saying that the operation has begun.
String content = "\uD83D\uDEA7 Clearing...";
Message botMessage = event.getHook().editOriginal(content).complete();
// actually delete the messages.
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);
// edit the message text and attach a button.
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());
}
}

View File

@@ -0,0 +1,39 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.CoinFlip;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class CoinFlipCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("coinflip",
"Flip a coin and get head or tails.");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// perform coin flip
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) -> {
});
}
}

View File

@@ -0,0 +1,50 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.DiceRoll;
import wtf.beatrice.hidekobot.objects.MessageResponse;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class DiceRollCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("diceroll", "Roll dice. You can roll multiple dice at the same time.")
.addOption(OptionType.STRING, "query",
"The dice to roll.",
false,
false);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.deferReply().queue();
OptionMapping textOption = event.getOption("query");
String messageContent = "";
if (textOption != null)
{
messageContent = textOption.getAsString();
}
String[] args = messageContent.split("\\s");
MessageResponse response = DiceRoll.buildResponse(event.getUser(), args);
if (response.content() != null)
{
event.getHook().editOriginal(response.content()).queue();
} else if (response.embed() != null)
{
event.getHook().editOriginalEmbeds(response.embed()).queue();
}
}
}

View File

@@ -0,0 +1,40 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
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.Cache;
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
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("die", "Stop the bot's process.")
.setDefaultPermissions(DefaultMemberPermissions.DISABLED);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
if (Cache.getBotOwnerId() != event.getUser().getIdLong())
{
event.reply("Sorry, only the bot owner can run this command!").setEphemeral(true).queue();
} else
{
event.reply("Going to sleep! Cya ✨").queue();
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.schedule(HidekoBot::shutdown, 3, TimeUnit.SECONDS);
}
}
}
}

View File

@@ -0,0 +1,37 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.Cache;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class HelpCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("help",
"Get general help on the bot.");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// defer reply because replying might take a while
event.deferReply().queue();
EmbedBuilder embedBuilder = new EmbedBuilder();
// embed processing
{
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("Help");
}
event.getHook().editOriginalEmbeds(embedBuilder.build()).queue();
}
}

View File

@@ -0,0 +1,49 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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 net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.commands.base.Invite;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class InviteCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("invite", "Get an invite link for the bot.");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// defer reply because this might take a moment
ReplyCallbackAction replyCallbackAction = event.deferReply();
// only make message permanent in DMs
if (event.getChannelType() != ChannelType.PRIVATE)
{
replyCallbackAction = replyCallbackAction.setEphemeral(true);
}
replyCallbackAction.queue();
MessageEmbed inviteEmbed = Invite.generateEmbed();
Button inviteButton = Invite.getInviteButton();
WebhookMessageEditAction<Message> reply =
event.getHook()
.editOriginalEmbeds(inviteEmbed)
.setActionRow(inviteButton);
reply.queue();
}
}

View File

@@ -0,0 +1,36 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
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.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class KickCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("kick", "Kick someone from the guild.")
.addOption(OptionType.MENTIONABLE, "target",
"The member user to kick.",
true,
false)
.addOption(OptionType.STRING, "reason",
"The reason for the punishment.",
false,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.KICK_MEMBERS));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.KICK);
}
}

View File

@@ -0,0 +1,63 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.LoveCalculator;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class LoveCalculatorCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("lovecalc",
"Calculate how much two people love each other.")
.addOption(OptionType.MENTIONABLE,
"first",
"The first person to account for",
true)
.addOption(OptionType.MENTIONABLE,
"second",
"The second person to account for",
false);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
User firstUser, secondUser;
OptionMapping firsUserArg = event.getOption("first");
if (firsUserArg != null)
{
firstUser = firsUserArg.getAsUser(); //todo null check?
} else
{
event.reply("\uD83D\uDE22 I need to know who to check! Please mention them.")
.setEphemeral(true)
.queue();
return;
}
OptionMapping secondUserArg = event.getOption("second");
if (secondUserArg != null)
{
secondUser = secondUserArg.getAsUser(); //todo null check?
} else
{
secondUser = event.getUser();
}
MessageEmbed embed = LoveCalculator.buildEmbedAndCacheResult(event.getUser(), firstUser, secondUser);
event.replyEmbeds(embed).queue();
}
}

View File

@@ -0,0 +1,49 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.MagicBall;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class MagicBallCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash(MagicBall.getLabels().get(0),
"Ask a question to the magic ball.")
.addOption(OptionType.STRING, "question",
"The question to ask.",
true,
false);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
// get the asked question
OptionMapping textOption = event.getOption("question");
String question = "";
if (textOption != null)
{
question = textOption.getAsString();
}
if (textOption == null || question.isEmpty())
{
event.reply("\uD83D\uDE20 Hey, you have to ask me a question!")
.setEphemeral(true)
.queue();
return;
}
MessageEmbed response = MagicBall.generateEmbed(question, event.getUser());
event.replyEmbeds(response).queue();
}
}

View File

@@ -0,0 +1,24 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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.objects.commands.SlashCommandImpl;
public class PingCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("ping",
"Test if the bot is responsive.");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.reply("Pong!").queue();
}
}

View File

@@ -0,0 +1,54 @@
package wtf.beatrice.hidekobot.commands.slash;
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 wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class SayCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("say", "Make the bot say something.")
.addOption(OptionType.STRING, "text",
"The message to send.",
true,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Say.getPermission()));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
MessageChannel channel = event.getChannel();
// get the text to send
OptionMapping textOption = event.getOption("text");
String messageContent = "";
if (textOption != null)
{
messageContent = textOption.getAsString();
}
if (textOption == null || messageContent.isEmpty())
{
event.reply("\uD83D\uDE20 Hey, you have to tell me what to say!")
.setEphemeral(true)
.queue();
return;
}
channel.sendMessage(messageContent).queue();
event.reply("Message sent! ✨")
.setEphemeral(true)
.queue();
}
}

View File

@@ -0,0 +1,40 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
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.UserPunishment;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
public class TimeoutCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("timeout", "Timeout someone in the guild.")
.addOption(OptionType.MENTIONABLE, "target",
"The member user to time out.",
true,
false)
.addOption(OptionType.STRING, "duration",
"The duration of the timeout.",
true,
false)
.addOption(OptionType.STRING, "reason",
"The reason for the punishment.",
false,
false)
.setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MODERATE_MEMBERS));
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
UserPunishment.handle(event, UserPunishment.PunishmentType.TIMEOUT);
}
}

View File

@@ -0,0 +1,51 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
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.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
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
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash("trivia",
"Start a Trivia session and play with others!");
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
MessageChannel channel = event.getChannel();
if (!(channel instanceof TextChannel))
{
event.reply(Trivia.getNoDMsError()).queue();
return;
}
if (Trivia.channelsRunningTrivia.contains(channel.getId()))
{
event.reply(Trivia.getTriviaAlreadyRunningError()).setEphemeral(true).queue();
return;
}
// if we got here, this might take a bit
event.deferReply().queue();
MessageResponse response = Trivia.generateMainScreen();
event.getHook().editOriginalEmbeds(response.embed()).setActionRow(response.components()).queue(message ->
{
Cache.getDatabaseSource().trackRanCommandReply(message, event.getUser());
Cache.getDatabaseSource().queueDisabling(message);
});
}
}

View File

@@ -0,0 +1,83 @@
package wtf.beatrice.hidekobot.commands.slash;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
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 net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import wtf.beatrice.hidekobot.commands.base.UrbanDictionary;
import wtf.beatrice.hidekobot.objects.commands.SlashCommandImpl;
import java.io.IOException;
public class UrbanDictionaryCommand extends SlashCommandImpl
{
@Override
public CommandData getSlashCommandData()
{
return Commands.slash(UrbanDictionary.getCommandLabels().get(0),
"Look up a term on Urban Dictionary.")
.addOption(OptionType.STRING, "term", "The term to look up", true);
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.deferReply().queue();
// get the term to look up
OptionMapping textOption = event.getOption("term");
String term = "";
if (textOption != null)
{
term = textOption.getAsString();
}
if (textOption == null || term.isEmpty())
{
event.reply(UrbanDictionary.getNoArgsError())
.setEphemeral(true)
.queue();
return;
}
final String sanitizedTerm = UrbanDictionary.sanitizeArgs(term, false);
String url = UrbanDictionary.generateUrl(sanitizedTerm);
Document doc;
try
{
doc = Jsoup.connect(url).get();
} catch (IOException e)
{
event.reply(UrbanDictionary.getTermNotFoundError())
.setEphemeral(true)
.queue();
return;
}
Elements definitions = doc.getElementsByClass("definition");
UrbanDictionary.UrbanSearch search = new UrbanDictionary.UrbanSearch(definitions);
MessageEmbed embed = UrbanDictionary.buildEmbed(sanitizedTerm, url, event.getUser(), search, 0);
// disable next page if we only have one result
Button nextPageBtnLocal = UrbanDictionary.getNextPageButton();
if (search.getPages() == 1) nextPageBtnLocal = nextPageBtnLocal.asDisabled();
ActionRow actionRow = ActionRow.of(UrbanDictionary.getPreviousPageButton().asDisabled(),
//disabled by default because we're on page 0
nextPageBtnLocal,
UrbanDictionary.getDeleteButton());
event.getHook().editOriginalEmbeds(embed).setComponents(actionRow).queue(message ->
UrbanDictionary.track(message, event.getUser(), search, sanitizedTerm));
}
}

View File

@@ -0,0 +1,33 @@
package wtf.beatrice.hidekobot.datasources;
public enum ConfigurationEntry
{
BOT_TOKEN("bot-token", "MTAxMjUzNzI5MTMwODI4NjAyMw.GWeNuh.00000000000000000000000000000000000000"),
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;
}
}

View File

@@ -0,0 +1,161 @@
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 java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
public class ConfigurationSource
{
private final LinkedHashMap<String, Object> configurationEntries = new LinkedHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationSource.class);
private final String configFilePath;
public ConfigurationSource(String configFilePath)
{
this.configFilePath = configFilePath;
}
public void initConfig()
{
// load the YAML file from the archive's resources folder
/*
* note: this is no longer technically a YAML file, but we are using a very similar structure
* to what SnakeYaml does, so that it can map all entries directly to a YAML file itself.
* we used to have a config.yml file in the "resources" folder, but that is no longer necessary.
*/
LinkedHashMap<String, Object> internalConfigContents = new LinkedHashMap<>(); // map holding all file entries
for (ConfigurationEntry entry : ConfigurationEntry.values())
{
internalConfigContents.put(entry.getPath(), entry.getDefaultValue());
}
if (internalConfigContents.isEmpty())
{
LOGGER.error("Error reading internal configuration!");
HidekoBot.shutdown();
return;
}
// check if config files exists in filesystem
File fsConfigFile = new File(configFilePath);
if (!fsConfigFile.exists())
{
// try to create config file
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
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.error(e.getMessage());
}
if (fsConfigContents == null) // if file contents are empty or corrupted...
{
// "clean" them (this effectively forces a config file reset)
fsConfigContents = new LinkedHashMap<>();
}
// check for missing keys
boolean missingKeys = false;
for (String key : internalConfigContents.keySet())
{
// if key is missing
if (!fsConfigContents.containsKey(key))
{
// quit and flag it, as we need to complete the file with the missing ones
missingKeys = true;
break;
}
}
// if keys are missing
if (missingKeys)
{
// 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 (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, copy the value from the example config file
filledEntries.put(key, entry.getValue());
}
}
try
{
// new writer to actually write the contents to the file
PrintWriter missingKeysWriter = new PrintWriter(fsConfigFile);
// set yaml options to make the output prettier
DumperOptions dumperOptions = new DumperOptions();
dumperOptions.setIndent(2);
dumperOptions.setPrettyFlow(true);
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
// create the yaml object and dump the values to filesystem
Yaml yaml = new Yaml(dumperOptions);
yaml.dump(filledEntries, missingKeysWriter);
} catch (FileNotFoundException e)
{
LOGGER.error(e.getMessage());
HidekoBot.shutdown();
return;
}
// finally, dump all entries to cache.
loadConfig(filledEntries);
} else
{
// if no key is missing, just cache all entries and values from filesystem.
loadConfig(fsConfigContents);
}
}
private void loadConfig(LinkedHashMap<String, Object> configurationEntries)
{
this.configurationEntries.putAll(configurationEntries);
}
public Object getConfigValue(ConfigurationEntry key)
{
return configurationEntries.get(key.getPath());
}
}

View File

@@ -0,0 +1,682 @@
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 org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class DatabaseSource
{
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(DatabaseSource.class);
private static final String JDBC_URL = "jdbc:sqlite:%path%";
private Connection dbConnection = null;
private final String dbPath;
public DatabaseSource(String dbPath)
{
this.dbPath = dbPath;
}
private void logException(SQLException e)
{
LOGGER.error("Database Exception", e);
}
public boolean connect()
{
String url = JDBC_URL.replace("%path%", dbPath);
if (!close()) return false;
try
{
dbConnection = DriverManager.getConnection(url);
LOGGER.info("Database connection established!");
return true;
} catch (SQLException e)
{
logException(e);
return false;
}
}
public boolean close()
{
if (dbConnection != null)
{
try
{
if (!dbConnection.isClosed())
{
dbConnection.close();
}
} catch (SQLException e)
{
logException(e);
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,
message_id TEXT NOT NULL,
user_id TEXT NOT NULL,
channel_type TEXT NOT NULL);
""");
newTables.add("""
CREATE TABLE IF NOT EXISTS urban_dictionary (
message_id TEXT NOT NULL,
page INTEGER NOT NULL,
meanings TEXT NOT NULL,
examples TEXT NOT NULL,
contributors TEXT NOT NULL,
dates TEXT NOT NULL,
term TEXT NOT NULL
);
""");
for (String sql : newTables)
{
try (Statement stmt = dbConnection.createStatement())
{
// execute the statement
stmt.execute(sql);
} catch (SQLException e)
{
logException(e);
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
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)
{
logException(e);
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)
{
logException(e);
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
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)
{
logException(e);
}
return false;
}
}

View File

@@ -0,0 +1,40 @@
package wtf.beatrice.hidekobot.datasources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.HidekoBot;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class PropertiesSource
{
private Properties properties = null;
private final String fileName = "default.properties";
private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesSource.class);
public void load()
{
properties = new Properties();
try (InputStream internalPropertiesStream = getClass()
.getClassLoader()
.getResourceAsStream(fileName))
{
properties.load(internalPropertiesStream);
} catch (IOException e)
{
LOGGER.error(e.getMessage());
HidekoBot.shutdown();
return;
}
}
public String getProperty(String property)
{
return properties == null ? "" : properties.getProperty(property);
}
}

View File

@@ -0,0 +1,47 @@
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 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;
public class ButtonInteractionListener extends ListenerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(ButtonInteractionListener.class);
@Override
public void onButtonInteraction(ButtonInteractionEvent event)
{
switch (event.getComponentId().toLowerCase())
{
// coinflip
case "coinflip_reflip" -> CoinFlip.buttonReFlip(event);
// generic dismiss button
case "generic_dismiss" -> CommandUtil.delete(event);
// urban dictionary navigation
case "urban_nextpage" -> UrbanDictionary.changePage(event, UrbanDictionary.ChangeType.NEXT);
case "urban_previouspage" -> UrbanDictionary.changePage(event, UrbanDictionary.ChangeType.PREVIOUS);
// trivia
case "trivia_correct" -> Trivia.handleAnswer(event, Trivia.AnswerType.CORRECT);
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());
}
}
}

View File

@@ -0,0 +1,149 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.ChannelType;
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 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 java.util.*;
public class MessageCommandListener extends ListenerAdapter
{
// map storing command labels and command object alphabetically.
private final TreeMap<LinkedList<String>, MessageCommand> registeredCommands =
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 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
public void registerCommand(MessageCommand command)
{
registeredCommands.put(command.getCommandLabels(), command);
}
public MessageCommand getRegisteredCommand(String label)
{
for (Map.Entry<LinkedList<String>, MessageCommand> entry : registeredCommands.entrySet())
{
LinkedList<String> aliases = entry.getKey();
for (String currentAlias : aliases)
{
if (label.equals(currentAlias))
{
return entry.getValue();
}
}
}
return null;
}
public LinkedList<MessageCommand> getRegisteredCommands()
{
return new LinkedList<>(registeredCommands.values());
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
// check if a bot is sending this message, and ignore it
if (event.getAuthor().isBot()) return;
// warning: we are getting the RAW value of the message content, not the DISPLAY value!
String eventMessage = event.getMessage().getContentRaw();
// check if the sent message matches the bot activation regex (prefix, name, ...)
if (!eventMessage.toLowerCase().matches("(?s)" + COMMAND_REGEX + ".*"))
return;
// generate args from the string
String argsString = eventMessage.replaceAll(COMMAND_REGEX + "\\s*", "");
// if no args were specified apart from the bot prefix
// note: we can't check argsRaw's size because String.split returns an array of size 1 if no match is found,
// and that element is the whole string passed as a single argument, which would be empty in this case
// (or contain text in other cases like "string split ," if the passed text doesn't contain any comma ->
// it will be the whole text as a single element.
if (argsString.isEmpty())
{
event.getMessage()
.reply("Hello there! ✨ Type `" + Cache.getBotPrefix() + " help` to get started!")
.queue();
return;
}
// split all passed arguments
String[] argsRaw = argsString.split("\\s+");
// extract the command that the user is trying to run
String commandLabel = argsRaw[0];
MessageCommand commandObject = getRegisteredCommand(commandLabel);
if (commandObject == null)
{
/* temporarily disabled because when people talk about the bot, it replies with this spammy message.
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
*/
return;
}
ChannelType channelType = event.getChannelType();
// permissions check
List<Permission> requiredPermissions = commandObject.getPermissions();
if (requiredPermissions != null && !requiredPermissions.isEmpty())
{
if (channelType.isGuild()) //todo: what about forum post
{
Member member = event.getMember();
GuildChannel channel = event.getGuildChannel(); //todo: what about forum post
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;
}
}
}
String[] commandArgs;
if (commandObject.passRawArgs())
{
// remove first argument, which is the command label
argsString = argsString.replaceAll("^[\\S]+\\s*", "");
// pass all other arguments as a single argument as the first array element
commandArgs = new String[]{argsString};
} else
{
// copy all split arguments to the array, except from the command label
commandArgs = Arrays.copyOfRange(argsRaw, 1, argsRaw.length);
}
// finally run the command, in a new thread to avoid locking.
new Thread(() -> commandObject.runCommand(event, commandLabel, commandArgs)).start();
}
}

View File

@@ -1,43 +0,0 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Configuration;
import wtf.beatrice.hidekobot.utils.Logger;
import wtf.beatrice.hidekobot.utils.RandomUtil;
public class MessageListener extends ListenerAdapter
{
private final Logger logger = new Logger(MessageListener.class);
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
if(event.getMessage().getContentDisplay().equalsIgnoreCase("ping"))
{
MessageChannel channel = event.getChannel();
channel.sendMessage("Pong!").queue();
}
if(event.getMessage().getContentDisplay().equalsIgnoreCase("flip a coin"))
{
MessageChannel channel = event.getChannel();
int rand = RandomUtil.getRandomNumber(0, 1);
String msg;
if(rand == 1)
{
msg = ":coin: It's **Heads**!";
} else {
msg = "It's **Tails**! :coin:";
}
channel.sendMessage(msg).queue();
}
}
}

View File

@@ -1,47 +1,55 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.entities.PrivateChannel;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
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.utils.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MessageLogger extends ListenerAdapter
{
// this class only gets loaded as a listener if verbosity is set to true on startup.
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 static String guildChannelFormat = "[%guild%] [#%channel%] %user%: %message%";
private final static String dmFormat = "[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().getName();
String userName = event.getAuthor().getAsTag();
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.info(atch.getUrl());
}
}
}
}

View File

@@ -0,0 +1,27 @@
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 wtf.beatrice.hidekobot.commands.base.Trivia;
public class SelectMenuInteractionListener extends ListenerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(SelectMenuInteractionListener.class);
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event)
{
switch (event.getComponentId().toLowerCase())
{
// trivia
case "trivia_categories" -> Trivia.handleMenuSelection(event);
// error handling
default -> LOGGER.warn("Received unhandled {}", event.getClass().getSimpleName());
}
}
}

View File

@@ -0,0 +1,43 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import wtf.beatrice.hidekobot.objects.commands.SlashArgumentsCompleter;
import java.util.LinkedList;
import java.util.TreeMap;
public class SlashCommandCompletionListener extends ListenerAdapter
{
// map that stores command label and command auto-completer alphabetically.
private final TreeMap<String, SlashArgumentsCompleter> registeredCompleters = new TreeMap<>();
public void registerCommandCompleter(SlashArgumentsCompleter completer)
{
String parentCommandName = completer.getCommand().getCommandName();
registeredCompleters.remove(parentCommandName);
registeredCompleters.put(parentCommandName, completer);
}
public SlashArgumentsCompleter getRegisteredCompleter(String label)
{
return registeredCompleters.get(label);
}
public LinkedList<SlashArgumentsCompleter> getRegisteredCompleters()
{
return new LinkedList<>(registeredCompleters.values());
}
@Override
public void onCommandAutoCompleteInteraction(CommandAutoCompleteInteractionEvent event)
{
String commandName = event.getName().toLowerCase();
SlashArgumentsCompleter completer = registeredCompleters.get(commandName);
if (completer == null) return;
// not running in a thread because nothing heavy should be done here...
completer.runCompletion(event);
}
}

View File

@@ -0,0 +1,44 @@
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 wtf.beatrice.hidekobot.objects.commands.SlashCommand;
import java.util.LinkedList;
import java.util.TreeMap;
public class SlashCommandListener extends ListenerAdapter
{
// map that stores command label and command object alphabetically.
private final TreeMap<String, SlashCommand> registeredCommands = new TreeMap<>();
public void registerCommand(SlashCommand command)
{
registeredCommands.remove(command.getCommandName());
registeredCommands.put(command.getCommandName(), command);
}
public SlashCommand getRegisteredCommand(String label)
{
return registeredCommands.get(label);
}
public LinkedList<SlashCommand> getRegisteredCommands()
{
return new LinkedList<>(registeredCommands.values());
}
@Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event)
{
String commandName = event.getName().toLowerCase();
SlashCommand command = registeredCommands.get(commandName);
if (command == null) return;
// finally run the command, in a new thread to avoid locking the main one.
new Thread(() -> command.runSlashCommand(event)).start();
}
}

View File

@@ -0,0 +1,44 @@
package wtf.beatrice.hidekobot.objects;
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)
{
@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) +
'}';
}
}

View File

@@ -0,0 +1,22 @@
package wtf.beatrice.hidekobot.objects.commands;
public enum CommandCategory
{
MODERATION("\uD83D\uDC40"),
FUN("\uD83C\uDFB2"),
TOOLS("\uD83D\uDEE0"),
;
private String emoji;
CommandCategory(String emoji)
{
this.emoji = emoji;
}
public String getEmoji()
{
return emoji;
}
}

View File

@@ -0,0 +1,79 @@
package wtf.beatrice.hidekobot.objects.commands;
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 java.util.LinkedList;
import java.util.List;
public interface MessageCommand
{
/**
* Get the command's label(s), which are used when determining if this is the correct command or not.
* The first label in the collection is considered the main command name. All other labels are considered
* command aliases.
*
* @return the command label.
*/
LinkedList<String> getCommandLabels();
/**
* A list of permissions required to run the command. This is preferred to checking them on your own
* as the message listener handles it more homogeneously.
*
* @return the list of required permissions.
*/
@Nullable
List<Permission> getPermissions();
/**
* Say if this command does its own text parsing, and tell the message listener if it should automatically
* split all arguments in separate entries of an array, or pass everything as the first entry of that array.
* <p>
* This is better instead of getting the message contents from the event, because the message listener will
* still strip the bot prefix and command name from the args, but leave the rest untouched.
*
* @return the boolean being true if no parsing should be made by the command handler.
*/
boolean passRawArgs();
/**
* Say what category this command belongs to.
*
* @return the command category.
*/
@NotNull
CommandCategory getCategory();
/**
* Say what this command does.
*
* @return a String explaining what this command does.
*/
@NotNull
String getDescription();
/**
* Say how people should use this command.
*
* @return a String explaining how to use the command, excluding the bot prefix and command name. Null if no parameter is needed
*/
@Nullable
String getUsage();
/**
* Run the command logic by parsing the event and replying accordingly.
*
* @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, ...).
*/
void runCommand(MessageReceivedEvent event, String label, String[] args);
}

View File

@@ -0,0 +1,22 @@
package wtf.beatrice.hidekobot.objects.commands;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import org.jetbrains.annotations.NotNull;
public interface SlashArgumentsCompleter
{
/**
* Get the parent slash command's object.
*
* @return the command object.
*/
SlashCommand getCommand();
/**
* Run the argument-completion logic by parsing the event and replying accordingly.
*
* @param event the received auto-complete event.
*/
void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event);
}

View File

@@ -0,0 +1,24 @@
package wtf.beatrice.hidekobot.objects.commands;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
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;
}
public void runCompletion(@NotNull CommandAutoCompleteInteractionEvent event)
{
return;
}
}

View File

@@ -0,0 +1,32 @@
package wtf.beatrice.hidekobot.objects.commands;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import org.jetbrains.annotations.NotNull;
public interface SlashCommand
{
/**
* Get the command's registered label, or how Discord sees and runs the registered command.
*
* @return the command label.
*/
String getCommandName();
/**
* Get a JDA command data object that will then be used to tell the Discord API the specifics of this
* command.
*
* @return the command data object.
*/
CommandData getSlashCommandData();
/**
* Run the command logic by parsing the event and replying accordingly.
*
* @param event the received slash command event.
*/
void runSlashCommand(@NotNull SlashCommandInteractionEvent event);
}

View File

@@ -0,0 +1,27 @@
package wtf.beatrice.hidekobot.objects.commands;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import org.jetbrains.annotations.NotNull;
public class SlashCommandImpl implements SlashCommand
{
@Override
public String getCommandName()
{
return getSlashCommandData().getName();
}
@Override
public CommandData getSlashCommandData()
{
return null;
}
@Override
public void runSlashCommand(@NotNull SlashCommandInteractionEvent event)
{
event.reply("Base command implementation").queue();
}
}

View File

@@ -0,0 +1,21 @@
package wtf.beatrice.hidekobot.objects.comparators;
import java.util.Comparator;
import java.util.LinkedList;
/**
* This class gets two linked lists, and compares their first value alphabetically.
*/
public class MessageCommandAliasesComparator implements Comparator<LinkedList<String>>
{
@Override
public int compare(LinkedList<String> linkedList, LinkedList<String> t1)
{
if (linkedList.isEmpty()) return 0;
if (t1.isEmpty()) return 0;
return linkedList.get(0).compareTo(t1.get(0));
}
}

View File

@@ -0,0 +1,18 @@
package wtf.beatrice.hidekobot.objects.comparators;
import wtf.beatrice.hidekobot.objects.fun.TriviaCategory;
import java.util.Comparator;
/**
* This class gets two trivia categories, and compares them by their name.
*/
public class TriviaCategoryComparator implements Comparator<TriviaCategory>
{
@Override
public int compare(TriviaCategory o1, TriviaCategory o2)
{
return CharSequence.compare(o1.categoryName(), o2.categoryName());
}
}

View File

@@ -0,0 +1,18 @@
package wtf.beatrice.hidekobot.objects.comparators;
import wtf.beatrice.hidekobot.objects.fun.TriviaScore;
import java.util.Comparator;
/**
* This class gets two trivia scores, and compares their score.
*/
public class TriviaScoreComparator implements Comparator<TriviaScore>
{
@Override
public int compare(TriviaScore o1, TriviaScore o2)
{
return Integer.compare(o2.getScore(), o1.getScore()); // inverted, because higher number should come first
}
}

View File

@@ -0,0 +1,45 @@
package wtf.beatrice.hidekobot.objects.fun;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.UUID;
public class Dice
{
private final int sides;
private int value = 0;
private final UUID uuid;
public Dice(int sides)
{
this.sides = sides;
this.uuid = UUID.randomUUID();
}
public Dice(Dice old)
{
this.sides = old.sides;
this.value = old.value;
this.uuid = UUID.randomUUID();
}
public int getValue()
{
return value;
}
public int getSides()
{
return sides;
}
public void roll()
{
value = RandomUtil.getRandomNumber(1, sides);
}
public UUID getUUID()
{
return uuid;
}
}

View File

@@ -0,0 +1,6 @@
package wtf.beatrice.hidekobot.objects.fun;
public record TriviaCategory(String categoryName, int categoryId)
{
}

View File

@@ -0,0 +1,9 @@
package wtf.beatrice.hidekobot.objects.fun;
import java.util.List;
public record TriviaQuestion(String question, String correctAnswer,
List<String> wrongAnswers)
{
}

View File

@@ -0,0 +1,37 @@
package wtf.beatrice.hidekobot.objects.fun;
import net.dv8tion.jda.api.entities.User;
public class TriviaScore
{
private final User user;
private int score = 0;
public TriviaScore(User user)
{
this.user = user;
}
public void changeScore(int add)
{
score += add;
}
public int getScore()
{
return score;
}
public User getUser()
{
return user;
}
@Override
public String toString()
{
return "[" + user.getAsTag() + "," + score + "]";
}
}

View File

@@ -0,0 +1,65 @@
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 java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class ExpiredMessageTask implements Runnable
{
private final DateTimeFormatter formatter;
private static final Logger LOGGER = LoggerFactory.getLogger(ExpiredMessageTask.class);
private DatabaseSource databaseSource;
public ExpiredMessageTask()
{
String format = Cache.getExpiryTimestampFormat();
formatter = DateTimeFormatter.ofPattern(format);
databaseSource = Cache.getDatabaseSource();
}
@Override
public void run()
{
databaseSource = Cache.getDatabaseSource();
if (databaseSource == null) return;
List<String> expiringMessages = Cache.getDatabaseSource().getQueuedExpiringMessages();
if (expiringMessages == null || expiringMessages.isEmpty()) return;
LocalDateTime now = LocalDateTime.now();
for (String messageId : expiringMessages)
{
if (Cache.isVerbose()) LOGGER.info("expired check: {}", messageId);
String expiryTimestamp = databaseSource.getQueuedExpiringMessageExpiryDate(messageId);
if (expiryTimestamp == null || expiryTimestamp.isEmpty()) // if missing timestamp
{
// count it as already expired
databaseSource.untrackExpiredMessage(messageId);
// move on to next message
continue;
}
LocalDateTime expiryDate = LocalDateTime.parse(expiryTimestamp, formatter);
if (now.isAfter(expiryDate))
{
if (Cache.isVerbose()) LOGGER.info("expired: {}", messageId);
CommandUtil.disableExpired(messageId);
}
}
}
}

View File

@@ -0,0 +1,47 @@
package wtf.beatrice.hidekobot.runnables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wtf.beatrice.hidekobot.Cache;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class HeartBeatTask implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(HeartBeatTask.class);
@Override
public void run()
{
String urlString = Cache.getFullHeartBeatLink();
if (urlString == null || urlString.isEmpty()) return;
try
{
URL heartbeatUrl = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) heartbeatUrl.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
int responseCode = connection.getResponseCode();
if (200 <= responseCode && responseCode < 300)
{
// only log ok response codes when verbosity is enabled
if (Cache.isVerbose()) LOGGER.info("Heartbeat response code: {}", responseCode);
} else
{
LOGGER.error("Heartbeat returned problematic response code: {}", responseCode);
}
} catch (IOException e)
{
LOGGER.error("Error while trying to push heartbeat", e);
}
}
}

View File

@@ -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!");
}
}
}

View File

@@ -0,0 +1,31 @@
package wtf.beatrice.hidekobot.runnables;
import net.dv8tion.jda.api.entities.Activity;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.util.RandomUtil;
import java.util.Arrays;
import java.util.List;
public class StatusUpdateTask implements Runnable
{
List<String> statuses = Arrays.asList(
"Hatsune Miku: Project DIVA",
"Wii Sports",
"Excel",
"Mii Channel",
"Wii Speak",
"Minetest",
"Mario Kart Wii"
);
@Override
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));
}
}

View File

@@ -0,0 +1,205 @@
package wtf.beatrice.hidekobot.runnables;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
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.json.JSONObject;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.commands.base.Trivia;
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 java.util.*;
import java.util.concurrent.ScheduledFuture;
public class TriviaTask implements Runnable
{
private final User author;
private final MessageChannel channel;
private Message previousMessage = null;
private final JSONObject triviaJson;
private final List<TriviaQuestion> questions;
private final TriviaCategory category;
ScheduledFuture<?> future = null;
private int iteration = 0;
public TriviaTask(User author, MessageChannel channel, TriviaCategory category)
{
this.author = author;
this.channel = channel;
this.category = category;
triviaJson = Trivia.fetchJson(Trivia.getTriviaLink(category.categoryId()));
questions = Trivia.parseQuestions(triviaJson); //todo: null check, rate limiting...
}
public void setScheduledFuture(ScheduledFuture<?> future)
{
this.future = future;
}
@Override
public void run()
{
if (previousMessage != null)
{
// todo: we shouldn't use this method, since it messes with the database... look at coin reflip
CommandUtil.disableExpired(previousMessage.getId());
String previousCorrectAnswer = questions.get(iteration - 1).correctAnswer();
// we need this to be thread-locking to avoid getting out of sync with the rest of the trivia features
previousMessage.reply("The correct answer was: **" + previousCorrectAnswer + "**!").complete();
// todo: maybe also add who replied correctly as a list
// clean the list of people who answered, so they can answer again for the new question
Trivia.channelAndWhoResponded.put(previousMessage.getChannel().getId(), new ArrayList<>());
}
if (iteration >= questions.size())
{
String scoreboardText = "\uD83D\uDC23 Trivia session is over!";
List<String> winners = new ArrayList<>();
int topScore = 0;
StringBuilder othersBuilder = new StringBuilder();
LinkedList<TriviaScore> triviaScores = Trivia.channelAndScores.get(channel.getId());
if (triviaScores == null) triviaScores = new LinkedList<>();
else triviaScores.sort(new TriviaScoreComparator());
int pos = 0;
Integer previousScore = null;
for (TriviaScore triviaScore : triviaScores)
{
if (pos > 10) break; // cap at top 10
String user = triviaScore.getUser().getAsMention();
int score = triviaScore.getScore();
if (previousScore == null)
{
previousScore = score;
topScore = score;
pos = 1;
} else
{
if (score != previousScore) pos++;
}
if (pos == 1) winners.add(user);
else
{
othersBuilder.append("\n").append(pos)
.append(" | ").append(user)
.append(": ").append(score).append(" points");
}
}
StringBuilder winnersBuilder = new StringBuilder();
for (int i = 0; i < winners.size(); i++)
{
String winner = winners.get(i);
winnersBuilder.append(winner);
if (i + 1 != winners.size())
{
winnersBuilder.append(", "); // separate with comma except on last run
} else
{
winnersBuilder.append(": ").append(topScore).append(" points \uD83C\uDF89");
}
}
String winnersTitle = "\uD83D\uDCAB ";
winnersTitle += winners.size() == 1 ? "Winner" : "Winners";
String winnersString = winnersBuilder.toString();
String othersString = othersBuilder.toString();
EmbedBuilder scoreboardBuilder = new EmbedBuilder();
scoreboardBuilder.setColor(Cache.getBotColor());
scoreboardBuilder.setTitle("\uD83C\uDF1F Trivia Scoreboard");
if (!winnersString.isEmpty()) scoreboardBuilder.addField(winnersTitle, winnersString, false);
else scoreboardBuilder.addField("\uD83D\uDE22 Sad Trivia",
"No one played \uD83D\uDE2D", false);
if (!othersString.isEmpty()) scoreboardBuilder.addField("☁️ Others", othersString, false);
channel.sendMessage(scoreboardText).addEmbeds(scoreboardBuilder.build()).queue();
// remove all cached data
Trivia.channelsRunningTrivia.remove(channel.getId());
Trivia.channelAndWhoResponded.remove(channel.getId());
Trivia.channelAndScores.remove(channel.getId());
future.cancel(false);
// we didn't implement null checks on the future on purpose, because we need to know if we were unable
// to cancel it (and console errors should make it clear enough).
return;
}
TriviaQuestion currentTriviaQuestion = questions.get(iteration);
List<Button> answerButtons = new ArrayList<>();
Button correctAnswerButton = Button.primary("trivia_correct", currentTriviaQuestion.correctAnswer());
answerButtons.add(correctAnswerButton);
int i = 0; // we need to add a number because buttons can't have the same id
for (String wrongAnswer : currentTriviaQuestion.wrongAnswers())
{
i++;
Button wrongAnswerButton = Button.primary("trivia_wrong_" + i, wrongAnswer);
answerButtons.add(wrongAnswerButton);
}
Collections.shuffle(answerButtons);
List<String> buttonEmojis = Arrays.asList("\uD83D\uDD34", "\uD83D\uDD35",
"\uD83D\uDFE2", "\uD83D\uDFE1", "\uD83D\uDFE4", "\uD83D\uDFE3", "\uD83D\uDFE0");
// add emojis to buttons
for (int emojiPos = 0; emojiPos < buttonEmojis.size(); emojiPos++)
{
if (emojiPos == answerButtons.size()) break;
String emoji = buttonEmojis.get(emojiPos);
Button button = answerButtons.get(emojiPos);
answerButtons.set(emojiPos, button.withEmoji(Emoji.fromUnicode(emoji)));
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setColor(Cache.getBotColor());
embedBuilder.setTitle("\uD83C\uDFB2 Trivia - " + category.categoryName() +
" (" + (iteration + 1) + "/" + questions.size() + ")");
embedBuilder.addField("❓ Question", currentTriviaQuestion.question(), false);
previousMessage = channel
.sendMessageEmbeds(embedBuilder.build())
.setActionRow(answerButtons)
.complete();
Cache.getDatabaseSource().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);
iteration++;
}
}

View File

@@ -0,0 +1,245 @@
package wtf.beatrice.hidekobot.util;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
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 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
{
private static final Logger LOGGER = LoggerFactory.getLogger(CommandUtil.class);
private CommandUtil()
{
throw new IllegalStateException("Utility class");
}
/**
* Function to delete a message when a user clicks the "delete" button attached to that message.
* This will check in the database if that user ran the command originally.
*
* @param event the button interaction event.
*/
public static void delete(ButtonInteractionEvent event)
{
// check if the user interacting is the same one who ran the command
if (!(Cache.getDatabaseSource().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.
}
/**
* Method to update slash commands registered on Discord's side.
* It runs automatically every time the bot starts, but only updates the commands in case differences
* are found, unless forced.
*
* @param force a boolean specifying if the update should be forced even if no differences were found.
*/
public static 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());
}
JDA jdaInstance = HidekoBot.getAPI();
// get all the already registered commands
List<Command> registeredCommands = jdaInstance.retrieveCommands().complete();
boolean update = false;
if (force)
{
update = true;
} else
{
// for each command that we have already registered...
for (Command currRegCmd : registeredCommands)
{
boolean found = false;
// iterate through all "recognized" commands
for (CommandData cmdData : allCommands)
{
// if we find the same command...
if (cmdData.getName().equals(currRegCmd.getName()))
{
// quit the loop since we found it.
found = true;
break;
}
}
// if no match was found, we need to send an updated command list because
// an old command was probably removed.
if (!found)
{
update = true;
// quit the loop since we only need to trigger this once.
break;
}
}
// if an update is not already queued...
if (!update)
{
// for each "recognized" valid command
for (CommandData currCmdData : allCommands)
{
boolean found = false;
// iterate through all already registered commands.
for (Command cmd : registeredCommands)
{
// if this command was already registered...
if (cmd.getName().equals(currCmdData.getName()))
{
// quit the loop since we found a match.
found = true;
break;
}
}
// if no match was found, we need to send an updated command list because
// a new command was probably added.
if (!found)
{
update = true;
// quit the loop since we only need to trigger this once.
break;
}
}
}
}
LOGGER.info("Found {} commands.", registeredCommands.size());
if (update)
{
// send updated command list.
jdaInstance.updateCommands().addCommands(allCommands).queue();
LOGGER.info("Commands updated. New total: {}.", allCommands.size());
}
}
/**
* Method to disable all buttons from an expired message.
*
* @param messageId the message id to disable.
*/
public static void disableExpired(String messageId)
{
DatabaseSource databaseSource = Cache.getDatabaseSource();
String channelId = databaseSource.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);
MessageChannel textChannel = null;
// this should never happen, but only message channels are supported.
if (!msgChannelType.isMessage())
{
databaseSource.untrackExpiredMessage(messageId);
return;
}
// if this is a DM
if (!(msgChannelType.isGuild()))
{
String userId = databaseSource.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);
return;
}
textChannel = user.openPrivateChannel().complete();
} else
{
String guildId = databaseSource.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);
return;
}
textChannel = guild.getTextChannelById(channelId);
}
if (textChannel == null)
{
// if channel is not found, count it as expired
// (channel was deleted or bot permissions restricted)
databaseSource.untrackExpiredMessage(messageId);
return;
}
RestAction<Message> retrieveAction = textChannel.retrieveMessageById(messageId);
if (Cache.isVerbose()) LOGGER.info("cleaning up: {}", messageId);
retrieveAction.queue(
message -> {
if (message == null)
{
databaseSource.untrackExpiredMessage(messageId);
return;
}
List<LayoutComponent> components = message.getComponents();
List<LayoutComponent> newComponents = new ArrayList<>();
for (LayoutComponent component : components)
{
component = component.asDisabled();
newComponents.add(component);
}
message.editMessageComponents(newComponents).queue();
databaseSource.untrackExpiredMessage(messageId);
},
error -> databaseSource.untrackExpiredMessage(messageId));
}
}

View File

@@ -0,0 +1,170 @@
package wtf.beatrice.hidekobot.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
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)
*
* @return the formatted String
*/
public static String getNiceTimeDiff(LocalDateTime start)
{
LocalDateTime now = LocalDateTime.now();
long uptimeSeconds = ChronoUnit.SECONDS.between(start, now);
Duration uptime = Duration.ofSeconds(uptimeSeconds);
return getNiceDuration(uptime);
}
/**
* Generate a nicely formatted duration String that omits unnecessary data
* (e.g. 0 days, 0 hours, 4 minutes, 32 seconds -> 4m 32s)
*
* @return the formatted String
*/
public static String getNiceDuration(Duration duration)
{
long days = duration.toDays();
long hours = duration.toHoursPart();
long minutes = duration.toMinutesPart();
long seconds = duration.toSecondsPart();
StringBuilder sb = new StringBuilder();
if (days > 0)
{
sb.append(days).append("d ");
sb.append(hours).append("h ");
sb.append(minutes).append("m ");
} else if (hours > 0)
{
sb.append(hours).append("h ");
sb.append(minutes).append("m ");
} else if (minutes > 0)
{
sb.append(minutes).append("m ");
}
sb.append(seconds).append("s");
return sb.toString();
}
/**
* Method to parse a string into a duration.
* Warning: this only supports up to days; months and longer timeframes are unsupported.
*
* @param duration the String to parse.
* @return a Duration of the parsed timeframe, or null if parsing failed.
*/
@Nullable
public static Duration parseDuration(String duration)
{
// sanitize a bit to avoid cluttering with garbled strings
if (duration.length() > 16) duration = duration.substring(0, 16);
duration = duration.replaceAll("[^\\w]", ""); //only keep digits and word characters
duration = duration.toLowerCase();
/* the following regex matches any number followed by any amount of characters, any amount of times.
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{1,10}[a-zA-Z]{1,10}){1,10}"))
return null;
String[] durationTimes = duration.split("[a-zA-Z]+");
String[] durationUnits = duration.split("\\d+");
// remove first element, because it will always be empty (there's nothing before the first character)
durationUnits = Arrays.copyOfRange(durationUnits, 1, durationUnits.length);
Duration fullDuration = Duration.ZERO;
for (int i = 0; i < durationTimes.length; i++)
{
String durationTimeStr = durationTimes[i];
String durationUnitStr = durationUnits[i];
int durationValue = Integer.parseInt(durationTimeStr);
TemporalUnit unit = parseTimeUnit(durationUnitStr);
if (unit != null)
fullDuration = fullDuration.plus(durationValue, unit);
else return null; // if we failed finding the time unit, instantly quit with failed parsing.
}
return fullDuration;
}
@Nullable
private static TemporalUnit parseTimeUnit(@NotNull String unitName)
{
// 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;
/*
parsing table
s, se, sec, second, seconds -> SECOND
m, mi, min, minute, minutes -> MINUTE
h, ho, hr, hour, hours -> HOUR
d, day, days -> DAY
(months and longer timeframes are unsupported due to Discord restrictions)
*/
switch (unitName)
{
case "s", "se", "sec", "second", "seconds" -> timeUnit = ChronoUnit.SECONDS;
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;
}
}

View File

@@ -0,0 +1,71 @@
package wtf.beatrice.hidekobot.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Deprecated(since = "0.5.16", forRemoval = true)
public class Logger<T>
{
// objects that we need to have for a properly formatted message
private final 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<T> 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.
try (ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor())
{
executor.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);
}
}

View File

@@ -0,0 +1,84 @@
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.
*
* @param min the minimum value (inclusive)
* @param max the maximum value (inclusive)
* @return a random number in range [min; max]
*/
public static int getRandomNumber(int min, int max)
{
if (min == max) return min; // dumbass
if (min > max) // swap em
{
min = min - max;
max = min + max;
min = max - min;
}
// find our range of randomness (eg. 5 -> 8 = 4), add +1 since we want to be inclusive at both sides
int difference = (max - min) + 1;
// find a number between 0 and our range (eg. 5 -> 8 = 0 -> 3)
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());
}
}

View File

@@ -0,0 +1,48 @@
package wtf.beatrice.hidekobot.util;
import org.apache.commons.lang3.SerializationException;
import java.io.*;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
public class SerializationUtil
{
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))
{
so.writeObject(dataList);
so.flush();
return Base64.getEncoder().encodeToString(bo.toByteArray());
} catch (IOException e)
{
throw new SerializationException("Error during serialization", e);
}
}
public static <T> LinkedList<T> deserializeBase64(String dataStr)
{
byte[] b = Base64.getDecoder().decode(dataStr);
ByteArrayInputStream bi = new ByteArrayInputStream(b);
ObjectInputStream si;
try
{
si = new ObjectInputStream(bi);
return LinkedList.class.cast(si.readObject());
} catch (IOException | ClassNotFoundException e)
{
throw new SerializationException("Error during deserialization", e);
}
}
}

View File

@@ -1,74 +0,0 @@
package wtf.beatrice.hidekobot.utils;
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();
}
// log a message to console, with our chosen format
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));
}
// log a message to console after delaying it (in seconds).
public void log(String message, int delay)
{
// create a new scheduled executor with an anonymous runnable...
Executors.newSingleThreadScheduledExecutor().schedule(new Runnable()
{
@Override
public void run()
{
// log the message
log(message);
}
//... after waiting X seconds.
}, delay, TimeUnit.SECONDS);
}
// avoid formatting the text and print whatever is passed.
public void logRaw(String message)
{
System.out.println(message);
}
public String getLogo()
{
return logo;
}
}

View File

@@ -1,29 +0,0 @@
package wtf.beatrice.hidekobot.utils;
import java.util.Random;
public class RandomUtil
{
private static final Random random = new Random();
public static int getRandomNumber(int min, int max)
{
if(min == max) return min; // dumbass
if(min > max) // swap em
{
min = min - max;
max = min + max;
min = max - min;
}
// find our range of randomness (eg. 5 -> 8 = 4), add +1 since we want to be inclusive at both sides
int difference = (max - min) + 1;
// find a number between 0 and our range (eg. 5 -> 8 = 0 -> 3)
int randomTemp = random.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;
}
}

View File

@@ -0,0 +1,2 @@
bot.version=${project.version}
repo.base_url=https://git.beatrice.wtf/bea/HidekoBot/

12
suppressions.xml Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<!--
<suppress>
<notes><![CDATA[
file name: snakeyaml-1.33.jar
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.yaml/snakeyaml@.*$</packageUrl>
<cve>CVE-2021-4235</cve>
</suppress>
-->
</suppressions>