Compare commits

...
245 Commits
Author SHA1 Message Date
Christian F. Brinkmann (cloud) f95b50af89 Bump version to 0.6.5 and update YouTube common dependency to 1.18.1 2026-05-10 23:22:18 +02:00
Christian F. Brinkmann (cloud) 1e6904d329 Fix Chewtils version in about/debug output
Work around the broken VERSION_REVISION placeholder in jda-chewtils 2.2.1.

- render the about embed from LoggedAboutCmd
- load the Chewtils version from a filtered project resource
- use the same resolved version in DebugCmd
2026-04-08 16:22:18 +02:00
Christian F. Brinkmann (cloud) 5ff7b2f28e Update README and add setup, docker, and changelog documentation 2026-03-30 14:31:45 +02:00
Christian F. Brinkmann (cloud) 28dfdcf680 Add playback analytics logging and bump version to 0.6.3
Add JDBC-backed activity logging for queue, playback, search, skip, listener, and command events.

Logging is disabled by default and can be enabled with `dblog.enabled=true`. The default JDBC URL uses SQLite via `jdbc:sqlite:musicbot.db`, but other JDBC backends can be configured with the same settings block.
2026-03-30 14:11:25 +02:00
Christian F. Brinkmann (cloud) c5073b8755 Bump version to 0.6.2
Fix status message repost races and add a config toggle to disable the feature.

Add the statusmessages config option in reference.conf. When enabled, the handler now uses stronger synchronization and safer message-id reset behavior during reposts and edit failures.
2026-03-30 14:09:52 +02:00
Christian F. Brinkmann (cloud) 5501f02a97 Bump version to 0.6.1 2026-03-12 16:23:41 +01:00
Christian F. Brinkmann (cloud) 66e207eabe Add StatusMessageHandler to manage playback status messages and update Bot and Listener classes 2026-03-12 16:23:12 +01:00
Christian F. Brinkmann (cloud) db3a57d517 Update .gitignore to exclude all hidden files and ensure proper formatting 2026-03-12 15:28:38 +01:00
Christian F. Brinkmann 81fa3fed25 Bump version to 0.6.0 2026-03-06 10:15:59 +01:00
Christian F. Brinkmann 9484c373ff Downgrade jsoup dependency version from 1.15.3 to 1.14.2 2026-03-06 09:32:02 +01:00
Christian F. Brinkmann 860822d520 Update JLyrics dependency version from master-SNAPSHOT to master 2026-03-05 15:40:27 +01:00
Christian F. Brinkmann a0c313047f Add DAVE protocol support and update dependencies for JDA 6.3+ 2026-03-05 15:26:25 +01:00
Christian F. Brinkmann db69ea6787 Update Docker Hub link in README 2025-11-15 23:56:45 +01:00
Christian F. Brinkmann 5dba2c355b Bump version from 0.5.1 to 0.5.2 in pom.xml 2025-11-15 12:57:11 +01:00
Christian F. Brinkmann 9fd246ac25 Fix JDA-Chewtils dependency groupId and update version to jda-6.1.1-SNAPSHOT 2025-11-15 12:28:38 +01:00
Christian F. Brinkmann 6a15bbb0df Add MESSAGE_CONTENT and additional intents to JMusicBot 2025-11-15 12:28:23 +01:00
Christian F. Brinkmann 16bc176685 Bump version to 0.5.1 and enable additional intents for JDA 2025-11-15 10:56:43 +01:00
Christian F. Brinkmann 50e8bff831 Bump version from 0.4.12 to 0.5.0 in pom.xml 2025-11-14 21:49:41 +01:00
Christian F. Brinkmann 9c012eb169 Update README to reflect JDA 6 upgrade and MESSAGE_CONTENT intent requirements 2025-11-14 21:49:33 +01:00
Christian F. Brinkmann 87771df1fb Update Lavaplayer and Lavalink dependencies to latest versions 2025-11-14 21:49:04 +01:00
Christian F. Brinkmann 5d35f5bb4c Enable MESSAGE_CONTENT intent for JDA
Appears to be a requirement for JDA 5 or later
2025-11-14 21:13:10 +01:00
Christian F. Brinkmann ecd8cdf77b Update logback-classic dependency to version 1.5.21 2025-11-14 20:29:33 +01:00
Christian F. Brinkmann 39e5bf7786 Upgrade JDA from 4.4.1_353 to 6.1.1
- Updated JDA dependency to 6.1.1
- Switched to Chew's JDA-Chewtils fork (fix/components-v2 branch) for JDA v6 support
- Fixed breaking API changes:
  - Channel types moved to channel.concrete package
  - MessageBuilder → MessageCreateBuilder/MessageCreateData
  - Events moved to events.session package
  - GuildMessageDeleteEvent → MessageDeleteEvent
  - Permissions: MESSAGE_READ → VIEW_CHANNEL, MESSAGE_WRITE → MESSAGE_SEND
  - Methods: inVoiceChannel() → inAudioChannel()
  - File uploads: sendFile() → sendFiles() with FileUpload
  - Removed CacheFlag.EMOTE
  - Removed LoginException handling (no longer thrown by JDABuilder)

Note: LoginException removal means less helpful error messages for invalid tokens.
2025-11-14 14:12:21 +01:00
Christian F. Brinkmann 650a9ea996 bump version to 0.4.12 and update Lavalink dependency to 1.13.2 2025-06-08 21:01:45 +02:00
Christian F. Brinkmann 0e0c05acf9 Update version to 0.4.11-1 and add Lavalink snapshot repository 2025-05-01 09:24:57 +02:00
Christian F. Brinkmann 2e52ecac3b bump version to 0.4.11 and update lavalink to 1.13.0 2025-05-01 01:07:01 +02:00
Christian F. Brinkmann (cloud) 450ee7acb8 bump lavalink to 1.12.0, bump version to 4.10 2025-03-28 01:16:49 +01:00
Christian F. Brinkmann a7b56b67ab Update README.md to mention nssm for windows 2025-02-26 23:39:18 +01:00
Christian F. Brinkmann fedb97e1c7 Update README.md 2025-02-26 16:17:21 +01:00
Christian F. Brinkmann 68f951dc63 Merge remote-tracking branch 'origin/master' 2025-02-26 15:46:45 +01:00
Christian F. Brinkmann 01eee8813a bump project version to 0.4.9 2025-02-26 15:46:07 +01:00
Christian F. Brinkmann 3df09a4cd2 bump lavaplayer common dependency version to 1.11.5 2025-02-26 15:45:24 +01:00
Christian F. Brinkmann 0084ba7baf Update README.md
Add demo server discord invite
2025-02-19 11:16:26 +01:00
Christian F. Brinkmann 35e756df9b bump project version to 0.4.8 2025-02-13 14:20:38 +01:00
Christian F. Brinkmann 426e28ed8c bump lavaplayer dependencies to latest versions 2025-02-13 14:16:16 +01:00
Christian F. Brinkmann dd72baca0d Reflect recent changes in the readme 2025-01-11 13:45:31 +01:00
Christian F. Brinkmann ac4e09cbd9 bump project patch version 2025-01-11 13:41:38 +01:00
Christian F. Brinkmann fd01d94cf2 bump lavaplayer version 2025-01-11 13:40:48 +01:00
Christian F. Brinkmann 95f72937c8 Merge remote-tracking branch 'other-fork/fix-youtube' 2025-01-11 13:38:53 +01:00
Michaili K 792cb7a8b7 Remove whitespace 2025-01-03 00:02:27 +01:00
Michaili K 03ba83f06b Log the entire exception in debug for brevity 2025-01-03 00:00:58 +01:00
Michaili K 3b3a483b17 Use Selenium with Chrome to obtain PO token and visitordata 2025-01-02 23:25:44 +01:00
Christian F. Brinkmann 1874252ff6 change urls for update check logic 2024-12-14 13:26:33 +01:00
Christian F. Brinkmann aa8e430ff1 bump version to 0.4.6 2024-12-14 13:26:17 +01:00
Christian F. Brinkmann 204bb6f04a bump lavalink version in the hopes that it fixes some errors 2024-12-14 13:05:20 +01:00
Michaili K 9ec21bf329 Update youtube-source to 1.11.1 2024-12-10 14:16:24 +01:00
Christian F. Brinkmann b419191bad use actual version instead of snapshot in pom.xml 2024-12-06 18:17:37 +01:00
Christian F. Brinkmann aa89145ae8 update readme to reflect that i have now made miniscule dependency changes myself 2024-12-06 18:11:20 +01:00
Christian F. Brinkmann 293733f0e7 bump versions of lavaplayer and lavalink to most recent in the hopes that this fixes something 2024-12-06 17:55:44 +01:00
Michaili K 7be02b1fe2 Add quotes around po token & visitor data config 2024-11-21 04:12:18 +01:00
Michaili K 1ac46d9ec5 Implement IP rotation for YouTube 2024-11-21 03:47:12 +01:00
Michaili K 026ce87440 Implement PO Tokens for YouTube playback 2024-11-21 01:41:13 +01:00
Michaili K e5f34cc900 Update deps 2024-11-21 01:25:57 +01:00
Christian F. Brinkmann c9a2477f86 Update README.md 2024-10-25 13:26:06 +02:00
root 61a18a1db7 update README to give some info about the fork 2024-10-24 17:24:38 +02:00
root 7fb44e296b Merge branch 'pr-1670', use 1.8.3 instead of 1.8.2 2024-10-24 14:01:24 +02:00
Lythus 4ae9f05776 ignore dockerfile 2024-10-19 21:28:44 +08:00
Lythus 84facb8b82 updated lavalink version to 1.8.3 2024-10-19 20:36:53 +08:00
Michaili K 070938b326 Add anothen message to onTrackError handler 2024-10-15 10:54:27 +02:00
Michaili K 58a9e76edb Formatting changes 2024-10-11 23:42:06 +02:00
Michaili K 96ffcd8291 Handle specific track exceptions with a special message 2024-10-10 20:00:40 +02:00
Michaili K 844a1bb75a Change youtubeoauth2 option description 2024-10-10 19:11:29 +02:00
Michaili K 6094e49ab2 Return early when failing to read the token file 2024-10-10 19:09:05 +02:00
Michaili K 88ce3e1149 Stop logging the refresh token on startup 2024-10-10 19:08:49 +02:00
Michaili K 463bf75783 Prepend "Google" to "accounts" in OAuth2 DM for clarification 2024-10-06 04:56:00 +02:00
Michaili K 348fc3ceec DM the user when the YoutubeOauth2Handler is providing us a token 2024-10-02 21:39:29 +02:00
Lythus 30eafb9239 Updated JDA-Utilities 2024-09-30 15:22:32 +08:00
Michaili K c95f2248f3 Bump youtube-source to 1.8.2 2024-09-26 02:12:38 +02:00
Michail b80b0c8459 Remove accidental HTML tag :) 2024-08-24 14:01:15 +02:00
Michail 4a8392fdb9 Catch potential exceptions thrown by the useOAuth2 call 2024-08-24 13:38:52 +02:00
Michail 9d6584525c Implement YouTube OAuth2 login 2024-08-24 13:25:11 +02:00
Michail 909b655f5c Update YouTube source manager 2024-08-24 12:09:43 +02:00
Michaili K 859e5c5862 Update lavaplayer youtube source to 1.5.2 (#1655) 2024-08-12 13:28:38 -04:00
njezi 43b63f1ba8 fixed a mistake (#1648) 2024-08-06 15:59:19 -04:00
John Grosh eca2563abf fix npe and an import (#1567)
* fix npe and an import

* stop double posting

* switched stacktrace to log, changed some logs
2024-08-01 09:42:17 -04:00
kokofixcomputersandMichaili K e6bfd1841f fix error when playing (#1632)
* fix error when playing

* Update youtube source to 1.5.1

---------

Co-authored-by: Michaili K <git@michaili.dev>
2024-08-01 09:14:51 -04:00
Timothy Gillespie f140b41bf5 Use shell comparsion (#1627) 2024-08-01 09:10:04 -04:00
John Grosh 8733899aa0 Update bug-report.yml 2024-07-10 17:35:50 -04:00
Moritz Bender e5fd05e601 Update dependencies to fix youtube issues (#1609) 2024-07-10 13:19:13 +02:00
John Grosh 6a5a9c7a75 Change playlist page count (#1542)
* change playlist page count

* make the value configurable

* small refactor
2024-05-10 17:43:49 -04:00
Michaili K 8557f7ab8e Log track exceptions in the audio handler (#1558) 2024-05-10 15:36:43 -04:00
Michaili K 48e62f1a50 Revert "Always self-deafen (#1491)" (#1551)
This reverts commit a7807b96e2.
2024-05-10 15:32:30 -04:00
John Grosh 6f12c33781 custom eval engine (#1530) 2024-05-10 15:23:30 -04:00
Michaili K 0afb3db269 Switch to lavalink's new YouTube audio source manager (#1552)
* Switch to new lavalink's new YouTube audio source manager

* Add back setPlaylistPageCount call for the youtube audio source manager

* Manually add all the individual audio sources EXCEPT for old youtube

* Remove unused import

* Upgrade lavaplayer-youtube-source to 1.0.3

* Upgrade lavaplayer-youtube-source to 1.0.4
2024-05-10 14:19:11 -04:00
Michaili KandWhew., Inc 81322ef993 Add seek command (supersedes #344) (#674)
* Add seek command

* Combine nested if statements

* Put the seek command in correct place to keep alphabetical order

* Add license header

* Brackets on next line

* Restructure if-statements

* Check for permissions with DJCommand#checkDJPermission

* Make regex slightly smaller

* Optimize imports

* Output length of current track if requested seek time is invalid

* Restate seeked point when seeked successfully

* Add empty newline at end of file to keep consistency

* Create TimeUtil class for parsing and formatting time

* Move FormatUtil#formatTime to TimeUtil, and refactor

* Apply requested changes (Pass 2)

* Seek based on current position in track

* Apply requested changes (Pass 3)

* Add javadoc param

* Apply requested changes (Pass 4)

* Fix merge

* Avoid reassigning parameter (Codacy)

* Rework timestamp parsing

* Refactor timestamp parsing

* Apply requested changes (Pass 5)

* Add examples in help

* Apply requested changes (Pass 6)

* Fix missing import

* Keep track of start timestamp & add "unit" times

* Fix my abdominal merge with QueuedTrack

* Use RequestMetadata to store start timestamp

* Store request info in request metadata

* Add regex to try getting a timestamp from the url

* Require RequestMetadata for QueuedTracks

* Add some unit tests for unit seeking

* Add docs & examples

---------

Co-authored-by: Whew., Inc <22574706+Whew-Inc@users.noreply.github.com>
2024-05-10 14:14:28 -04:00
Michaili KandJohn Grosh b3ffbdba7d Add log level configuration (#1156)
* Add loglevel config

* Change order of fields

---------

Co-authored-by: John Grosh <john.a.grosh@gmail.com>
2024-03-30 15:34:46 -04:00
John Grosh 801b3769a1 Fix startup message type and add URL to message (#1521)
* fix

* add URL to error message
2024-03-30 15:18:52 -04:00
Michaili Kandunknown 49c3ec7960 Switch to lavalink's lavaplayer fork (#1490)
* Switch to lavalink's lavaplayer fork

* Create logback turbofilter to suppress YouTube auth tokens warning

* added license header

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-04 19:38:04 -05:00
John Grosh 6752695b43 rename github action file and update readme (#1494) 2024-03-04 19:35:32 -05:00
John Grosh bb17c984a2 Codefactor Fixes (#1493)
* codefactor fixes

* anotha one
2024-03-04 19:35:17 -05:00
Michaili K a7807b96e2 Always self-deafen (#1491) 2024-03-04 19:08:27 -05:00
2e9dd5df49 add LinearQueue (#1194)
* add LinearQueue

* rework queueType to be server-specific

* rework QueueType enum and command

* add QueueType supplier

* fix queue type formatting

* add QueueSupplier util

* fix code issues

* Fix unit tests

* add suggested changes on PR

---------

Co-authored-by: Michaili K <mysteriouscursor+git@protonmail.com>
Co-authored-by: Michail <git@michaili.dev>
2024-03-04 18:49:01 -05:00
Maoyue 貓月andunknown 1ad16181d0 Support new Discord username (#1397)
* Support new username

* Support new username, again

* Make a separate username formatting function

* Add some missing usage

* switch away from using static imports, add shortcuts for jda User and metadata UserInfo, add null check

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-02 19:39:16 -05:00
John Grosh bb53bad1ff Update test.yml 2024-03-01 20:11:30 -05:00
Kevin S 8a1066cc87 Allow configurable default skip ratio (#1140)
* Allow configurable default skip ratio

Revert "Allow configurable default skip ratio"

This reverts commit 10e38b4132ee2b28fb583603d69e6228da211ecd.

* FIX: Use -1 to denote a guild has not set a skip ratio, and opted to use the bot default.
2024-03-01 19:42:39 -05:00
Michaili Kandunknown 23c2dfa78e Make JMusicBot error & shut down if ran in an unsupported manner (#1486)
* Make JMusicBot error & shut down if ran in an unsupported manner

* add info about message content intent

* five second rule

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-01 19:26:58 -05:00
Michaili Kandunknown 18bf6ac0f6 Remove channel topic info (#1485)
* Remove channel topic info

As Discord has severely increased the ratelimits for updating the channel topic, updating the channel topic has become not viable for bots.

* fix compilation error

---------

Co-authored-by: unknown <john.a.grosh@gmail.com>
2024-03-01 19:19:18 -05:00
John Grosh fd6de60fc7 Merge pull request #1489 from jagrosh/add-more-sources
Add more sources and check more nulls
2024-03-01 19:00:50 -05:00
unknown 4c8ff655d8 update github actions to jdk 11 2024-03-01 18:26:10 -05:00
John Grosh ac8661c904 Merge pull request #1487 from MichailiK/fix/server-settings-logging
Don't warn about failing to load server settings when the file isn't found
2024-03-01 18:24:09 -05:00
unknown 4f926baf76 update circleci settings 2024-03-01 18:08:46 -05:00
unknown c5ecf89852 switch to java 11 2024-03-01 18:04:26 -05:00
unknown a52103620a update version 2024-03-01 18:00:02 -05:00
unknown fa5e33e2a5 fix formatting and nulls in metadata 2024-03-01 17:53:08 -05:00
unknown 781a94ea91 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2024-03-01 17:50:39 -05:00
Michail ff1ede9d12 Log "loaded config" message again after successfully initializing the GUI 2024-02-28 21:33:45 +01:00
Michail 90d9a1631a Create a new serversettings.json file if one doesn't exist 2024-02-28 21:33:10 +01:00
John Grosh c4502bbe16 Merge pull request #1460 from jagrosh/dependabot/maven/ch.qos.logback-logback-classic-1.2.13
Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13
2024-02-28 15:13:45 -05:00
Michail 2d4f2671c9 Don't warn about failing to load server settings when the file isn't found 2024-02-28 21:01:08 +01:00
John Grosh e595de2033 Merge pull request #1479 from MichailiK/fix/eval-shenanigans
Fix eval command failing in DMs
2024-02-10 17:28:33 -05:00
Michail 53875f2791 Properly fix eval command failing in DMs 2024-02-10 23:25:11 +01:00
Michail cd7e643228 Possible workaround for Eval command failing 2024-02-10 23:10:07 +01:00
dependabot[bot] 0e1f0d3bfa Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.2.3 to 1.2.13.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.2.3...v_1.2.13)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-05 22:17:47 +00:00
John Grosh 2c76164f6d Merge pull request #1304 from LachlanPond/master
Catch JDA exception when there is no internet connection
2023-03-06 16:31:28 -05:00
Lachlan Pond 788cd40f2c Added import for ErrorResponseException 2023-03-04 13:59:42 +10:00
Michaili K 1e12e4e01d Bump JDA to 4.4.1_353 2023-03-01 20:52:53 +01:00
Lachlan Pond c4cbfe2a11 Catch JDA exception when there is no internet connection 2023-02-27 21:52:22 +10:00
John Grosh bbed9e8990 Merge pull request #1294 from lrbn86/master
Sanitize the playlist name
2023-02-14 12:22:51 -05:00
lrbn86 354a0b3def Sanitize the playlist name 2023-02-13 18:31:14 -05:00
John Grosh 2faef67b30 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2022-09-02 21:56:23 -04:00
John Grosh aee076c854 add more sources 2022-09-02 21:56:19 -04:00
John Grosh b76f6fa07c Merge pull request #1141 from t3rminus/fix-warnings-deprecations
Fix warnings and deprecations
2022-09-02 21:55:14 -04:00
John Grosh ec4e9e624c Merge pull request #1209 from jagrosh/dependabot/maven/org.jsoup-jsoup-1.15.3
Bump jsoup from 1.14.2 to 1.15.3
2022-09-02 21:53:47 -04:00
dependabot[bot] a7be2c4602 Bump jsoup from 1.14.2 to 1.15.3
Bumps [jsoup](https://github.com/jhy/jsoup) from 1.14.2 to 1.15.3.
- [Release notes](https://github.com/jhy/jsoup/releases)
- [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES)
- [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.14.2...jsoup-1.15.3)

---
updated-dependencies:
- dependency-name: org.jsoup:jsoup
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 23:43:19 +00:00
John Grosh 6007d3e99b Merge pull request #1192 from yo-louk/queue-links
Queue entry formatting fix
2022-07-07 16:00:25 -04:00
yo-louk a17b2f20ee check uri type before formatting queue entry 2022-07-07 21:26:18 +02:00
John Grosh 4375d4caa8 Merge pull request #1155 from MichailiK/pr/ci/simplify-release
Update generated description in release workflow
2022-04-09 15:01:04 -04:00
Michaili K c22807bcbb Update make-release.yml 2022-04-09 18:25:01 +02:00
John Grosh baf629a910 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2022-04-06 18:28:07 -04:00
John Grosh c4e11d682e update deps, minor code cleanup 2022-04-06 18:27:59 -04:00
Xiao Quan 全笑andMichaili K 041ab27b94 Fixes #514 Require inputing name for playlist make <name> command (#1046)
* Fixes #514 Require inputing name for `playlist make <name>` command

playlist make command without a name will now not make an empty playlist without a playlist name.

* Update src/main/java/com/jagrosh/jmusicbot/commands/owner/PlaylistCmd.java

Co-authored-by: Michaili K <mysteriouscursor+git@protonmail.com>

* Rid excessive hints

Co-authored-by: Michaili K <mysteriouscursor+git@protonmail.com>
2022-04-06 12:44:52 -04:00
John Grosh 85aebe8a89 Update make-release.yml 2022-04-05 19:06:50 -04:00
John Grosh 3c306a8d7a Update make-release.yml 2022-04-05 19:04:50 -04:00
John Grosh cadaf94cf7 Update make-release.yml 2022-04-05 18:55:05 -04:00
John Grosh 15a47a7b4d Update make-release.yml 2022-04-05 18:51:36 -04:00
John Grosh 92fca22059 Update make-release.yml 2022-04-05 18:42:44 -04:00
John Grosh 0cdc0d2d34 Update make-release.yml 2022-04-05 18:29:06 -04:00
John Grosh 1d1dfe103e Create make-release.yml 2022-04-05 18:25:29 -04:00
John Grosh ab39d2cd48 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2022-04-05 17:47:09 -04:00
John Grosh 32d165d1b3 add natives to base repo because I don't think any other repo is opening up soon 2022-04-05 17:46:59 -04:00
Kevin S 8457f944cd Fix warnings and deprecations 2022-03-31 18:30:19 -07:00
John Grosh c404e92932 Create test.yml 2022-01-06 16:10:02 -05:00
John Grosh 6af86cae5d Update bug-report.yml 2022-01-06 10:24:11 -05:00
John Grosh 9421730229 Delete feature-request.md 2022-01-06 10:18:58 -05:00
John Grosh b76e31c7e2 Delete bug-report.md 2022-01-06 10:18:48 -05:00
John Grosh 6056ff2c13 Merge pull request #1032 from yo-louk/queue-links
Queue links
2021-11-13 14:14:21 -05:00
yo-louk 8c92a7a75a changed QueuedTrack class instead 2021-11-12 00:55:16 +01:00
yo-louk e942f3ef21 changed list format 2021-11-12 00:31:31 +01:00
John Grosh 157da6c0da Update config.yml 2021-10-04 17:36:01 -04:00
John Grosh 03b909a3f5 whoops 2021-10-04 08:09:12 -04:00
Louk e08c1491c2 Voice channel mention (#932)
* Voice channel mention

I recently noticed voice channels can be mentioned just like text channels. A small change to the code to implement this feature.

* Voice channel mention

* vc.getName() changed to vc.getAsMention()
2021-10-02 15:17:00 -04:00
John Grosh 95de05ff2d Merge pull request #940 from MichailiK/feature/use-lyrics-anytime
Allow usage of LyricsCmd without playing music
2021-10-02 15:15:31 -04:00
Michaili K e242fa9557 Allow usage of LyricsCmd without playing music 2021-10-01 17:47:36 +02:00
John Grosh b83028fb97 use lavaplayer fork, fix update alerts, fix special case repeat mode parse 2021-09-27 14:19:48 -04:00
Michaili K 7273b5bf42 Add single repeat (#359)
* Add single repeat mode

* Toggle between NONE and REPEAT modes when no argument has been given

* Allow single as repeat mode

* Add one to the arguments

* Add backwards comparability for old versions of MusicBot.

* Use JSONObject#getEnum for parsing repeat mode

* Rename repeat modes & fix repeat one not repeating properly

* Add copyright notice & author to RepeatMode

* Fix spacing in SettingsManager

* Rename REPEAT_ONE to One in RepeatMode

* Rename repeat to repeat_mode

* Use variable on track end in AudioHandler & update RepeatMode & its usages

* Use user friendly name in RepeatCmd

* Remove unused constants

* Fix spacing in SettingsManager

* Rename repeat modes to off/all/single respectively
2021-09-27 19:51:57 +02:00
John Grosh 6f6064521b Create feature-request.yml 2021-09-21 22:08:51 -04:00
John Grosh 48263ec60a Merge pull request #825 from MichailiK/fix/path-loading-bug
Fix Windows path loading bug
2021-09-18 16:23:30 -04:00
Michaili K c6e5b8cbda Add "clear" alias to stop command (#847)
* Add "clear" alias to stop command

* Use "leave" as an alias to stop instead of "clear"
2021-09-18 16:21:55 -04:00
John Grosh 3b8892d2c0 fix queuedtrack string 2021-09-17 18:09:46 -04:00
John Grosh 54cb757dc5 Merge branch 'master' of https://github.com/jagrosh/MusicBot 2021-09-17 17:06:18 -04:00
John Grosh c4de816c7c added skip ratio and more request metadata beyond user id 2021-09-17 17:06:09 -04:00
John Grosh ccb421e110 Update bug-report.yml 2021-09-17 13:49:19 -04:00
John Grosh 23334e4253 Update bug-report.yml 2021-09-17 13:48:57 -04:00
John Grosh 826aec2492 Create bug-report.yml 2021-09-17 13:46:23 -04:00
John Grosh c397afcf8d Merge branch 'master' of https://github.com/jagrosh/MusicBot 2021-09-16 20:49:58 -04:00
John Grosh 95c07eab8e updated pom with working repos, and add comments with what we need to fix 2021-09-16 20:48:49 -04:00
John Grosh 58e0e5737d update pom with lowercase for circleci 2021-09-16 18:50:50 -04:00
John Grosh 84bc57e3cf Merge branch 'master' of https://github.com/jagrosh/MusicBot 2021-09-16 18:45:04 -04:00
John Grosh 67f6169e61 update pom 2021-09-16 18:45:02 -04:00
John Grosh 6f7c47757b Create FUNDING.yml 2021-09-16 18:42:46 -04:00
John Grosh 0a353e0fa3 Update README.md 2021-09-16 18:36:14 -04:00
Michaili K ddf95da19f Fix Windows path loading bug 2021-09-16 20:30:17 +02:00
John Grosh d5bb16d143 Update config.yml 2021-09-16 09:31:49 -04:00
John Grosh d47cfee8bf Create CNAME 2021-09-15 18:09:07 -04:00
John Grosh 1ee089e7ad Delete CNAME 2021-09-15 17:42:24 -04:00
John Grosh 7255e4b6db Create CNAME 2021-09-15 17:42:18 -04:00
John Grosh 4e314da86d Update README.md 2021-09-14 17:32:39 -04:00
John Grosh ec06cf10e9 update reference.conf 2021-08-30 21:09:48 -04:00
John Grosh ad972185d4 update deps
add transformative audio source
fix intents
2021-08-30 02:17:34 -04:00
John Grosh bc64b45d5c whoops 2021-04-02 14:21:39 -04:00
John Grosh 699c2a0e6e update deps and add noprompt mode 2021-04-02 14:02:16 -04:00
John Grosh ebf6e43084 Merge pull request #583 from MichailiK/feature/auto-leave-when-alone
Add "alone time until stop"
2021-02-09 09:55:50 -05:00
Michaili K 6527afb88c Ignore all bots when checking if the bot is alone 2021-02-09 15:52:41 +01:00
Michaili K 18e142ed88 Store guild ID instead of the guild object in AloneInVoiceHandler 2021-02-09 15:09:59 +01:00
John Grosh 797d306a4e Update README.md 2021-02-08 17:10:24 -05:00
John Grosh 267bf71d47 Update feature-request.md 2021-02-08 10:03:08 -05:00
John Grosh 88a89f2178 Update config.yml 2021-02-08 09:37:16 -05:00
John Grosh 8d2f60eba5 Merge origin/master 2021-02-06 19:53:03 -05:00
John Grosh b4c9a125e5 deps update and ref cleanup 2021-02-06 19:52:48 -05:00
Michaili K 36c53cfd41 Add "alone time until stop" feature & config 2021-01-13 01:58:21 +01:00
John Grosh d1223e9795 Update config.yml 2020-12-03 12:33:45 -05:00
John Grosh cebdbc3aa5 initial version of automation script 2020-11-04 18:56:10 -05:00
John Grosh be4f235071 Merge origin/master 2020-11-04 08:07:20 -04:00
John Grosh e8df9e9523 fix aboutcommand error 2020-11-04 08:07:00 -04:00
John Grosh 9a79308065 Update and rename feature_request.md to feature-request.md 2020-11-02 15:52:57 -05:00
John Grosh 7a55e76a35 Update bug-report.md 2020-11-02 15:51:15 -05:00
John Grosh c92c55ca52 Create config.yml 2020-11-02 15:46:36 -05:00
John Grosh 2bb43b57ba return to main lavaplayer 2020-10-28 07:10:56 -04:00
John Grosh 403a43530f deps 2020-10-27 16:44:58 -04:00
John Grosh 51f9024498 first iteration of handling new gateway 2020-10-27 05:57:05 -04:00
John Grosh 9f7ad33707 Merge pull request #512 from jagrosh/dependabot/maven/junit-junit-4.13.1
Bump junit from 4.12 to 4.13.1
2020-10-21 23:09:23 -04:00
dependabot[bot] fa5241490f Bump junit from 4.12 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 10:57:44 +00:00
John Grosh 27fd75aa67 deps update and small code cleanup 2020-07-04 17:02:19 -04:00
John Grosh c5a583b96f update deps and deal with discords new ratelimits 2020-05-30 13:26:06 -04:00
John Grosh f1e301b48e fix windows path loading bug
fix empty game bug
2020-04-09 16:13:48 -04:00
John Grosh 61d4c51276 update deps 2020-03-23 17:05:02 -04:00
John Grosh cbdbe7bf4b update lavaplayer 2020-02-02 20:42:00 -05:00
John Grosh baa3a60b05 Merge origin/master
Conflicts:
	pom.xml
2020-01-28 21:02:36 -05:00
John Grosh 338dafca0c deps update 2020-01-28 21:00:57 -05:00
John Grosh b1dddb649b Merge pull request #342 from SplitPixl/patch-1
Add the letter "s"
2020-01-17 10:13:39 -05:00
SplitPixl 3eb284da55 Add the letter "s"
As of January 2020, JCenter rejects all non-https connections with a 403 Forbidden. 

https://jfrog.com/blog/secure-jcenter-with-https/
2020-01-16 20:02:26 -08:00
John Grosh 5fb969283d Update bug-report.md 2019-11-10 00:54:29 -05:00
John Grosh ae8b5cfe90 first pass of per-server prefixes 2019-11-09 20:22:53 -05:00
John Grosh b6fa7de7e2 Merge origin/master 2019-11-09 18:16:13 -05:00
John Grosh a4f13ca2de Merge pull request #308 from MichailiK/pr/feature/config/aliases
Add configurable aliases for all commands
2019-11-09 18:15:38 -05:00
John Grosh 5172deb12d dep update and fix playlist shuffling 2019-11-07 22:07:31 -05:00
MichailiK 179f447a70 Catch ConfigException.Missing in BotConfig#getAliases(string) 2019-10-26 09:39:56 +02:00
MichailiK f4a937cc6b Remove unused import from SCSearchCmd 2019-10-25 20:03:45 +02:00
MichailiK 0b4c6c9af1 Use Bot#getConfig() instead of using a public static config & Add missing newline 2019-10-25 19:48:41 +02:00
MichailiK f17058a344 Put aliases inside an "aliases"-block 2019-10-25 19:25:05 +02:00
MichailiK 4fcf86a5d6 Fix alias of volume 2019-10-25 18:14:23 +02:00
MichailiK 94362d640b Change Example 2 2019-10-25 17:56:55 +02:00
MichailiK 7192d55732 Add configurable aliases for all commands 2019-10-25 17:51:56 +02:00
John Grosh 11c9b00510 fix DJ permissions in PlayCmd
general cleanup
2019-10-21 01:02:48 -04:00
John Grosh 4579d6ca09 update deps and bump version 2019-10-21 00:27:19 -04:00
John Grosh c3a9937c90 first pass of debug command 2019-10-21 00:25:45 -04:00
MichailiandM R S E 8650f82210 Fix bot joining in AFK voice channels (#292)
* Fix bot joining in AFK voice channels

Since Discord automatically mutes users in AFK channels, bots shouldn't
play music inside AFK channels. This commit also fixes #147.

* Apply requested changes

* Fix potential NPE

Co-Authored-By: M R S E <Maurice.Craft@t-online.de>

* Fix indention
2019-10-19 23:02:28 -04:00
John Grosh 3ba13d2daa Merge pull request #296 from MichailiK/rp/fix/nowplayinghandler/refreshes-even-with-npimages-enabled
Fix nowplaying being updated even when NPImages is true.
2019-10-16 18:34:29 -04:00
MichailiK dace101c30 Fix nowplaying being updated even when NPImages is true. 2019-10-17 00:27:07 +02:00
John Grosh 05e8ec6814 Merge pull request #290 from MichailiK/pr/fix/pausebug
Fix player being paused when it is paused, then the queue being cleared
2019-10-15 13:19:26 -04:00
John Grosh 646ed6e9b1 Merge pull request #294 from MichailiK/pr/feature/command/queue/wrap-page-ends
Add wrapping of page ends in queue command
2019-10-15 13:15:41 -04:00
MichailiK a501b00ab7 Add wrapping of page ends in queue command
This feature wraps both page ends, meaning that users can jump from the
first page to the last page, using the previous page reaction,
while using the next page button on the last page brings you to the
first page.

Resolves #233.
2019-10-15 18:33:32 +02:00
MichailiK 6ae683b749 Fix player being paused when it is paused, then the queue being cleared
This commit fixes #75, where the player doesn't start playing when
it's been previously paused.
This may happen when a user pauses the player, clears the queue or skips
all songs and another user tries to use the player later.
2019-10-15 10:48:04 +02:00
John Grosh 235af8c8c6 Merge pull request #287 from MichailiK/pr-forceremovecmd-copyright
Add copyright and author to ForceRemoveCmd.java
2019-10-12 16:44:02 -04:00
MichailiK 33e4e8e5ce Add copyright and author to ForceRemoveCmd.java 2019-10-12 22:33:53 +02:00
John Grosh 20b23a3b0d Merge pull request #243 from MichailiK/pr-forceremovecmd
Added ForceRemoveCmd
2019-10-07 09:52:23 -04:00
MichailiK d616c1e386 Apply requested changes (Pass 3) 2019-10-05 19:37:19 +02:00
MichailiK e6f2b23ca9 Fix minor oversight 2019-10-05 10:30:59 +02:00
MichailiK fbfea7ab1f Apply requested changes (Pass 2) 2019-10-04 17:19:11 +02:00
MichailiK bc85fb9597 Apply requested changes 2019-09-12 14:44:13 +02:00
John Grosh a34f06669d removed build files 2019-09-11 19:31:15 -04:00
John Grosh 6508c2b62c dependency and version bump 2019-09-11 19:04:13 -04:00
John Grosh fb53b9e8ea deps updates 2019-07-31 23:35:50 -04:00
Michaili K 75abab2832 Removed redundant code. 2019-06-30 19:09:43 +02:00
Michaili K 13d9fa87d8 Added ForceRemoveCmd 2019-06-30 14:54:42 +02:00
101 changed files with 6198 additions and 929 deletions

No files matched your search

-21
View File
@@ -1,21 +0,0 @@
version: '{build}'
os: Windows Server 2012
install:
- ps: |
Add-Type -AssemblyName System.IO.Compression.FileSystem
if (!(Test-Path -Path "C:\maven" )) {
(new-object System.Net.WebClient).DownloadFile(
'http://www.us.apache.org/dist/maven/maven-3/3.2.5/binaries/apache-maven-3.2.5-bin.zip',
'C:\maven-bin.zip'
)
[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "C:\maven")
}
- cmd: SET M2_HOME=C:\maven\apache-maven-3.2.5
- cmd: SET PATH=%SystemRoot%\system32;C:\maven\apache-maven-3.2.5\bin;%JAVA_HOME%\bin;%PATH
build_script:
- mvn clean package --batch-mode -DskipTest
test_script:
- mvn clean install --batch-mode
cache:
- C:\maven\
- C:\Users\appveyor\.m2
+12 -1
View File
@@ -7,7 +7,7 @@ jobs:
build:
docker:
# specify the version you desire here
- image: circleci/openjdk:8u171-jdk
- image: cimg/openjdk:11.0
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
@@ -39,3 +39,14 @@ jobs:
# run tests!
- run: mvn integration-test
workflows:
version: 2
build:
jobs:
- build:
filters:
branches:
ignore:
- gh-pages
- gh-pages-generated
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: jagrosh
patreon: jagrosh
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-30
View File
@@ -1,30 +0,0 @@
---
name: Bug Report
about: Report a bug or unexpected behavior
title: "[Bug Report] Title Here"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Run with '...'
2. Do a command '....'
3. Some other step '....'
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version Info (please complete the following information):**
- Operating System: [e.g. Windows 64bit]
- JMusicBot Version: [e.g. 0.2.1]
**Additional context**
Add any other context about the problem here.
+64
View File
@@ -0,0 +1,64 @@
name: Bug Report
description: Report a bug or unexpected behavior
title: "[Bug Report] Title Here "
labels: []
assignees: []
body:
- type: textarea
id: description
attributes:
label: Bug Description
description: Please provide a clear and concise description of what the bug is.
placeholder: The bug is...
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce
description: Provide steps you can take to reliably reproduce this bug
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Result
description: What did you expect to happen when this bug occurred?
placeholder: I expected...
validations:
required: true
- type: textarea
id: debug
attributes:
label: Debug Output
description: Please run the `debug` command and paste it here or upload it as an attachment. If you cannot run the command, please provide information about your system such as operating system version, JMusicBot version, etc.
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Info
description: Any additional info that you think might be useful or relevant to this bug
- type: checkboxes
id: checks
attributes:
label: Checklist
description: Before submitting your issue, please affirm the following
options:
- label: I have looked for information about this [within the documentation](https://jmusicbot.com/)
required: true
- label: I have searched for similar issues [on the issues page](https://github.com/jagrosh/MusicBot/issues?q=is%3Aissue)
required: true
- label: I am using an official release from [the releases page](https://github.com/jagrosh/MusicBot/releases)
required: true
- type: input
id: version
attributes:
label: JMusicBot Version
description: Which version of JMusicBot are you running?
validations:
required: true
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/jagrosh/MusicBot/discussions/categories/questions
about: For general questions, please check the Discussions
- name: Troubleshooting
url: https://github.com/jagrosh/MusicBot/discussions/categories/troubleshooting
about: For troubleshooting help, please check the Discussions
@@ -0,0 +1,52 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature Request] Title Here"
labels: []
assignees: []
body:
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem? Please describe.
description: Please provide a clear and concise description of what the problem is.
placeholder: A common problem users face is...
validations:
required: true
- type: textarea
id: solution
attributes:
label: What is your ideal solution to the problem?
description: Describe a clear and concise description of what you want to happen.
placeholder: My solution to the above problem is...
validations:
required: true
- type: textarea
id: procedure
attributes:
label: How would this feature be used?
description: Describe the basic flow/steps of using this feature.
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Info
description: Any additional info that you think might be useful or relevant to this feature request
- type: checkboxes
id: checks
attributes:
label: Checklist
description: Before submitting your issue, please affirm the following
options:
- label: I have checked [the documentation](https://jmusicbot.com/) to make sure this feature doesn't already exist
required: true
- label: I have searched for [similar feature requests](https://github.com/jagrosh/MusicBot/labels/enhancement)
required: true
- label: I have confirmed that my requests is **not** on the [list of things that will not be added](https://jmusicbot.com/things-that-wont-be-added)
required: true
- label: "I am running the latest version of the bot: ![Release](https://img.shields.io/github/release/jagrosh/MusicBot.svg)"
required: true
-27
View File
@@ -1,27 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request] Title Here"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe the basic flow/steps of using this feature**
How this feature would be used:
1. Set something '...'
2. Run some command '...'
3. Other steps or related info '...'
4. ...
**Additional context**
Add any other context or screenshots about the feature request here.
**Have you confirmed that this idea is not already described here?**
https://github.com/jagrosh/MusicBot/projects/1
+22
View File
@@ -0,0 +1,22 @@
name: Build and Test
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: maven
- name: Build and Test with Maven
run: mvn --batch-mode --update-snapshots verify
+70
View File
@@ -0,0 +1,70 @@
name: Make Release
on:
workflow_dispatch:
inputs:
version_number:
description: 'Version Number'
required: true
type: string
info:
description: 'Description of this Release'
required: true
type: string
jobs:
build_jar:
name: Build Jar
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: maven
- name: Set Version
uses: datamonsters/replace-action@v2
with:
files: 'pom.xml'
replacements: 'Snapshot=${{ github.event.inputs.version_number }}'
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
- name: Rename jar
run: mv target/*-All.jar JMusicBot-${{ github.event.inputs.version_number }}.jar
- name: Upload jar
uses: actions/upload-artifact@v3
with:
name: jar
path: JMusicBot-${{ github.event.inputs.version_number }}.jar
if-no-files-found: error
create_release:
name: Create Release
runs-on: ubuntu-latest
needs: build_jar
steps:
- name: Download a Build Artifact
uses: actions/download-artifact@v3.0.0
with:
name: jar
path: .
- name: Show Artifacts
run: ls -R
- name: Create Release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.event.inputs.version_number }}
name: Version ${{ github.event.inputs.version_number }}
draft: true
prerelease: false
artifacts: "*.jar"
body: |
${{ github.event.inputs.info }}
---
### Setup
https://jmusicbot.com/setup
https://jmusicbot.com/config
# Download: [JMusicBot-${{ github.event.inputs.version_number }}.jar](https://github.com/jagrosh/MusicBot/releases/download/${{ github.event.inputs.version_number }}/JMusicBot-${{ github.event.inputs.version_number }}.jar)
+4 -1
View File
@@ -1,6 +1,9 @@
/nbproject/
/target/
/Playlists/
/test/
*.json
*.txt
nb*.xml
nb*.xml
dockerfile
.*
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<settings xsi:schemaLocation='http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd'
xmlns='http://maven.apache.org/SETTINGS/1.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
<profiles>
<profile>
<id>bintray</id>
<repositories>
<repository>
<id>central</id>
<name>bintray</name>
<url>http://jcenter.bintray.com</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>bintray</activeProfile>
</activeProfiles>
</settings>
-2
View File
@@ -1,2 +0,0 @@
language: java
install: mvn install -Dmaven.javadoc.skip=true --batch-mode --show-version --settings .travis.settings.xml
+1
View File
@@ -0,0 +1 @@
jmusicbot.com
+111 -31
View File
@@ -1,42 +1,122 @@
<img align="right" src="https://i.imgur.com/zrE80HY.png" height="200" width="200">
# JMusicBot
# JMusicBot Fork
[![Downloads](https://img.shields.io/github/downloads/jagrosh/MusicBot/total.svg)](https://github.com/jagrosh/MusicBot/releases/latest)
[![Stars](https://img.shields.io/github/stars/jagrosh/MusicBot.svg)](https://github.com/jagrosh/MusicBot/stargazers)
[![Release](https://img.shields.io/github/release/jagrosh/MusicBot.svg)](https://github.com/jagrosh/MusicBot/releases/latest)
[![License](https://img.shields.io/github/license/jagrosh/MusicBot.svg)](https://github.com/jagrosh/MusicBot/blob/master/LICENSE)
[![Discord](https://discordapp.com/api/guilds/147698382092238848/widget.png)](https://discord.gg/0p9LSGoRLu6Pet0k)<br>
[![CircleCI](https://img.shields.io/circleci/project/github/jagrosh/MusicBot/master.svg)](https://circleci.com/gh/jagrosh/MusicBot)
[![Travis-CI](https://travis-ci.com/jagrosh/MusicBot.svg?branch=master)](https://travis-ci.com/jagrosh/MusicBot)
[![AppVeyor](https://ci.appveyor.com/api/projects/status/gdu6nyte5psj6xfk/branch/master?svg=true)](https://ci.appveyor.com/project/jagrosh/musicbot/branch/master)
[![CodeFactor](https://www.codefactor.io/repository/github/jagrosh/musicbot/badge)](https://www.codefactor.io/repository/github/jagrosh/musicbot)
A self-hosted Discord music bot fork based on JMusicBot, updated to keep working on modern Discord and JDA versions.
A cross-platform Discord music bot with a clean interface, and that is easy to set up and run yourself!
This repository keeps the bot practical to run today, while also adding fork-specific features such as status messages and playback analytics logging. Possibly more.
[![Setup](http://i.imgur.com/VvXYp5j.png)](https://github.com/jagrosh/MusicBot/wiki/Setup)
## Demo
Temporary invite: [discord.gg/4cp9CvjwaW](https://discord.gg/4cp9CvjwaW)
Permanent invite: [discord.gg/hK95396uB3](https://discord.gg/hK95396uB3)
## What This Fork Adds
- Make it work again, through:
- JDA 6 support and current dependency updates
- DAVE voice encryption support for current Discord voice requirements
- Optional YouTube account login support
- Optional YouTube PO token and visitor-data support
- New niche features, such as:
- Optional per-channel playback status message
- Optional JDBC/SQLite playback analytics logging
- Basic QoL improvements, such as:
- Docker image and container-friendly deployment path
## Quick Links
- Fork changes and version notes: [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md)
- Setup guide: [docs/setup.md](/git/MusicBot/docs/setup.md)
- Docker usage: [docs/docker.md](/git/MusicBot/docs/docker.md)
## Features
* Easy to run (just make sure Java is installed, and run!)
* Fast loading of songs
* No external keys needed (besides a Discord Bot token)
* Smooth playback
* Server-specific setup for the "DJ" role that can moderate the music
* Clean and beautiful menus
* Channel-topic playback bar
* Supports many sites, including Youtube, Soundcloud, and more
* Supports many online radio/streams
* Supports local files
* Playlist support (both web/youtube, and local)
## Example
![Loading Example...](https://i.imgur.com/kVtTKvS.gif)
- Easy self-hosting
- Fast song loading
- Smooth playback
- DJ role and server-specific settings
- Queue management and playlist support
- Support for many sources, streams, and local files
## Setup
Please see the [Setup Page](https://github.com/jagrosh/MusicBot/wiki/Setup) in the wiki to run this bot yourself!
## Supported Sources And Formats
## Questions/Suggestions/Bug Reports
**Please read the [Suggested/Planned Features List](https://github.com/jagrosh/MusicBot/projects/1) before suggesting a feature**. If you'd like to suggest changes to how the bot functions, recommend more customization options, or report bugs, feel free to either open an [Issue](https://github.com/jagrosh/MusicBot/issues) on this repository, or join [my Discord server](https://discord.gg/0p9LSGoRLu6Pet0k). (Note: I will not accept any feature requests that will require additional API keys, nor any non-music features). If you like this bot, be sure to add a star to the libraries that make this possible: [**JDA**](https://github.com/DV8FromTheWorld/JDA) and [**lavaplayer**](https://github.com/sedmelluq/lavaplayer)
JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats).
## Editing
This bot (and the source code here) might not be easy to edit for inexperienced programmers. The main purpose of having the source public is to show the capabilities of the libraries, to allow others to understand how the bot works, and to allow those knowledgeable about java, JDA, and Discord bot development to contribute. There are many requirements and dependencies required to edit and compile it, and there will not be support provided for people looking to make changes on their own. Instead, consider making a feature request (see the above section). If you choose to make edits, please do so in accordance with the Apache 2.0 License.
Sources:
- YouTube
- SoundCloud
- Bandcamp
- Vimeo
- Twitch streams
- Local files
- HTTP URLs
Formats:
- MP3
- FLAC
- WAV
- Matroska/WebM
- MP4/M4A
- OGG streams
- AAC streams
- Stream playlists such as M3U and PLS
## Setup Notes
If you only need the essentials:
1. Create a Discord bot and get its token, see [here](https://jmusicbot.com/getting-a-bot-token/).
2. Enable the `MESSAGE_CONTENT` intent in the Discord Developer Portal [here](https://discord.com/developers/applications) ([Wiki link](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/)).
3. Configure `config.txt`.
4. Run the jar directly or use Docker.
For the detailed version, see [docs/setup.md](/git/MusicBot/docs/setup.md).
## Docker
A Docker image is published here: [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
For compose examples and deployment notes, see [docs/docker.md](/git/MusicBot/docs/docker.md).
## YouTube Login
This fork supports Google account login for YouTube playback workarounds.
Enable:
```conf
youtubeoauth2=true
```
Do not use your main Google account. Use a burner or otherwise disposable account instead.
More details are in [docs/setup.md](/git/MusicBot/docs/setup.md).
If you use PO tokens or visitor data for YouTube, those details are also covered in [docs/setup.md](/git/MusicBot/docs/setup.md).
## DAVE / Voice Encryption
Discord currently requires DAVE for voice on modern API versions. This fork configures `libdave-jvm` during startup and bundles native binaries for common Linux, Windows, and macOS targets.
If you deploy on an unsupported architecture, add the matching native dependency in [pom.xml](/git/MusicBot/pom.xml). You probably need to build the library from source for your architecture, so have fun with that.
## Upstream And Related Work
This fork incorporates ideas and patches related to:
- [PR-1670](https://github.com/jagrosh/MusicBot/pull/1670)
- [PR-1703](https://github.com/jagrosh/MusicBot/pull/1703)
- [PR-1772](https://github.com/jagrosh/MusicBot/pull/1772)
If this fork does not fit your needs, another fork you may want to inspect is [SeVile/MusicBot](https://github.com/SeVile/MusicBot).
## Questions And Contributions
If you run into issues, please include:
- the version you are running
- whether you are using Docker or the jar directly
- relevant config changes
- relevant logs
Fork-specific version notes live in [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md), which should help with troubleshooting and migration as the fork evolves.
+93
View File
@@ -0,0 +1,93 @@
# Docker Guide
This fork supports running the bot in Docker, which is usually the easiest deployment path for a server.
## Image
Published image:
- [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
Container build sources:
- [github.com/chrisb09/jmb-container](https://github.com/chrisb09/jmb-container)
## Minimal Compose Example
```yaml
version: "2.2"
services:
jmusicbot:
image: chrisb09/jmusicbot:latest
container_name: jmusicbot
restart: unless-stopped
volumes:
- ./config:/jmb/config
```
Replace `./config` with the host path you actually want to use.
## Config Files
Your persistent bot config should live in the mounted config directory. That lets you redeploy containers without losing your bot settings.
Typical things to configure there:
- bot token
- owner ID
- prefix
- YouTube login toggle
- YouTube PO token and visitor-data values if needed
- status message toggle
- analytics logging settings
## Deploy Flow
A typical update flow is:
1. Pull or rebuild the image.
2. Restart the container.
3. Check logs for startup errors.
Useful commands:
```bash
docker-compose up -d
docker-compose logs -f jmusicbot
```
## Alpine / musl Note
This fork bundles native support for Alpine-compatible musl environments as part of its DAVE setup, so Alpine-based container deployments are supported.
## SQLite Logging In Docker
If you enable analytics logging with SQLite:
```conf
dblog.enabled = true
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
```
make sure the database file lives somewhere inside your mounted persistent config or data path if you want it to survive container recreation.
## YouTube Token Notes In Docker
If you use YouTube login or PO token based configuration, keep those values in your persistent mounted config rather than baking them into the image.
Relevant config values include:
```conf
youtubeoauth2 = true
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
## Recommendation
For production-style use, Docker is the cleanest path if you want:
- reproducible runtime behavior
- simple upgrades
- easier separation between bot code and persistent config
+57
View File
@@ -0,0 +1,57 @@
# Fork Changelog
This file tracks fork-specific changes that are useful to operators and contributors. It is not meant to replace commit history, but to summarize what changed at a higher level.
## 0.6.3
- Added JDBC-backed playback analytics logging.
- Added SQLite as the default logging backend.
- Logged queue, playback, listener, search, skip, and command activity.
- Added `dblog.*` config settings to enable and configure logging.
Config:
```conf
dblog.enabled = true
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
dblog.user = ""
dblog.password = ""
```
By default, logging is disabled.
## 0.6.2
- Fixed several status-message race conditions and repost issues.
- Added debug logging around status-message creation, editing, deletion, and repost flow.
- Added a config toggle to disable the status-message feature entirely.
Config:
```conf
statusmessages = true
```
Set it to `false` to disable the feature.
## 0.6.1
- Bumped the fork version after the initial status-message work had landed.
## 0.6.0
- Updated the bot for modern JDA and Discord requirements.
- Added DAVE voice support required for current Discord voice playback.
- Continued dependency refreshes for lavaplayer and related components.
- Carried forward work related to YouTube login, PO token handling, and buildability on the newer stack.
Imported work included in this fork:
- PR-1670, support for logging into a YouTube account, is included from `0.4.5` onward.
- PR-1703, changes to allow building the project from source more easily, is included from `0.4.5` onward.
- `jagrosh#1772`, YouTube PO token and IP rotation support, is included from `0.4.7` onward.
## Notes
- This fork may move faster than upstream in areas related to Discord API compatibility and deployment practicality.
- When reporting issues, mention the exact fork version because behavior around playback, status messages, and logging has changed across recent releases.
+163
View File
@@ -0,0 +1,163 @@
# Setup Guide
This guide covers the normal non-Docker setup path and the main configuration points that matter on this fork.
## Prerequisites
- Java 11 or newer
- A Discord bot token
- A Discord application with the bot invited to your server
## Discord Setup
1. Create a bot in the [Discord Developer Portal](https://discord.com/developers/applications).
2. Copy the bot token.
3. Enable the `MESSAGE_CONTENT` intent.
This intent is required for command-based bots like this one.
## Privileged Intent Notes
This fork moved from JDA 4 to JDA 6, so `MESSAGE_CONTENT` is now an important operational requirement for the classic text-command flow.
Keep in mind:
- If the bot joins more than 100 servers, this intent must be enabled in the [Discord Developer Portal](https://discord.com/developers/applications).
- `MESSAGE_CONTENT` is a privileged intent.
- Discord verification requirements become relevant as the bot grows.
- The related portal controls historically only become visible once the application is already in a fairly large number of servers.
For the broader Discord/JDA behavior, see the [JDA gateway intents documentation](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/).
## First Configuration
The bot reads from `config.txt` using the values defined in [reference.conf](/git/MusicBot/src/main/resources/reference.conf).
The minimum settings you should care about first are:
```conf
token = "YOUR_BOT_TOKEN"
owner = 123456789012345678
prefix = "@mention"
```
Useful fork-specific options:
```conf
youtubeoauth2 = true
statusmessages = true
dblog.enabled = false
```
You may also need these for YouTube PO token support:
```conf
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
## Running The Bot
Build:
```bash
mvn clean package
```
Run:
```bash
java -jar target/JMusicBot-*-All.jar
```
This fork includes changes to keep source builds practical on the newer stack as well.
## YouTube Login
If YouTube playback is unreliable, enable:
```conf
youtubeoauth2 = true
```
After startup, the bot prints instructions and sends the owner a DM with the authorization flow.
Important:
- Do not use your main Google account.
- Use a burner or otherwise disposable account.
If you are starting from a clean config, this option should already exist with a default of `false`.
## YouTube PO Tokens And Visitor Data
This fork also supports explicit YouTube PO token and visitor-data configuration.
Config:
```conf
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
You can generate these values with:
- [iv-org/youtube-trusted-session-generator](https://github.com/iv-org/youtube-trusted-session-generator)
This is useful when normal YouTube playback is not enough and you need a more explicit trusted-session style setup.
## DAVE / Voice Support
This fork includes DAVE support for current Discord voice requirements. In normal supported environments, nothing extra is required beyond using the provided shaded jar.
If you run on an unsupported architecture, add the correct native `libdave-jvm` dependency in [pom.xml](/git/MusicBot/pom.xml).
Bundled native targets include:
- Linux glibc x86_64
- Linux musl x86_64
- Windows x86_64
- macOS
## Status Messages
The bot can maintain a single playback status message in the active text channel.
Config:
```conf
statusmessages = true
```
Disable it with:
```conf
statusmessages = false
```
## Analytics Logging
Playback analytics logging is off by default.
Enable SQLite logging:
```conf
dblog.enabled = true
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
```
This creates a local SQLite database file and records queue, playback, listener, and command activity.
## Windows Note
If you run the bot directly on Windows rather than in Docker, install Java first and consider a service wrapper such as [NSSM](http://nssm.cc/) if you want it to run in the background.
## Related Fork Context
This fork incorporates or builds on work related to:
- [PR-1670](https://github.com/jagrosh/MusicBot/pull/1670) for YouTube account login support
- [PR-1703](https://github.com/jagrosh/MusicBot/pull/1703) for source builds
- [PR-1772](https://github.com/jagrosh/MusicBot/pull/1772) for YouTube PO token and IP rotation work
If this fork does not fit your setup, another fork you may want to inspect is [SeVile/MusicBot](https://github.com/SeVile/MusicBot).
+216 -27
View File
@@ -3,58 +3,236 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.jagrosh</groupId>
<artifactId>JMusicBot</artifactId>
<version>0.2.3</version>
<version>0.6.5</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>central</id>
<name>bintray</name>
<url>http://jcenter.bintray.com</url>
<id>m2-chew</id>
<name>Maven Chewtral</name>
<url>https://m2.chew.pro/releases</url>
</repository>
<repository>
<id>bintray-jagrosh-maven</id>
<name>bintray</name>
<url>https://dl.bintray.com/jagrosh/maven</url>
<id>dv8tion</id>
<name>m2-dv8tion</name>
<url>https://m2.dv8tion.net/releases</url>
</repository>
<repository>
<id>central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
<repository>
<id>m2.duncte123.dev</id>
<name>m2-duncte123</name>
<url>https://m2.duncte123.dev/releases</url>
</repository>
<repository>
<id>arbjergDev</id>
<name>Lavalink Repository</name>
<url>https://maven.lavalink.dev/releases</url>
</repository>
<repository>
<id>arbjergDevSnapshots</id>
<name>Lavalink Repository</name>
<url>https://maven.lavalink.dev/snapshots</url>
</repository>
</repositories>
<dependencies>
<!-- Discord Dependencies -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>3.8.3_463</version>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>6.3.1</version>
</dependency>
<dependency>
<groupId>com.sedmelluq</groupId>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>adapter-jda</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>impl-jni</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-linux-x86-64</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-linux-musl-x86-64</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-win-x86-64</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>moe.kyokobot.libdave</groupId>
<artifactId>natives-darwin</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-command</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-examples</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>${jda.chewtils.version}</version>
<scope>compile</scope>
</dependency>
<!-- <dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-command</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>2.2.1</version>
</dependency> -->
<!--
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-command</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.github.chew.jda-chewtils</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>2.2.1</version>
</dependency>
-->
<!-- Music Dependencies -->
<dependency>
<groupId>dev.arbjerg</groupId>
<artifactId>lavaplayer</artifactId>
<version>1.3.17</version>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>com.jagrosh</groupId>
<artifactId>jda-utilities</artifactId>
<version>2.1.5</version>
<type>pom</type>
<groupId>dev.arbjerg</groupId>
<artifactId>lavaplayer-ext-youtube-rotator</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>com.jagrosh</groupId>
<artifactId>JLyrics</artifactId>
<version>0.2</version>
<groupId>dev.lavalink.youtube</groupId>
<artifactId>common</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.github.jagrosh</groupId>
<artifactId>JLyrics</artifactId>
<version>master</version>
</dependency>
<dependency>
<groupId>com.dunctebot</groupId>
<artifactId>sourcemanagers</artifactId>
<version>1.9.0</version>
</dependency>
<!-- Misc Internal Dependencies -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.1.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.2</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.27.0</version>
<!-- https://github.com/SeleniumHQ/selenium/issues/13791 -->
<exclusions>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-devtools-v85</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-devtools-v129</artifactId>
</exclusion>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-devtools-v130</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.2</version>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -64,8 +242,18 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/filtered-resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -106,10 +294,11 @@
</plugin>
</plugins>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<jda.chewtils.version>2.2.1</jda.chewtils.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# This will have this script check for a new version of JMusicBot every
# startup (and download it if the latest version isn't currently downloaded)
DOWNLOAD=true
# This will cause the script to run in a loop so that the bot auto-restarts
# when you use the shutdown command
LOOP=true
download() {
if [ $DOWNLOAD = true ]; then
URL=$(curl -s https://api.github.com/repos/jagrosh/MusicBot/releases/latest \
| grep -i "browser_download_url.*\.jar" \
| sed 's/.*\(http.*\)"/\1/')
FILENAME=$(echo $URL | sed 's/.*\/\([^\/]*\)/\1/')
if [ -f $FILENAME ]; then
echo "Latest version already downloaded (${FILENAME})"
else
curl -L $URL -o $FILENAME
fi
fi
}
run() {
java -Dnogui=true -jar $(ls -t JMusicBot* | head -1)
}
while
download
run
$LOOP
do
continue
done
@@ -0,0 +1 @@
jda-chewtils.version=${jda.chewtils.version}
+56 -8
View File
@@ -18,16 +18,20 @@ package com.jagrosh.jmusicbot;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
import com.jagrosh.jmusicbot.audio.AloneInVoiceHandler;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.NowplayingHandler;
import com.jagrosh.jmusicbot.audio.PlayerManager;
import com.jagrosh.jmusicbot.audio.StatusMessageHandler;
import com.jagrosh.jmusicbot.datalog.DataLogService;
import com.jagrosh.jmusicbot.gui.GUI;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader;
import com.jagrosh.jmusicbot.settings.SettingsManager;
import java.util.Objects;
import net.dv8tion.jda.core.JDA;
import net.dv8tion.jda.core.entities.Game;
import net.dv8tion.jda.core.entities.Guild;
import com.jagrosh.jmusicbot.utils.YoutubeOauth2TokenHandler;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
/**
*
@@ -42,6 +46,10 @@ public class Bot
private final PlayerManager players;
private final PlaylistLoader playlists;
private final NowplayingHandler nowplaying;
private final StatusMessageHandler statusMessageHandler;
private final AloneInVoiceHandler aloneInVoiceHandler;
private final DataLogService dataLogService;
private final YoutubeOauth2TokenHandler youTubeOauth2TokenHandler;
private boolean shuttingDown = false;
private JDA jda;
@@ -54,10 +62,29 @@ public class Bot
this.settings = settings;
this.playlists = new PlaylistLoader(config);
this.threadpool = Executors.newSingleThreadScheduledExecutor();
this.players = new PlayerManager(this);
this.youTubeOauth2TokenHandler = new YoutubeOauth2TokenHandler();
this.youTubeOauth2TokenHandler.init();
this.players = new PlayerManager(this, config);
this.players.init();
this.nowplaying = new NowplayingHandler(this);
this.nowplaying.init();
this.statusMessageHandler = new StatusMessageHandler(this);
this.statusMessageHandler.init();
this.aloneInVoiceHandler = new AloneInVoiceHandler(this);
this.aloneInVoiceHandler.init();
DataLogService dls = null;
if(config.getDataLogEnabled())
{
try
{
dls = new DataLogService(this, config.getDataLogJdbcUrl(), config.getDataLogUser(), config.getDataLogPassword());
}
catch(Exception ex)
{
dls = null;
}
}
this.dataLogService = dls;
}
public BotConfig getConfig()
@@ -94,6 +121,26 @@ public class Bot
{
return nowplaying;
}
public StatusMessageHandler getStatusMessageHandler()
{
return statusMessageHandler;
}
public AloneInVoiceHandler getAloneInVoiceHandler()
{
return aloneInVoiceHandler;
}
public DataLogService getDataLogService()
{
return dataLogService;
}
public YoutubeOauth2TokenHandler getYouTubeOauth2Handler()
{
return youTubeOauth2TokenHandler;
}
public JDA getJDA()
{
@@ -109,9 +156,9 @@ public class Bot
public void resetGame()
{
Game game = config.getGame()==null || config.getGame().getName().equalsIgnoreCase("none") ? null : config.getGame();
if(!Objects.equals(jda.getPresence().getGame(), game))
jda.getPresence().setGame(game);
Activity game = config.getGame()==null || config.getGame().getName().equalsIgnoreCase("none") ? null : config.getGame();
if(!Objects.equals(jda.getPresence().getActivity(), game))
jda.getPresence().setActivity(game);
}
public void shutdown()
@@ -130,13 +177,14 @@ public class Bot
{
ah.stopAndClear();
ah.getPlayer().destroy();
nowplaying.updateTopic(g.getIdLong(), ah, true);
}
});
jda.shutdown();
}
if(gui!=null)
gui.dispose();
if(dataLogService != null)
dataLogService.shutdown();
System.exit(0);
}
@@ -16,16 +16,20 @@
package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.entities.Prompt;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.jagrosh.jmusicbot.utils.YouTubeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
import com.typesafe.config.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import net.dv8tion.jda.core.OnlineStatus;
import net.dv8tion.jda.core.entities.Game;
import java.util.List;
import java.util.stream.Collectors;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
/**
*
@@ -40,13 +44,22 @@ public class BotConfig
private final static String END_TOKEN = "/// END OF JMUSICBOT CONFIG ///";
private Path path = null;
private String token, prefix, altprefix, helpWord, playlistsFolder,
successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji;
private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots;
private long owner, maxSeconds;
private String token, prefix, altprefix, helpWord, playlistsFolder, logLevel,
successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji,
chromePath, chromeDriverPath, evalEngine;
private YouTubeUtil.RoutingPlanner ytRoutingPlanner;
private List<IpBlock> ytIpBlocks;
private boolean youtubeOauth2, stayInChannel, songInGame, npImages, updatealerts, chromeHeadless, useEval, dbots;
private boolean statusMessages;
private boolean dataLogEnabled;
private String dataLogJdbcUrl, dataLogUser, dataLogPassword;
private long owner, maxSeconds, aloneTimeUntilStop;
private int maxYTPlaylistPages;
private double skipratio;
private OnlineStatus status;
private Game game;
private Activity game;
private Config aliases, transforms;
private boolean valid = false;
public BotConfig(Prompt prompt)
@@ -62,13 +75,7 @@ public class BotConfig
try
{
// get the path to the config, default config.txt
path = Paths.get(System.getProperty("config.file", System.getProperty("config", "config.txt")));
if(path.toFile().exists())
{
if(System.getProperty("config.file") == null)
System.setProperty("config.file", System.getProperty("config", "config.txt"));
ConfigFactory.invalidateCaches();
}
path = getConfigPath();
// load in the config file, plus the default values
//Config config = ConfigFactory.parseFile(path.toFile()).withFallback(ConfigFactory.load());
@@ -87,14 +94,32 @@ public class BotConfig
searchingEmoji = config.getString("searching");
game = OtherUtil.parseGame(config.getString("game"));
status = OtherUtil.parseStatus(config.getString("status"));
youtubeOauth2 = config.getBoolean("youtubeoauth2");
stayInChannel = config.getBoolean("stayinchannel");
songInGame = config.getBoolean("songinstatus");
statusMessages = config.getBoolean("statusmessages");
npImages = config.getBoolean("npimages");
updatealerts = config.getBoolean("updatealerts");
logLevel = config.getString("loglevel");
useEval = config.getBoolean("eval");
evalEngine = config.getString("evalengine");
maxSeconds = config.getLong("maxtime");
maxYTPlaylistPages = config.getInt("maxytplaylistpages");
aloneTimeUntilStop = config.getLong("alonetimeuntilstop");
playlistsFolder = config.getString("playlistsfolder");
aliases = config.getConfig("aliases");
chromePath = config.getString("chromepath");
chromeDriverPath = config.getString("chromedriverpath");
chromeHeadless = config.getBoolean("chromeheadless");
ytRoutingPlanner = config.getEnum(YouTubeUtil.RoutingPlanner.class, "ytroutingplanner");
ytIpBlocks = config.getStringList("ytipblocks").stream().map(YouTubeUtil::parseIpBlock).collect(Collectors.toList());
transforms = config.getConfig("transforms");
skipratio = config.getDouble("skipratio");
dbots = owner == 113156185389092864L;
dataLogEnabled = config.getBoolean("dblog.enabled");
dataLogJdbcUrl = config.getString("dblog.jdbc_url");
dataLogUser = config.getString("dblog.user");
dataLogPassword = config.getString("dblog.password");
// we may need to write a new config file
boolean write = false;
@@ -135,7 +160,7 @@ public class BotConfig
if(owner<=0)
{
prompt.alert(Prompt.Level.ERROR, CONTEXT, "Invalid User ID! Exiting.\n\nConfig Location: " + path.toAbsolutePath().toString());
System.exit(0);
return;
}
else
{
@@ -144,31 +169,7 @@ public class BotConfig
}
if(write)
{
String original = OtherUtil.loadResource(this, "/reference.conf");
byte[] bytes;
if(original==null)
{
bytes = ("token = "+token+"\r\nowner = "+owner).getBytes();
}
else
{
bytes = original.substring(original.indexOf(START_TOKEN)+START_TOKEN.length(), original.indexOf(END_TOKEN))
.replace("BOT_TOKEN_HERE", token)
.replace("0 // OWNER ID", Long.toString(owner))
.trim().getBytes();
}
try
{
Files.write(path, bytes);
}
catch(IOException ex)
{
prompt.alert(Prompt.Level.WARNING, CONTEXT, "Failed to write new config options to config.txt: "+ex
+ "\nPlease make sure that the files are not on your desktop or some other restricted area.\n\nConfig Location: "
+ path.toAbsolutePath().toString());
}
}
writeToFile();
// if we get through the whole config, it's good to go
valid = true;
@@ -179,6 +180,60 @@ public class BotConfig
}
}
private void writeToFile()
{
byte[] bytes = loadDefaultConfig().replace("BOT_TOKEN_HERE", token)
.replace("0 // OWNER ID", Long.toString(owner))
.trim().getBytes();
try
{
Files.write(path, bytes);
}
catch(IOException ex)
{
prompt.alert(Prompt.Level.WARNING, CONTEXT, "Failed to write new config options to config.txt: "+ex
+ "\nPlease make sure that the files are not on your desktop or some other restricted area.\n\nConfig Location: "
+ path.toAbsolutePath().toString());
}
}
private static String loadDefaultConfig()
{
String original = OtherUtil.loadResource(new JMusicBot(), "/reference.conf");
return original==null
? "token = BOT_TOKEN_HERE\r\nowner = 0 // OWNER ID"
: original.substring(original.indexOf(START_TOKEN)+START_TOKEN.length(), original.indexOf(END_TOKEN)).trim();
}
private static Path getConfigPath()
{
Path path = OtherUtil.getPath(System.getProperty("config.file", System.getProperty("config", "config.txt")));
if(path.toFile().exists())
{
if(System.getProperty("config.file") == null)
System.setProperty("config.file", System.getProperty("config", path.toAbsolutePath().toString()));
ConfigFactory.invalidateCaches();
}
return path;
}
public static void writeDefaultConfig()
{
Prompt prompt = new Prompt(null, null, true, true);
prompt.alert(Prompt.Level.INFO, "JMusicBot Config", "Generating default config file");
Path path = BotConfig.getConfigPath();
byte[] bytes = BotConfig.loadDefaultConfig().getBytes();
try
{
prompt.alert(Prompt.Level.INFO, "JMusicBot Config", "Writing default config file to " + path.toAbsolutePath().toString());
Files.write(path, bytes);
}
catch(Exception ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot Config", "An error occurred writing the default config file: " + ex.getMessage());
}
}
public boolean isValid()
{
return valid;
@@ -204,10 +259,35 @@ public class BotConfig
return token;
}
public double getSkipRatio()
{
return skipratio;
}
public long getOwnerId()
{
return owner;
}
public boolean getDataLogEnabled()
{
return dataLogEnabled;
}
public String getDataLogJdbcUrl()
{
return dataLogJdbcUrl;
}
public String getDataLogUser()
{
return dataLogUser;
}
public String getDataLogPassword()
{
return dataLogPassword;
}
public String getSuccess()
{
@@ -234,11 +314,16 @@ public class BotConfig
return searchingEmoji;
}
public Game getGame()
public Activity getGame()
{
return game;
}
public boolean isGameNone()
{
return game != null && game.getName().equalsIgnoreCase("none");
}
public OnlineStatus getStatus()
{
return status;
@@ -249,6 +334,11 @@ public class BotConfig
return helpWord;
}
public boolean useYoutubeOauth2()
{
return youtubeOauth2;
}
public boolean getStay()
{
return stayInChannel;
@@ -258,6 +348,11 @@ public class BotConfig
{
return songInGame;
}
public boolean useStatusMessages()
{
return statusMessages;
}
public String getPlaylistsFolder()
{
@@ -273,12 +368,47 @@ public class BotConfig
{
return updatealerts;
}
public String getLogLevel()
{
return logLevel;
}
public String getChromePath()
{
return chromePath.equals("AUTO") ? null : chromePath;
}
public String getChromeDriverPath()
{
return chromeDriverPath.equals("AUTO") ? null : chromeDriverPath;
}
public boolean getChromeHeadless()
{
return chromeHeadless;
}
public YouTubeUtil.RoutingPlanner getYTRoutingPlanner()
{
return ytRoutingPlanner;
}
public List<IpBlock> getYTIpBlocks()
{
return ytIpBlocks;
}
public boolean useEval()
{
return useEval;
}
public String getEvalEngine()
{
return evalEngine;
}
public boolean useNPImages()
{
return npImages;
@@ -289,9 +419,19 @@ public class BotConfig
return maxSeconds;
}
public int getMaxYTPlaylistPages()
{
return maxYTPlaylistPages;
}
public String getMaxTime()
{
return FormatUtil.formatTime(maxSeconds * 1000);
return TimeUtil.formatTime(maxSeconds * 1000);
}
public long getAloneTimeUntilStop()
{
return aloneTimeUntilStop;
}
public boolean isTooLong(AudioTrack track)
@@ -300,4 +440,21 @@ public class BotConfig
return false;
return Math.round(track.getDuration()/1000.0) > maxSeconds;
}
public String[] getAliases(String command)
{
try
{
return aliases.getStringList(command).toArray(new String[0]);
}
catch(NullPointerException | ConfigException.Missing e)
{
return new String[0];
}
}
public Config getTransforms()
{
return transforms;
}
}
@@ -18,7 +18,8 @@ package com.jagrosh.jmusicbot;
import com.jagrosh.jdautilities.command.CommandClient;
import com.jagrosh.jdautilities.command.CommandClientBuilder;
import com.jagrosh.jdautilities.commons.waiter.EventWaiter;
import com.jagrosh.jdautilities.examples.command.*;
import com.jagrosh.jmusicbot.commands.general.LoggedAboutCmd;
import com.jagrosh.jmusicbot.commands.general.LoggedPingCmd;
import com.jagrosh.jmusicbot.commands.admin.*;
import com.jagrosh.jmusicbot.commands.dj.*;
import com.jagrosh.jmusicbot.commands.general.*;
@@ -28,12 +29,22 @@ import com.jagrosh.jmusicbot.entities.Prompt;
import com.jagrosh.jmusicbot.gui.GUI;
import com.jagrosh.jmusicbot.settings.SettingsManager;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import moe.kyokobot.libdave.NativeDaveFactory;
import moe.kyokobot.libdave.jda.LDJDADaveSessionFactory;
import java.awt.Color;
import java.util.Arrays;
import javax.security.auth.login.LoginException;
import net.dv8tion.jda.core.*;
import net.dv8tion.jda.core.entities.Game;
import net.dv8tion.jda.api.*;
import net.dv8tion.jda.api.audio.AudioModuleConfig;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import static com.jagrosh.jmusicbot.utils.YouTubeUtil.GetPoTokenAndVisitorData;
/**
*
@@ -41,49 +52,151 @@ import org.slf4j.LoggerFactory;
*/
public class JMusicBot
{
public final static String PLAY_EMOJI = "\u25B6"; // ▶
public final static String PAUSE_EMOJI = "\u23F8"; // ⏸
public final static String STOP_EMOJI = "\u23F9"; // ⏹
public final static Permission[] RECOMMENDED_PERMS = new Permission[]{Permission.MESSAGE_READ, Permission.MESSAGE_WRITE, Permission.MESSAGE_HISTORY, Permission.MESSAGE_ADD_REACTION,
public final static Logger LOG = LoggerFactory.getLogger(JMusicBot.class);
public final static Permission[] RECOMMENDED_PERMS = {Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND, Permission.MESSAGE_HISTORY, Permission.MESSAGE_ADD_REACTION,
Permission.MESSAGE_EMBED_LINKS, Permission.MESSAGE_ATTACH_FILES, Permission.MESSAGE_MANAGE, Permission.MESSAGE_EXT_EMOJI,
Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT, Permission.VOICE_SPEAK, Permission.NICKNAME_CHANGE};
Permission.VOICE_CONNECT, Permission.VOICE_SPEAK, Permission.NICKNAME_CHANGE};
public final static GatewayIntent[] INTENTS = {GatewayIntent.DIRECT_MESSAGES, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_VOICE_STATES, GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_EXPRESSIONS, GatewayIntent.SCHEDULED_EVENTS};
/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
// startup log
Logger log = LoggerFactory.getLogger("Startup");
// create prompt to handle startup
Prompt prompt = new Prompt("JMusicBot", "Switching to nogui mode. You can manually start in nogui mode by including the -Dnogui=true flag.",
"true".equalsIgnoreCase(System.getProperty("nogui", "false")));
// check deprecated nogui mode (new way of setting it is -Dnogui=true)
for(String arg: args)
if("-nogui".equalsIgnoreCase(arg))
if(args.length > 0)
switch(args[0].toLowerCase())
{
prompt.alert(Prompt.Level.WARNING, "GUI", "The -nogui flag has been deprecated. "
+ "Please use the -Dnogui=true flag before the name of the jar. Example: java -jar -Dnogui=true JMusicBot.jar");
break;
case "generate-config":
BotConfig.writeDefaultConfig();
return;
default:
}
startBot();
}
private static void startBot()
{
// create prompt to handle startup
Prompt prompt = new Prompt("JMusicBot");
// get and check latest version
String version = OtherUtil.checkVersion(prompt);
// startup checks
OtherUtil.checkVersion(prompt);
OtherUtil.checkJavaVersion(prompt);
// load config
BotConfig config = new BotConfig(prompt);
config.load();
if(!config.isValid())
return;
LOG.info("Loaded config from " + config.getConfigLocation());
// set log level from config
((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(
Level.toLevel(config.getLogLevel(), Level.INFO));
// set up the listener
EventWaiter waiter = new EventWaiter();
SettingsManager settings = new SettingsManager();
Bot bot = new Bot(waiter, config, settings);
CommandClient client = createCommandClient(config, settings, bot);
AboutCommand aboutCommand = new AboutCommand(Color.BLUE.brighter(),
"a music bot that is [easy to host yourself!](https://github.com/jagrosh/MusicBot) (v"+version+")",
if(!prompt.isNoGUI())
{
try
{
GUI gui = new GUI(bot);
bot.setGUI(gui);
gui.init();
LOG.info("Loaded config from " + config.getConfigLocation());
}
catch(Exception e)
{
LOG.error("Could not start GUI. If you are "
+ "running on a server or in a location where you cannot display a "
+ "window, please run in nogui mode using the -Dnogui=true flag.");
}
}
// attempt to log in and start
try
{
JDA jda = JDABuilder.create(config.getToken(), Arrays.asList(INTENTS))
.enableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE)
.disableCache(CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS, CacheFlag.ONLINE_STATUS)
.setActivity(config.isGameNone() ? null : Activity.playing("loading..."))
.setStatus(config.getStatus()==OnlineStatus.INVISIBLE || config.getStatus()==OnlineStatus.OFFLINE
? OnlineStatus.INVISIBLE : OnlineStatus.DO_NOT_DISTURB)
.setAudioModuleConfig(createAudioModuleConfig())
.addEventListeners(client, waiter, new Listener(bot))
.setBulkDeleteSplittingEnabled(true)
.build();
bot.setJDA(jda);
// check if something about the current startup is not supported
String unsupportedReason = OtherUtil.getUnsupportedBotReason(jda);
if (unsupportedReason != null)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "JMusicBot cannot be run on this Discord bot: " + unsupportedReason);
try{ Thread.sleep(5000);}catch(InterruptedException ignored){} // this is awful but until we have a better way...
jda.shutdown();
System.exit(1);
}
// other check that will just be a warning now but may be required in the future
// check if the user has changed the prefix and provide info about the
// message content intent
if(!"@mention".equals(config.getPrefix()))
{
LOG.info("JMusicBot", "You currently have a custom prefix set. "
+ "If your prefix is not working, make sure that the 'MESSAGE CONTENT INTENT' is Enabled "
+ "on https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot");
}
}
catch(IllegalStateException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex.getMessage());
LOG.error("Failed to initialize DAVE protocol support", ex);
System.exit(1);
}
catch(IllegalArgumentException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "Some aspect of the configuration is "
+ "invalid: " + ex + "\nConfig Location: " + config.getConfigLocation());
System.exit(1);
}
catch(ErrorResponseException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex + "\nInvalid reponse returned when "
+ "attempting to connect, please make sure you're connected to the internet");
System.exit(1);
}
GetPoTokenAndVisitorData(config.getChromePath(), config.getChromeDriverPath(), config.getChromeHeadless());
}
private static AudioModuleConfig createAudioModuleConfig()
{
try
{
NativeDaveFactory.ensureAvailable();
LOG.info("Configured DAVE protocol support using libdave-jvm.");
return new AudioModuleConfig()
.withDaveSessionFactory(new LDJDADaveSessionFactory(new NativeDaveFactory()));
}
catch(Throwable ex)
{
throw new IllegalStateException("Unable to initialize DAVE protocol support via libdave-jvm. "
+ "Verify native dependencies are present for this platform (Linux glibc/musl x86_64, Windows x86_64, or macOS).", ex);
}
}
private static CommandClient createCommandClient(BotConfig config, SettingsManager settings, Bot bot)
{
// instantiate about command
LoggedAboutCmd aboutCommand = new LoggedAboutCmd(bot, Color.BLUE.brighter(),
"a music bot that is [easy to host yourself!](https://github.com/chrisb09/MusicBot) (v" + OtherUtil.getCurrentVersion() + ")",
new String[]{"High-quality music playback", "FairQueue™ Technology", "Easy to host yourself"},
RECOMMENDED_PERMS);
aboutCommand.setIsAuthor(false);
@@ -99,100 +212,64 @@ public class JMusicBot
.setLinkedCacheSize(200)
.setGuildSettingsManager(settings)
.addCommands(aboutCommand,
new PingCommand(),
new SettingsCmd(),
new LoggedPingCmd(bot),
new SettingsCmd(bot),
new LyricsCmd(bot),
new NowplayingCmd(bot),
new PlayCmd(bot, config.getLoading()),
new PlayCmd(bot),
new PlaylistsCmd(bot),
new QueueCmd(bot),
new RemoveCmd(bot),
new SearchCmd(bot, config.getSearching()),
new SCSearchCmd(bot, config.getSearching()),
new SearchCmd(bot),
new SCSearchCmd(bot),
new SeekCmd(bot),
new ShuffleCmd(bot),
new SkipCmd(bot),
new ForceRemoveCmd(bot),
new ForceskipCmd(bot),
new MoveTrackCmd(bot),
new PauseCmd(bot),
new PlaynextCmd(bot, config.getLoading()),
new PlaynextCmd(bot),
new RepeatCmd(bot),
new SkiptoCmd(bot),
new StopCmd(bot),
new VolumeCmd(bot),
new SetdjCmd(),
new SettcCmd(),
new SetvcCmd(),
new PrefixCmd(bot),
new QueueTypeCmd(bot),
new SetdjCmd(bot),
new SkipratioCmd(bot),
new SettcCmd(bot),
new SetvcCmd(bot),
new AutoplaylistCmd(bot),
new DebugCmd(bot),
new PlaylistCmd(bot),
new SetavatarCmd(),
new SetgameCmd(),
new SetnameCmd(),
new SetstatusCmd(),
new SetavatarCmd(bot),
new SetgameCmd(bot),
new SetnameCmd(bot),
new SetstatusCmd(bot),
new ShutdownCmd(bot)
);
// enable eval if applicable
if(config.useEval())
cb.addCommand(new EvalCmd(bot));
boolean nogame = false;
if(config.getStatus()!=OnlineStatus.UNKNOWN)
// set status if set in config
if(config.getStatus() != OnlineStatus.UNKNOWN)
cb.setStatus(config.getStatus());
if(config.getGame()==null)
// set game
if(config.getGame() == null)
cb.useDefaultGame();
else if(config.getGame().getName().equalsIgnoreCase("none"))
{
cb.setGame(null);
nogame = true;
}
else if(config.isGameNone())
cb.setActivity(null);
else
cb.setGame(config.getGame());
CommandClient client = cb.build();
cb.setActivity(config.getGame());
if(!prompt.isNoGUI())
{
try
{
GUI gui = new GUI(bot);
bot.setGUI(gui);
gui.init();
}
catch(Exception e)
{
log.error("Could not start GUI. If you are "
+ "running on a server or in a location where you cannot display a "
+ "window, please run in nogui mode using the -Dnogui=true flag.");
}
}
log.info("Loaded config from "+config.getConfigLocation());
// attempt to log in and start
try
{
JDA jda = new JDABuilder(AccountType.BOT)
.setToken(config.getToken())
.setAudioEnabled(true)
.setGame(nogame ? null : Game.playing("loading..."))
.setStatus(config.getStatus()==OnlineStatus.INVISIBLE||config.getStatus()==OnlineStatus.OFFLINE ? OnlineStatus.INVISIBLE : OnlineStatus.DO_NOT_DISTURB)
.addEventListener(client, waiter, new Listener(bot))
.setBulkDeleteSplittingEnabled(true)
.build();
bot.setJDA(jda);
}
catch (LoginException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex + "\nPlease make sure you are "
+ "editing the correct config.txt file, and that you have used the "
+ "correct token (not the 'secret'!)\nConfig Location: " + config.getConfigLocation());
System.exit(1);
}
catch(IllegalArgumentException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "Some aspect of the configuration is "
+ "invalid: " + ex + "\nConfig Location: " + config.getConfigLocation());
System.exit(1);
}
return cb.build();
}
}
@@ -17,15 +17,20 @@ package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import java.util.concurrent.TimeUnit;
import net.dv8tion.jda.core.JDA;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.core.entities.User;
import net.dv8tion.jda.core.entities.VoiceChannel;
import net.dv8tion.jda.core.events.ReadyEvent;
import net.dv8tion.jda.core.events.ShutdownEvent;
import net.dv8tion.jda.core.events.guild.GuildJoinEvent;
import net.dv8tion.jda.core.events.message.guild.GuildMessageDeleteEvent;
import net.dv8tion.jda.core.hooks.ListenerAdapter;
import com.jagrosh.jmusicbot.utils.YoutubeOauth2TokenHandler;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.events.session.ShutdownEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,11 +50,11 @@ public class Listener extends ListenerAdapter
@Override
public void onReady(ReadyEvent event)
{
if(event.getJDA().getGuilds().isEmpty())
if(event.getJDA().getGuildCache().isEmpty())
{
Logger log = LoggerFactory.getLogger("MusicBot");
log.warn("This bot is not on any guilds! Use the following link to add the bot to your guilds!");
log.warn(event.getJDA().asBot().getInviteUrl(JMusicBot.RECOMMENDED_PERMS));
log.warn(event.getJDA().getInviteUrl(JMusicBot.RECOMMENDED_PERMS));
}
credit(event.getJDA());
event.getJDA().getGuilds().forEach((guild) ->
@@ -69,9 +74,9 @@ public class Listener extends ListenerAdapter
{
bot.getThreadpool().scheduleWithFixedDelay(() ->
{
User owner = bot.getJDA().getUserById(bot.getConfig().getOwnerId());
if(owner!=null)
try
{
User owner = bot.getJDA().retrieveUserById(bot.getConfig().getOwnerId()).complete();
String currentVersion = OtherUtil.getCurrentVersion();
String latestVersion = OtherUtil.getLatestVersion();
if(latestVersion!=null && !currentVersion.equalsIgnoreCase(latestVersion))
@@ -80,16 +85,50 @@ public class Listener extends ListenerAdapter
owner.openPrivateChannel().queue(pc -> pc.sendMessage(msg).queue());
}
}
catch(Exception ignored) {} // ignored
}, 0, 24, TimeUnit.HOURS);
}
if (bot.getConfig().useYoutubeOauth2())
{
YoutubeOauth2TokenHandler.Data data = bot.getYouTubeOauth2Handler().getData();
if (data != null)
{
PrivateChannel channel = bot.getJDA().openPrivateChannelById(bot.getConfig().getOwnerId()).complete();
channel
.sendMessage(
"# DO NOT AUTHORISE THIS WITH YOUR MAIN GOOGLE ACCOUNT!!!\n"
+ "## Create or use an alternative/burner Google account!\n"
+ "To give JMusicBot access to your Google account, go to "
+ data.getAuthorisationUrl()
+ " and enter the code **" + data.getCode() + "**")
.queue();
}
}
}
@Override
public void onGuildMessageDelete(GuildMessageDeleteEvent event)
public void onMessageDelete(MessageDeleteEvent event)
{
bot.getNowplayingHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong());
if(event.isFromGuild()) {
bot.getNowplayingHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong());
bot.getStatusMessageHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong());
}
}
@Override
public void onMessageReceived(MessageReceivedEvent event)
{
bot.getStatusMessageHandler().onMessageReceived(event);
}
@Override
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event)
{
bot.getAloneInVoiceHandler().onVoiceUpdate(event);
if(bot.getDataLogService() != null)
bot.getDataLogService().onVoiceUpdate(event);
}
@Override
public void onShutdown(ShutdownEvent event)
{
@@ -0,0 +1,98 @@
/*
* Copyright 2021 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.Bot;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
*
* @author Michaili K (mysteriouscursor+git@protonmail.com)
*/
public class AloneInVoiceHandler
{
private final Bot bot;
private final HashMap<Long, Instant> aloneSince = new HashMap<>();
private long aloneTimeUntilStop = 0;
public AloneInVoiceHandler(Bot bot)
{
this.bot = bot;
}
public void init()
{
aloneTimeUntilStop = bot.getConfig().getAloneTimeUntilStop();
if(aloneTimeUntilStop > 0)
bot.getThreadpool().scheduleWithFixedDelay(() -> check(), 0, 5, TimeUnit.SECONDS);
}
private void check()
{
Set<Long> toRemove = new HashSet<>();
for(Map.Entry<Long, Instant> entrySet: aloneSince.entrySet())
{
if(entrySet.getValue().getEpochSecond() > Instant.now().getEpochSecond() - aloneTimeUntilStop) continue;
Guild guild = bot.getJDA().getGuildById(entrySet.getKey());
if(guild == null)
{
toRemove.add(entrySet.getKey());
continue;
}
((AudioHandler) guild.getAudioManager().getSendingHandler()).stopAndClear();
guild.getAudioManager().closeAudioConnection();
toRemove.add(entrySet.getKey());
}
toRemove.forEach(id -> aloneSince.remove(id));
}
public void onVoiceUpdate(GuildVoiceUpdateEvent event)
{
if(aloneTimeUntilStop <= 0) return;
Guild guild = event.getEntity().getGuild();
if(!bot.getPlayerManager().hasHandler(guild)) return;
boolean alone = isAlone(guild);
boolean inList = aloneSince.containsKey(guild.getIdLong());
if(!alone && inList)
aloneSince.remove(guild.getIdLong());
else if(alone && !inList)
aloneSince.put(guild.getIdLong(), Instant.now());
}
private boolean isAlone(Guild guild)
{
if(guild.getAudioManager().getConnectedChannel() == null) return false;
return guild.getAudioManager().getConnectedChannel().getMembers().stream()
.noneMatch(x ->
!x.getVoiceState().isDeafened()
&& !x.getUser().isBot());
}
}
@@ -15,10 +15,14 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.JMusicBot;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import com.jagrosh.jmusicbot.queue.AbstractQueue;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
@@ -26,17 +30,19 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.jagrosh.jmusicbot.queue.FairQueue;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack;
import net.dv8tion.jda.core.EmbedBuilder;
import net.dv8tion.jda.core.JDA;
import net.dv8tion.jda.core.MessageBuilder;
import net.dv8tion.jda.core.audio.AudioSendHandler;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.core.entities.User;
import java.nio.ByteBuffer;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
@@ -44,7 +50,12 @@ import net.dv8tion.jda.core.entities.User;
*/
public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
{
private final FairQueue<QueuedTrack> queue = new FairQueue<>();
public final static String PLAY_EMOJI = "\u25B6"; // ▶
public final static String PAUSE_EMOJI = "\u23F8"; // ⏸
public final static String STOP_EMOJI = "\u23F9"; // ⏹
private final static Logger LOGGER = LoggerFactory.getLogger(AudioHandler.class);
private final List<AudioTrack> defaultQueue = new LinkedList<>();
private final Set<String> votes = new HashSet<>();
@@ -53,12 +64,20 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
private final long guildId;
private AudioFrame lastFrame;
private AbstractQueue<QueuedTrack> queue;
protected AudioHandler(PlayerManager manager, Guild guild, AudioPlayer player)
{
this.manager = manager;
this.audioPlayer = player;
this.guildId = guild.getIdLong();
this.setQueueType(manager.getBot().getSettingsManager().getSettings(guildId).getQueueType());
}
public void setQueueType(QueueType type)
{
queue = type.createInstance(queue);
}
public int addTrackToFront(QueuedTrack qtrack)
@@ -86,7 +105,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
return queue.add(qtrack);
}
public FairQueue<QueuedTrack> getQueue()
public AbstractQueue<QueuedTrack> getQueue()
{
return queue;
}
@@ -101,7 +120,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
public boolean isMusicPlaying(JDA jda)
{
return guild(jda).getSelfMember().getVoiceState().inVoiceChannel() && audioPlayer.getPlayingTrack()!=null;
return guild(jda).getSelfMember().getVoiceState().inAudioChannel() && audioPlayer.getPlayingTrack()!=null;
}
public Set<String> getVotes()
@@ -114,11 +133,12 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
return audioPlayer;
}
public long getRequester()
public RequestMetadata getRequestMetadata()
{
if(audioPlayer.getPlayingTrack()==null || audioPlayer.getPlayingTrack().getUserData(Long.class)==null)
return 0;
return audioPlayer.getPlayingTrack().getUserData(Long.class);
if(audioPlayer.getPlayingTrack() == null)
return RequestMetadata.EMPTY;
RequestMetadata rm = audioPlayer.getPlayingTrack().getUserData(RequestMetadata.class);
return rm == null ? RequestMetadata.EMPTY : rm;
}
public boolean playFromDefault()
@@ -137,10 +157,13 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
return false;
pl.loadTracks(manager, (at) ->
{
at.setUserData(RequestMetadata.fromDefaultPlaylist(settings.getDefaultPlaylist()));
if(audioPlayer.getPlayingTrack()==null)
audioPlayer.playTrack(at);
else
defaultQueue.add(at);
if(manager.getBot().getDataLogService() != null)
manager.getBot().getDataLogService().logQueueAdd(guild(manager.getBot().getJDA()), null, at, "DEFAULT_PLAYLIST", null, null, settings.getDefaultPlaylist());
}, () ->
{
if(pl.getTracks().isEmpty() && !manager.getBot().getConfig().getStay())
@@ -153,19 +176,30 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
@Override
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason)
{
if(manager.getBot().getDataLogService() != null)
manager.getBot().getDataLogService().logPlayEnd(guild(manager.getBot().getJDA()), endReason.name());
RepeatMode repeatMode = manager.getBot().getSettingsManager().getSettings(guildId).getRepeatMode();
// if the track ended normally, and we're in repeat mode, re-add it to the queue
if(endReason==AudioTrackEndReason.FINISHED && manager.getBot().getSettingsManager().getSettings(guildId).getRepeatMode())
if(endReason==AudioTrackEndReason.FINISHED && repeatMode != RepeatMode.OFF)
{
queue.add(new QueuedTrack(track.makeClone(), track.getUserData(Long.class)==null ? 0L : track.getUserData(Long.class)));
QueuedTrack clone = new QueuedTrack(track.makeClone(), track.getUserData(RequestMetadata.class));
if(repeatMode == RepeatMode.ALL)
queue.add(clone);
else
queue.addAt(0, clone);
}
if(queue.isEmpty())
{
if(!playFromDefault())
{
manager.getBot().getNowplayingHandler().onTrackUpdate(guildId, null, this);
manager.getBot().getNowplayingHandler().onTrackUpdate(null);
manager.getBot().getStatusMessageHandler().onTrackUpdate(guildId);
if(!manager.getBot().getConfig().getStay())
manager.getBot().closeAudioConnection(guildId);
// unpause, in the case when the player was paused and the track has been skipped.
// this is to prevent the player being paused next time it's being used.
player.setPaused(false);
}
}
else
@@ -175,32 +209,59 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
}
}
@Override
public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception)
{
if (
exception.getMessage().equals("Sign in to confirm you're not a bot")
|| exception.getMessage().equals("Please sign in")
)
LOGGER.error(
"Track {} has failed to play: {}"
+ "You will need to sign in to Google to play YouTube tracks. More info: https://jmusicbot.com/youtube-oauth2",
track.getIdentifier(),
exception.getMessage()
);
else
LOGGER.error("Track {} has failed to play", track.getIdentifier(), exception);
}
@Override
public void onTrackStart(AudioPlayer player, AudioTrack track)
{
votes.clear();
manager.getBot().getNowplayingHandler().onTrackUpdate(guildId, track, this);
manager.getBot().getNowplayingHandler().onTrackUpdate(track);
manager.getBot().getStatusMessageHandler().onTrackUpdate(guildId);
if(manager.getBot().getDataLogService() != null)
{
RequestMetadata rm = getRequestMetadata();
String source = rm.queueInfo == null ? null : rm.queueInfo.source;
String searchQuery = rm.queueInfo == null ? null : rm.queueInfo.searchQuery;
String playlistName = rm.queueInfo == null ? null : rm.queueInfo.playlistName;
manager.getBot().getDataLogService().logPlayStart(guild(manager.getBot().getJDA()), track, rm, source, searchQuery, playlistName);
}
}
// Formatting
public Message getNowPlaying(JDA jda)
public MessageCreateData getNowPlaying(JDA jda)
{
if(isMusicPlaying(jda))
{
Guild guild = guild(jda);
AudioTrack track = audioPlayer.getPlayingTrack();
MessageBuilder mb = new MessageBuilder();
mb.append(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing in "+guild.getSelfMember().getVoiceState().getChannel().getName()+"...**"));
MessageCreateBuilder mb = new MessageCreateBuilder();
mb.addContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing in "+guild.getSelfMember().getVoiceState().getChannel().getAsMention()+"...**"));
EmbedBuilder eb = new EmbedBuilder();
eb.setColor(guild.getSelfMember().getColor());
if(getRequester() != 0)
RequestMetadata rm = getRequestMetadata();
if(rm.getOwner() != 0L)
{
User u = guild.getJDA().getUserById(getRequester());
User u = guild.getJDA().getUserById(rm.user.id);
if(u==null)
eb.setAuthor("Unknown (ID:"+getRequester()+")", null, null);
eb.setAuthor(FormatUtil.formatUsername(rm.user), null, rm.user.avatar);
else
eb.setAuthor(u.getName()+"#"+u.getDiscriminator(), null, u.getEffectiveAvatarUrl());
eb.setAuthor(FormatUtil.formatUsername(u), null, u.getEffectiveAvatarUrl());
}
try
@@ -221,47 +282,113 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
eb.setFooter("Source: " + track.getInfo().author, null);
double progress = (double)audioPlayer.getPlayingTrack().getPosition()/track.getDuration();
eb.setDescription((audioPlayer.isPaused() ? JMusicBot.PAUSE_EMOJI : JMusicBot.PLAY_EMOJI)
eb.setDescription(getStatusEmoji()
+ " "+FormatUtil.progressBar(progress)
+ " `[" + FormatUtil.formatTime(track.getPosition()) + "/" + FormatUtil.formatTime(track.getDuration()) + "]` "
+ " `[" + TimeUtil.formatTime(track.getPosition()) + "/" + TimeUtil.formatTime(track.getDuration()) + "]` "
+ FormatUtil.volumeIcon(audioPlayer.getVolume()));
return mb.setEmbed(eb.build()).build();
return mb.setEmbeds(eb.build()).build();
}
else return null;
}
public Message getNoMusicPlaying(JDA jda)
public MessageCreateData getNoMusicPlaying(JDA jda)
{
Guild guild = guild(jda);
return new MessageBuilder()
return new MessageCreateBuilder()
.setContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing...**"))
.setEmbed(new EmbedBuilder()
.setEmbeds(new EmbedBuilder()
.setTitle("No music playing")
.setDescription(JMusicBot.STOP_EMOJI+" "+FormatUtil.progressBar(-1)+" "+FormatUtil.volumeIcon(audioPlayer.getVolume()))
.setDescription(STOP_EMOJI+" "+FormatUtil.progressBar(-1)+" "+FormatUtil.volumeIcon(audioPlayer.getVolume()))
.setColor(guild.getSelfMember().getColor())
.build()).build();
}
public String getTopicFormat(JDA jda)
public MessageCreateData getStatusMessage(JDA jda)
{
if(isMusicPlaying(jda))
Guild guild = guild(jda);
if(guild == null)
return null;
MessageCreateBuilder mb = new MessageCreateBuilder();
mb.setContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing...**"));
EmbedBuilder eb = new EmbedBuilder();
eb.setColor(guild.getSelfMember().getColor());
AudioTrack track = audioPlayer.getPlayingTrack();
if(isMusicPlaying(jda) && track != null)
{
long userid = getRequester();
AudioTrack track = audioPlayer.getPlayingTrack();
String title = track.getInfo().title;
if(title==null || title.equals("Unknown Title"))
title = track.getInfo().uri;
return "**"+title+"** ["+(userid==0 ? "autoplay" : "<@"+userid+">")+"]"
+ "\n" + (audioPlayer.isPaused() ? JMusicBot.PAUSE_EMOJI : JMusicBot.PLAY_EMOJI) + " "
+ "[" + FormatUtil.formatTime(track.getDuration()) + "] "
+ FormatUtil.volumeIcon(audioPlayer.getVolume());
try
{
eb.setTitle(track.getInfo().title, track.getInfo().uri);
}
catch(Exception e)
{
eb.setTitle(track.getInfo().title);
}
}
else return "No music playing " + JMusicBot.STOP_EMOJI + " " + FormatUtil.volumeIcon(audioPlayer.getVolume());
else
{
eb.setTitle("No music playing");
}
StringBuilder desc = new StringBuilder();
if(isMusicPlaying(jda) && track != null)
{
double progress = (double)track.getPosition()/track.getDuration();
desc.append(getStatusEmoji())
.append(" ").append(FormatUtil.progressBar(progress))
.append(" `[").append(TimeUtil.formatTime(track.getPosition()))
.append("/").append(TimeUtil.formatTime(track.getDuration())).append("]` ")
.append(FormatUtil.volumeIcon(audioPlayer.getVolume()));
}
else
{
desc.append(STOP_EMOJI)
.append(" ").append(FormatUtil.progressBar(-1))
.append(" ").append(FormatUtil.volumeIcon(audioPlayer.getVolume()));
}
List<QueuedTrack> list = queue.getList();
if(!list.isEmpty())
{
QueuedTrack next = list.get(0);
String nextTitle = next.getTrack().getInfo().title;
String requester = next.getRequestMetadata() == null || next.getRequestMetadata().user == null
? null
: FormatUtil.formatUsername(next.getRequestMetadata().user);
desc.append("\n").append("Next: **").append(nextTitle).append("**");
if(requester != null && !requester.isEmpty())
desc.append(" (by ").append(requester).append(")");
}
long total = 0L;
for(QueuedTrack qt : list)
total += qt.getTrack().getDuration();
Settings settings = manager.getBot().getSettingsManager().getSettings(guildId);
desc.append("\n")
.append(manager.getBot().getConfig().getSuccess())
.append(" Queue | ").append(list.size()).append(" entries | `")
.append(TimeUtil.formatTime(total)).append("` | ")
.append(settings.getQueueType().getEmoji()).append(" `")
.append(settings.getQueueType().getUserFriendlyName()).append("`");
if(settings.getRepeatMode().getEmoji() != null)
desc.append(" | ").append(settings.getRepeatMode().getEmoji());
if(!guild.getSelfMember().getVoiceState().inAudioChannel())
desc.append("\n").append(manager.getBot().getConfig().getWarning()).append(" Disconnected");
eb.setDescription(FormatUtil.filter(desc.toString()));
return mb.setEmbeds(eb.build()).build();
}
public String getStatusEmoji()
{
return audioPlayer.isPaused() ? PAUSE_EMOJI : PLAY_EMOJI;
}
// Audio Send Handler methods
@Override
/*@Override
public boolean canProvide()
{
if (lastFrame == null)
@@ -280,6 +407,19 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
lastFrame = null;
return data;
}*/
@Override
public boolean canProvide()
{
lastFrame = audioPlayer.provide();
return lastFrame != null;
}
@Override
public ByteBuffer provide20MsAudio()
{
return ByteBuffer.wrap(lastFrame.getData());
}
@Override
@@ -23,12 +23,15 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Game;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.core.exceptions.PermissionException;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.exceptions.PermissionException;
import net.dv8tion.jda.api.exceptions.RateLimitedException;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.utils.messages.MessageEditData;
/**
*
@@ -47,12 +50,13 @@ public class NowplayingHandler
public void init()
{
bot.getThreadpool().scheduleWithFixedDelay(() -> updateAll(), 0, 5, TimeUnit.SECONDS);
if(!bot.getConfig().useNPImages())
bot.getThreadpool().scheduleWithFixedDelay(() -> updateAll(), 0, 5, TimeUnit.SECONDS);
}
public void setLastNPMessage(Message m)
{
lastNP.put(m.getGuild().getIdLong(), new Pair<>(m.getTextChannel().getIdLong(), m.getIdLong()));
lastNP.put(m.getGuild().getIdLong(), new Pair<>(m.getChannel().getIdLong(), m.getIdLong()));
}
public void clearLastNPMessage(Guild guild)
@@ -79,7 +83,7 @@ public class NowplayingHandler
continue;
}
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
Message msg = handler.getNowPlaying(bot.getJDA());
MessageCreateData msg = handler.getNowPlaying(bot.getJDA());
if(msg==null)
{
msg = handler.getNoMusicPlaying(bot.getJDA());
@@ -87,7 +91,7 @@ public class NowplayingHandler
}
try
{
tc.editMessageById(pair.getValue(), msg).queue(m->{}, t -> lastNP.remove(guildId));
tc.editMessageById(pair.getValue(), MessageEditData.fromCreateData(msg)).queue(m->{}, t -> lastNP.remove(guildId));
}
catch(Exception e)
{
@@ -96,52 +100,18 @@ public class NowplayingHandler
}
toRemove.forEach(id -> lastNP.remove(id));
}
public void updateTopic(long guildId, AudioHandler handler, boolean wait)
{
Guild guild = bot.getJDA().getGuildById(guildId);
if(guild==null)
return;
Settings settings = bot.getSettingsManager().getSettings(guildId);
TextChannel tchan = settings.getTextChannel(guild);
if(tchan!=null && guild.getSelfMember().hasPermission(tchan, Permission.MANAGE_CHANNEL))
{
String otherText;
if(tchan.getTopic()==null || tchan.getTopic().isEmpty())
otherText = "\u200B";
else if(tchan.getTopic().contains("\u200B"))
otherText = tchan.getTopic().substring(tchan.getTopic().lastIndexOf("\u200B"));
else
otherText = "\u200B\n "+tchan.getTopic();
String text = handler.getTopicFormat(bot.getJDA()) + otherText;
if(!text.equals(tchan.getTopic()))
{
try
{
if(wait)
tchan.getManager().setTopic(text).complete();
else
tchan.getManager().setTopic(text).queue();
}
catch(PermissionException ignore) {}
}
}
}
// "event"-based methods
public void onTrackUpdate(long guildId, AudioTrack track, AudioHandler handler)
public void onTrackUpdate(AudioTrack track)
{
// update bot status if applicable
if(bot.getConfig().getSongInStatus())
{
if(track!=null && bot.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inVoiceChannel()).count()<=1)
bot.getJDA().getPresence().setGame(Game.listening(track.getInfo().title));
if(track!=null && bot.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inAudioChannel()).count()<=1)
bot.getJDA().getPresence().setActivity(Activity.listening(track.getInfo().title));
else
bot.resetGame();
}
// update channel topic if applicable
updateTopic(guildId, handler, false);
}
public void onMessageDelete(Guild guild, long messageId)
@@ -15,12 +15,38 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.dunctebot.sourcemanagers.DuncteBotSources;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.BotConfig;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.jagrosh.jmusicbot.utils.YouTubeUtil;
import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import net.dv8tion.jda.core.entities.Guild;
import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.getyarn.GetyarnAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.nico.NicoAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager;
import com.sedmelluq.lava.extensions.youtuberotator.YoutubeIpRotatorSetup;
import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner;
import com.sedmelluq.lava.extensions.youtuberotator.planner.BalancingIpRoutePlanner;
import com.sedmelluq.lava.extensions.youtuberotator.planner.NanoIpRoutePlanner;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import dev.lavalink.youtube.clients.Web;
import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
/**
*
@@ -28,20 +54,83 @@ import net.dv8tion.jda.core.entities.Guild;
*/
public class PlayerManager extends DefaultAudioPlayerManager
{
private final static Logger LOGGER = LoggerFactory.getLogger(PlayerManager.class);
private final Bot bot;
public PlayerManager(Bot bot)
private final BotConfig config;
public PlayerManager(Bot bot, BotConfig config)
{
this.bot = bot;
this.config = config;
}
public void init()
{
AudioSourceManagers.registerRemoteSources(this);
TransformativeAudioSourceManager.createTransforms(bot.getConfig().getTransforms()).forEach(t -> registerSourceManager(t));
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
if (config.getYTRoutingPlanner() != YouTubeUtil.RoutingPlanner.NONE)
{
AbstractRoutePlanner routePlanner = YouTubeUtil.createRouterPlanner(config.getYTRoutingPlanner(), config.getYTIpBlocks());
YoutubeIpRotatorSetup rotator = new YoutubeIpRotatorSetup(routePlanner);
rotator.forConfiguration(yt.getHttpInterfaceManager(), false)
.withMainDelegateFilter(yt.getContextFilter())
.setup();
}
yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages());
registerSourceManager(yt);
registerSourceManager(SoundCloudAudioSourceManager.createDefault());
registerSourceManager(new BandcampAudioSourceManager());
registerSourceManager(new VimeoAudioSourceManager());
registerSourceManager(new TwitchStreamAudioSourceManager());
registerSourceManager(new BeamAudioSourceManager());
registerSourceManager(new GetyarnAudioSourceManager());
registerSourceManager(new NicoAudioSourceManager());
registerSourceManager(new HttpAudioSourceManager(MediaContainerRegistry.DEFAULT_REGISTRY));
AudioSourceManagers.registerLocalSource(this);
source(YoutubeAudioSourceManager.class).setPlaylistPageCount(10);
DuncteBotSources.registerAll(this, "en-US");
}
private YoutubeAudioSourceManager setupYoutubeAudioSourceManager()
{
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages());
// OAuth2 setup
if (bot.getConfig().useYoutubeOauth2())
{
String token = null;
try
{
token = Files.readString(OtherUtil.getPath("youtubetoken.txt"));
}
catch (NoSuchFileException e)
{
/* ignored */
}
catch (IOException e)
{
LOGGER.warn("Failed to read YouTube OAuth2 token file: {}", e.getMessage());
return yt;
}
LOGGER.debug("Read YouTube OAuth2 refresh token from youtubetoken.txt");
try
{
yt.useOauth2(token, false);
}
catch (Exception e)
{
LOGGER.warn("Failed to authorise with YouTube. If this issue persists, delete the youtubetoken.txt file to reauthorise.", e);
}
}
return yt;
}
public Bot getBot()
{
return bot;
@@ -1,5 +1,5 @@
/*
* Copyright 2016 John Grosh <john.a.grosh@gmail.com>.
* Copyright 2021 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,10 +15,11 @@
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import com.jagrosh.jmusicbot.queue.Queueable;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.core.entities.User;
import net.dv8tion.jda.api.entities.User;
/**
*
@@ -27,22 +28,22 @@ import net.dv8tion.jda.core.entities.User;
public class QueuedTrack implements Queueable
{
private final AudioTrack track;
public QueuedTrack(AudioTrack track, User owner)
{
this(track, owner.getIdLong());
}
public QueuedTrack(AudioTrack track, long owner)
private final RequestMetadata requestMetadata;
public QueuedTrack(AudioTrack track, RequestMetadata rm)
{
this.track = track;
this.track.setUserData(owner);
this.track.setUserData(rm == null ? RequestMetadata.EMPTY : rm);
this.requestMetadata = rm;
if (this.track.isSeekable() && rm != null)
track.setPosition(rm.requestInfo.startTimestamp);
}
@Override
public long getIdentifier()
{
return track.getUserData(Long.class);
return requestMetadata.getOwner();
}
public AudioTrack getTrack()
@@ -50,9 +51,17 @@ public class QueuedTrack implements Queueable
return track;
}
public RequestMetadata getRequestMetadata()
{
return requestMetadata;
}
@Override
public String toString()
{
return "`[" + FormatUtil.formatTime(track.getDuration()) + "]` **" + track.getInfo().title + "** - <@" + track.getUserData(Long.class) + ">";
String entry = "`[" + TimeUtil.formatTime(track.getDuration()) + "]` ";
AudioTrackInfo trackInfo = track.getInfo();
entry = entry + (trackInfo.uri.startsWith("http") ? "[**" + trackInfo.title + "**]("+trackInfo.uri+")" : "**" + trackInfo.title + "**");
return entry + " - <@" + track.getUserData(RequestMetadata.class).getOwner() + ">";
}
}
@@ -0,0 +1,124 @@
/*
* Copyright 2021 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.entities.User;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class RequestMetadata
{
public static final RequestMetadata EMPTY = new RequestMetadata(null, null, null);
public final UserInfo user;
public final RequestInfo requestInfo;
public final QueueInfo queueInfo;
public RequestMetadata(User user, RequestInfo requestInfo, QueueInfo queueInfo)
{
this.user = user == null ? null : new UserInfo(user.getIdLong(), user.getName(), user.getDiscriminator(), user.getEffectiveAvatarUrl());
this.requestInfo = requestInfo;
this.queueInfo = queueInfo;
}
public long getOwner()
{
return user == null ? 0L : user.id;
}
public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event)
{
return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri),
new QueueInfo("PLAY", null, event.getArgs(), System.currentTimeMillis()));
}
public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event, String source, String playlistName, String searchQuery)
{
return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri),
new QueueInfo(source, playlistName, searchQuery, System.currentTimeMillis()));
}
public static RequestMetadata fromDefaultPlaylist(String playlistName)
{
return new RequestMetadata(null, new RequestInfo(playlistName, null),
new QueueInfo("DEFAULT_PLAYLIST", playlistName, null, System.currentTimeMillis()));
}
public static class RequestInfo
{
public final String query, url;
public final long startTimestamp;
public RequestInfo(String query, String url)
{
this(query, url, tryGetTimestamp(query));
}
private RequestInfo(String query, String url, long startTimestamp)
{
this.url = url;
this.query = query;
this.startTimestamp = startTimestamp;
}
private static final Pattern youtubeTimestampPattern = Pattern.compile("youtu(?:\\.be|be\\..+)/.*\\?.*(?!.*list=)t=([\\dhms]+)");
private static long tryGetTimestamp(String url)
{
if(url == null || url.isEmpty())
return 0;
Matcher matcher = youtubeTimestampPattern.matcher(url);
return matcher.find() ? TimeUtil.parseUnitTime(matcher.group(1)) : 0;
}
}
public static class QueueInfo
{
public final String source;
public final String playlistName;
public final String searchQuery;
public final long queuedAt;
public QueueInfo(String source, String playlistName, String searchQuery, long queuedAt)
{
this.source = source;
this.playlistName = playlistName;
this.searchQuery = searchQuery;
this.queuedAt = queuedAt;
}
}
public static class UserInfo
{
public final long id;
public final String username, discrim, avatar;
private UserInfo(long id, String username, String discrim, String avatar)
{
this.id = id;
this.username = username;
this.discrim = discrim;
this.avatar = avatar;
}
}
}
@@ -0,0 +1,309 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.audio;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.settings.Settings;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
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.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.requests.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.utils.messages.MessageEditData;
public class StatusMessageHandler
{
private static final Logger LOG = LoggerFactory.getLogger("StatusMessage");
private final Bot bot;
private final Map<Long, StatusState> states;
public StatusMessageHandler(Bot bot)
{
this.bot = bot;
this.states = new ConcurrentHashMap<>();
}
public void init()
{
if(!bot.getConfig().useStatusMessages())
return;
bot.getThreadpool().scheduleWithFixedDelay(() -> updateAll(), 0, 1, TimeUnit.SECONDS);
}
public void onMessageReceived(MessageReceivedEvent event)
{
if(!bot.getConfig().useStatusMessages())
return;
if(!event.isFromGuild())
return;
Guild guild = event.getGuild();
StatusState state = states.computeIfAbsent(guild.getIdLong(), id -> new StatusState());
synchronized(state)
{
MessageChannelUnion channelUnion = event.getChannel();
if(channelUnion == null || channelUnion.getType() != ChannelType.TEXT)
return;
TextChannel channel = channelUnion.asTextChannel();
boolean isCommand = isCommandMessage(event.getMessage(), guild);
Settings settings = bot.getSettingsManager().getSettings(guild);
if(isCommand && !event.getAuthor().isBot() && settings.getTextChannel(guild) == null)
state.lastCommandChannelId = channel.getIdLong();
Long statusChannelId = resolveStatusChannelId(guild, state);
if(statusChannelId == null || statusChannelId == 0L)
return;
if(isCommand && !event.getAuthor().isBot() && channel.getIdLong() == statusChannelId)
state.pendingRepost = true;
if(channel.getIdLong() != statusChannelId)
return;
if(state.messageId != 0L && event.getMessageIdLong() == state.messageId)
return;
if(state.pendingRepost)
{
long selfId = bot.getJDA().getSelfUser().getIdLong();
if(event.getAuthor().getIdLong() == selfId)
{
debug(guild, state, "pending_repost_trigger", "author_is_self", event.getMessageIdLong());
repostStatusMessage(guild, channel, state);
state.pendingRepost = false;
}
return;
}
// Avoid repost loops when the status message itself is created
if(event.getAuthor().getIdLong() == bot.getJDA().getSelfUser().getIdLong() && state.createInFlight)
return;
debug(guild, state, "repost_due_to_message", "message_author", event.getAuthor().getIdLong());
repostStatusMessage(guild, channel, state);
}
}
public void onMessageDelete(Guild guild, long messageId)
{
if(!bot.getConfig().useStatusMessages())
return;
StatusState state = states.get(guild.getIdLong());
if(state == null)
return;
synchronized(state)
{
if(state.createInFlight)
{
debug(guild, state, "delete_ignored_in_flight", "message", messageId);
return;
}
if(state.messageId == messageId)
state.messageId = 0L;
}
}
public void onTrackUpdate(long guildId)
{
if(!bot.getConfig().useStatusMessages())
return;
StatusState state = states.computeIfAbsent(guildId, id -> new StatusState());
state.forceUpdate = true;
}
private void updateAll()
{
if(!bot.getConfig().useStatusMessages())
return;
JDA jda = bot.getJDA();
if(jda == null)
return;
for(Map.Entry<Long, StatusState> entry : states.entrySet())
{
long guildId = entry.getKey();
StatusState state = entry.getValue();
synchronized(state)
{
Guild guild = jda.getGuildById(guildId);
if(guild == null)
continue;
TextChannel channel = resolveStatusChannel(guild, state);
if(channel == null)
continue;
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
if(handler == null)
continue;
if(state.createInFlight)
continue;
boolean isPlaying = handler.isMusicPlaying(jda);
if(!isPlaying && !state.forceUpdate)
continue;
if(state.pendingRepost && (state.messageId == 0L || state.channelId != channel.getIdLong()))
continue;
MessageCreateData statusMessage = handler.getStatusMessage(jda);
if(state.messageId == 0L || state.channelId != channel.getIdLong())
{
debug(guild, state, "repost_due_to_missing_or_channel", "channel", channel.getIdLong());
repostStatusMessage(guild, channel, state, statusMessage);
}
else
{
final long editingId = state.messageId;
channel.editMessageById(editingId, MessageEditData.fromCreateData(statusMessage))
.queue(m -> {}, t ->
{
debug(guild, state, "edit_failed", t, editingId);
if(state.messageId == editingId && shouldResetMessageId(t))
state.messageId = 0L;
});
}
state.forceUpdate = false;
}
}
}
private void repostStatusMessage(Guild guild, TextChannel channel, StatusState state)
{
MessageCreateData statusMessage = getStatusMessage(guild);
if(statusMessage == null)
return;
repostStatusMessage(guild, channel, state, statusMessage);
}
private void repostStatusMessage(Guild guild, TextChannel channel, StatusState state, MessageCreateData statusMessage)
{
if(state.createInFlight)
return;
state.createInFlight = true;
debug(guild, state, "repost_start", "channel", channel.getIdLong());
deleteOldStatusMessage(guild, state);
channel.sendMessage(statusMessage).queue(m ->
{
state.channelId = channel.getIdLong();
state.messageId = m.getIdLong();
state.createInFlight = false;
debug(guild, state, "repost_success", "message", m.getIdLong());
}, t ->
{
state.createInFlight = false;
debug(guild, state, "repost_failed", t, 0L);
});
}
private void deleteOldStatusMessage(Guild guild, StatusState state)
{
if(state.messageId == 0L)
return;
TextChannel oldChannel = guild.getTextChannelById(state.channelId);
if(oldChannel == null)
return;
oldChannel.deleteMessageById(state.messageId).queue(m -> {}, t -> debug(guild, state, "delete_failed", t, state.messageId));
}
private boolean shouldResetMessageId(Throwable throwable)
{
if(throwable instanceof ErrorResponseException)
{
ErrorResponse response = ((ErrorResponseException)throwable).getErrorResponse();
return response == ErrorResponse.UNKNOWN_MESSAGE || response == ErrorResponse.UNKNOWN_CHANNEL;
}
return false;
}
private void debug(Guild guild, StatusState state, String action, String key, long value)
{
if(LOG.isDebugEnabled())
LOG.debug("guild={} action={} {}={} msgId={} chanId={} pending={} inFlight={} force={}",
guild.getId(), action, key, value, state.messageId, state.channelId, state.pendingRepost, state.createInFlight, state.forceUpdate);
}
private void debug(Guild guild, StatusState state, String action, Throwable throwable, long messageId)
{
if(LOG.isDebugEnabled())
{
String err = throwable == null ? "null" : throwable.getClass().getSimpleName() + ": " + throwable.getMessage();
LOG.debug("guild={} action={} error={} msgId={} chanId={} pending={} inFlight={} force={}",
guild.getId(), action, err, messageId == 0L ? state.messageId : messageId,
state.channelId, state.pendingRepost, state.createInFlight, state.forceUpdate);
}
}
private MessageCreateData getStatusMessage(Guild guild)
{
AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler();
if(handler == null)
return null;
return handler.getStatusMessage(bot.getJDA());
}
private TextChannel resolveStatusChannel(Guild guild, StatusState state)
{
Long id = resolveStatusChannelId(guild, state);
return id == null ? null : guild.getTextChannelById(id);
}
private Long resolveStatusChannelId(Guild guild, StatusState state)
{
Settings settings = bot.getSettingsManager().getSettings(guild);
if(settings == null)
return null;
if(settings.getTextChannel(guild) != null)
return settings.getTextChannel(guild).getIdLong();
return state.lastCommandChannelId == 0L ? null : state.lastCommandChannelId;
}
private boolean isCommandMessage(Message message, Guild guild)
{
String content = message.getContentRaw();
if(content == null || content.isEmpty())
return false;
Settings settings = bot.getSettingsManager().getSettings(guild);
String prefix = settings.getPrefix() != null ? settings.getPrefix() : bot.getConfig().getPrefix();
String altprefix = bot.getConfig().getAltPrefix();
if(prefix != null && prefix.equalsIgnoreCase("@mention"))
{
long botId = bot.getJDA().getSelfUser().getIdLong();
String mention = "<@" + botId + ">";
String mentionNick = "<@!" + botId + ">";
if(content.startsWith(mention) || content.startsWith(mentionNick))
return true;
}
if(prefix != null && !prefix.equalsIgnoreCase("@mention") && content.startsWith(prefix))
return true;
return altprefix != null && !altprefix.isEmpty() && content.startsWith(altprefix);
}
private static class StatusState
{
private long channelId = 0L;
private long messageId = 0L;
private long lastCommandChannelId = 0L;
private boolean pendingRepost = false;
private boolean forceUpdate = false;
private boolean createInFlight = false;
}
}
@@ -0,0 +1,104 @@
/*
* Copyright 2021 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.audio;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.typesafe.config.Config;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class TransformativeAudioSourceManager extends YoutubeAudioSourceManager
{
private final static Logger log = LoggerFactory.getLogger(TransformativeAudioSourceManager.class);
private final String name, regex, replacement, selector, format;
public TransformativeAudioSourceManager(String name, Config object)
{
this(name, object.getString("regex"), object.getString("replacement"), object.getString("selector"), object.getString("format"));
}
public TransformativeAudioSourceManager(String name, String regex, String replacement, String selector, String format)
{
this.name = name;
this.regex = regex;
this.replacement = replacement;
this.selector = selector;
this.format = format;
}
@Override
public String getSourceName()
{
return name;
}
@Override
public AudioItem loadItem(AudioPlayerManager apm, AudioReference ar)
{
if(ar.identifier == null || !ar.identifier.matches(regex))
return null;
try
{
String url = ar.identifier.replaceAll(regex, replacement);
Document doc = Jsoup.connect(url).get();
String value = doc.selectFirst(selector).ownText();
String formattedValue = String.format(format, value);
return super.loadItem(apm, new AudioReference(formattedValue, null));
}
catch (PatternSyntaxException ex)
{
log.info(String.format("Invalid pattern syntax '%s' in source '%s'", regex, name));
}
catch (IOException ex)
{
log.warn(String.format("Failed to resolve URL in source '%s': ", name), ex);
}
catch (Exception ex)
{
log.warn(String.format("Exception in source '%s'", name), ex);
}
return null;
}
public static List<TransformativeAudioSourceManager> createTransforms(Config transforms)
{
try
{
return transforms.root().entrySet().stream()
.map(e -> new TransformativeAudioSourceManager(e.getKey(), transforms.getConfig(e.getKey())))
.collect(Collectors.toList());
}
catch (Exception ex)
{
log.warn("Invalid transform ", ex);
return Collections.emptyList();
}
}
}
@@ -16,7 +16,11 @@
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.Command;
import net.dv8tion.jda.core.Permission;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
import net.dv8tion.jda.api.Permission;
/**
*
@@ -24,8 +28,11 @@ import net.dv8tion.jda.core.Permission;
*/
public abstract class AdminCommand extends Command
{
public AdminCommand()
protected final Bot bot;
public AdminCommand(Bot bot)
{
this.bot = bot;
this.category = new Category("Admin", event ->
{
if(event.getAuthor().getId().equals(event.getClient().getOwnerId()))
@@ -36,4 +43,40 @@ public abstract class AdminCommand extends Command
});
this.guildOnly = true;
}
@Override
protected final void execute(CommandEvent event)
{
CommandLogContext.clear();
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -15,10 +15,11 @@
*/
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.settings.Settings;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Role;
/**
*
@@ -29,17 +30,19 @@ public abstract class DJCommand extends MusicCommand
public DJCommand(Bot bot)
{
super(bot);
this.category = new Category("DJ", event ->
{
if(event.getAuthor().getId().equals(event.getClient().getOwnerId()))
return true;
if(event.getGuild()==null)
return true;
if(event.getMember().hasPermission(Permission.MANAGE_SERVER))
return true;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
Role dj = settings.getRole(event.getGuild());
return dj!=null && (event.getMember().getRoles().contains(dj) || dj.getIdLong()==event.getGuild().getIdLong());
});
this.category = new Category("DJ", event -> checkDJPermission(event));
}
public static boolean checkDJPermission(CommandEvent event)
{
if(event.getAuthor().getId().equals(event.getClient().getOwnerId()))
return true;
if(event.getGuild()==null)
return true;
if(event.getMember().hasPermission(Permission.MANAGE_SERVER))
return true;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
Role dj = settings.getRole(event.getGuild());
return dj!=null && (event.getMember().getRoles().contains(dj) || dj.getIdLong()==event.getGuild().getIdLong());
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
public abstract class LoggedCommand extends Command
{
protected final Bot bot;
public LoggedCommand(Bot bot)
{
this.bot = bot;
}
@Override
protected final void execute(CommandEvent event)
{
CommandLogContext.clear();
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -20,10 +20,12 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import net.dv8tion.jda.core.entities.GuildVoiceState;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.core.entities.VoiceChannel;
import net.dv8tion.jda.core.exceptions.PermissionException;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.exceptions.PermissionException;
/**
*
@@ -45,6 +47,7 @@ public abstract class MusicCommand extends Command
@Override
protected void execute(CommandEvent event)
{
CommandLogContext.clear();
Settings settings = event.getClient().getSettingsFor(event.getGuild());
TextChannel tchannel = settings.getTextChannel(event.getGuild());
if(tchannel!=null && !event.getTextChannel().equals(tchannel))
@@ -54,26 +57,38 @@ public abstract class MusicCommand extends Command
event.getMessage().delete().queue();
} catch(PermissionException ignore){}
event.replyInDm(event.getClient().getError()+" You can only use that command in "+tchannel.getAsMention()+"!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "wrong_text_channel"));
return;
}
bot.getPlayerManager().setUpHandler(event.getGuild()); // no point constantly checking for this later
if(bePlaying && !((AudioHandler)event.getGuild().getAudioManager().getSendingHandler()).isMusicPlaying(event.getJDA()))
{
event.reply(event.getClient().getError()+" There must be music playing to use that!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "no_music_playing"));
return;
}
if(beListening)
{
VoiceChannel current = event.getGuild().getSelfMember().getVoiceState().getChannel();
VoiceChannel current = (VoiceChannel) event.getGuild().getSelfMember().getVoiceState().getChannel();
if(current==null)
current = settings.getVoiceChannel(event.getGuild());
GuildVoiceState userState = event.getMember().getVoiceState();
if(!userState.inVoiceChannel() || userState.isDeafened() || (current!=null && !userState.getChannel().equals(current)))
if(!userState.inAudioChannel() || userState.isDeafened() || (current!=null && !userState.getChannel().equals(current)))
{
event.replyError("You must be listening in "+(current==null ? "a voice channel" : "**"+current.getName()+"**")+" to use that!");
event.replyError("You must be listening in "+(current==null ? "a voice channel" : current.getAsMention())+" to use that!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "not_listening"));
return;
}
if(!event.getGuild().getSelfMember().getVoiceState().inVoiceChannel())
VoiceChannel afkChannel = userState.getGuild().getAfkChannel();
if(afkChannel != null && afkChannel.equals(userState.getChannel()))
{
event.replyError("You cannot use that command in an AFK channel!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "afk_channel"));
return;
}
if(!event.getGuild().getSelfMember().getVoiceState().inAudioChannel())
{
try
{
@@ -81,14 +96,42 @@ public abstract class MusicCommand extends Command
}
catch(PermissionException ex)
{
event.reply(event.getClient().getError()+" I am unable to connect to **"+userState.getChannel().getName()+"**!");
event.reply(event.getClient().getError()+" I am unable to connect to "+userState.getChannel().getAsMention()+"!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "cannot_connect"));
return;
}
}
}
doCommand(event);
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -16,6 +16,10 @@
package com.jagrosh.jmusicbot.commands;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -23,9 +27,48 @@ import com.jagrosh.jdautilities.command.Command;
*/
public abstract class OwnerCommand extends Command
{
public OwnerCommand()
protected final Bot bot;
public OwnerCommand(Bot bot)
{
this.bot = bot;
this.category = new Category("Owner");
this.ownerCommand = true;
}
@Override
protected final void execute(CommandEvent event)
{
CommandLogContext.clear();
try
{
doCommand(event);
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
if(entry != null && entry.manual)
return;
String result = entry == null || entry.result == null ? "OK" : entry.result;
JSONObject meta = entry == null ? null : entry.meta;
logCommand(event, result, meta);
}
catch(Exception ex)
{
CommandLogContext.CommandLogEntry entry = CommandLogContext.consume();
JSONObject meta = entry == null ? null : entry.meta;
if(meta == null)
meta = new JSONObject();
meta.put("error_reason", ex.getClass().getSimpleName());
logCommand(event, "ERROR", meta);
throw ex;
}
}
public abstract void doCommand(CommandEvent event);
protected void logCommand(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2018 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.admin;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class PrefixCmd extends AdminCommand
{
public PrefixCmd(Bot bot)
{
super(bot);
this.name = "prefix";
this.help = "sets a server-specific prefix";
this.arguments = "<prefix|NONE>";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.replyError("Please include a prefix or NONE");
CommandLogContext.setError("missing_prefix");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getPrefix();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setPrefix(null);
event.replySuccess("Prefix cleared.");
JSONObject meta = new JSONObject().put("before", before).put("after", JSONObject.NULL);
CommandLogContext.setMeta(meta);
}
else
{
s.setPrefix(event.getArgs());
event.replySuccess("Custom prefix set to `" + event.getArgs() + "` on *" + event.getGuild().getName() + "*");
JSONObject meta = new JSONObject().put("before", before).put("after", event.getArgs());
CommandLogContext.setMeta(meta);
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2022 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.admin;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
* @author Wolfgang Schwendtbauer
*/
public class QueueTypeCmd extends AdminCommand
{
public QueueTypeCmd(Bot bot)
{
super(bot);
this.name = "queuetype";
this.help = "changes the queue type";
this.arguments = "[" + String.join("|", QueueType.getNames()) + "]";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
public void doCommand(CommandEvent event)
{
String args = event.getArgs();
QueueType value;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
QueueType before = settings.getQueueType();
if (args.isEmpty())
{
QueueType currentType = settings.getQueueType();
event.reply(currentType.getEmoji() + " Current queue type is: `" + currentType.getUserFriendlyName() + "`.");
CommandLogContext.setMeta(new JSONObject().put("current", currentType.name()));
return;
}
try
{
value = QueueType.valueOf(args.toUpperCase());
}
catch (IllegalArgumentException e)
{
event.replyError("Invalid queue type. Valid types are: [" + String.join("|", QueueType.getNames()) + "]");
CommandLogContext.setError("invalid_queue_type");
return;
}
if (settings.getQueueType() != value)
{
settings.setQueueType(value);
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
if (handler != null)
handler.setQueueType(value);
}
event.reply(value.getEmoji() + " Queue type was set to `" + value.getUserFriendlyName() + "`.");
CommandLogContext.setMeta(new JSONObject().put("before", before.name()).put("after", value.name()));
}
}
@@ -18,10 +18,13 @@ package com.jagrosh.jmusicbot.commands.admin;
import java.util.List;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.api.entities.Role;
import org.json.JSONObject;
/**
*
@@ -29,38 +32,50 @@ import net.dv8tion.jda.core.entities.Role;
*/
public class SetdjCmd extends AdminCommand
{
public SetdjCmd()
public SetdjCmd(Bot bot)
{
super(bot);
this.name = "setdj";
this.help = "sets the DJ role for certain music commands";
this.arguments = "<rolename|NONE>";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a role name or NONE");
CommandLogContext.setError("missing_role");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getRole(event.getGuild()) == null ? null : s.getRole(event.getGuild()).getId();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setDJRole(null);
event.reply(event.getClient().getSuccess()+" DJ role cleared; Only Admins can use the DJ commands.");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
}
else
{
List<Role> list = FinderUtil.findRoles(event.getArgs(), event.getGuild());
if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" No Roles found matching \""+event.getArgs()+"\"");
CommandLogContext.setError("role_not_found");
}
else if (list.size()>1)
{
event.reply(event.getClient().getWarning()+FormatUtil.listOfRoles(list, event.getArgs()));
CommandLogContext.setError("role_ambiguous");
}
else
{
s.setDJRole(list.get(0));
event.reply(event.getClient().getSuccess()+" DJ commands can now be used by users with the **"+list.get(0).getName()+"** role.");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -18,10 +18,13 @@ package com.jagrosh.jmusicbot.commands.admin;
import java.util.List;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.json.JSONObject;
/**
*
@@ -29,38 +32,51 @@ import net.dv8tion.jda.core.entities.TextChannel;
*/
public class SettcCmd extends AdminCommand
{
public SettcCmd()
public SettcCmd(Bot bot)
{
super(bot);
this.name = "settc";
this.help = "sets the text channel for music commands";
this.arguments = "<channel|NONE>";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a text channel or NONE");
CommandLogContext.setError("missing_text_channel");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getTextChannel(event.getGuild()) == null ? null : s.getTextChannel(event.getGuild()).getId();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setTextChannel(null);
event.reply(event.getClient().getSuccess()+" Music commands can now be used in any channel");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
}
else
{
List<TextChannel> list = FinderUtil.findTextChannels(event.getArgs(), event.getGuild());
@SuppressWarnings({"unchecked", "rawtypes"})
List<TextChannel> list = (List) FinderUtil.findTextChannels(event.getArgs(), event.getGuild());
if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" No Text Channels found matching \""+event.getArgs()+"\"");
CommandLogContext.setError("text_channel_not_found");
}
else if (list.size()>1)
{
event.reply(event.getClient().getWarning()+FormatUtil.listOfTChannels(list, event.getArgs()));
CommandLogContext.setError("text_channel_ambiguous");
}
else
{
s.setTextChannel(list.get(0));
event.reply(event.getClient().getSuccess()+" Music commands can now only be used in <#"+list.get(0).getId()+">");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -18,10 +18,13 @@ package com.jagrosh.jmusicbot.commands.admin;
import java.util.List;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.core.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.json.JSONObject;
/**
*
@@ -29,38 +32,51 @@ import net.dv8tion.jda.core.entities.VoiceChannel;
*/
public class SetvcCmd extends AdminCommand
{
public SetvcCmd()
public SetvcCmd(Bot bot)
{
super(bot);
this.name = "setvc";
this.help = "sets the voice channel for playing music";
this.arguments = "<channel|NONE>";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a voice channel or NONE");
CommandLogContext.setError("missing_voice_channel");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
String before = s.getVoiceChannel(event.getGuild()) == null ? null : s.getVoiceChannel(event.getGuild()).getId();
if(event.getArgs().equalsIgnoreCase("none"))
{
s.setVoiceChannel(null);
event.reply(event.getClient().getSuccess()+" Music can now be played in any channel");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
}
else
{
List<VoiceChannel> list = FinderUtil.findVoiceChannels(event.getArgs(), event.getGuild());
@SuppressWarnings({"unchecked", "rawtypes"})
List<VoiceChannel> list = (List) FinderUtil.findVoiceChannels(event.getArgs(), event.getGuild());
if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" No Voice Channels found matching \""+event.getArgs()+"\"");
CommandLogContext.setError("voice_channel_not_found");
}
else if (list.size()>1)
{
event.reply(event.getClient().getWarning()+FormatUtil.listOfVChannels(list, event.getArgs()));
CommandLogContext.setError("voice_channel_ambiguous");
}
else
{
s.setVoiceChannel(list.get(0));
event.reply(event.getClient().getSuccess()+" Music can now only be played in **"+list.get(0).getName()+"**");
event.reply(event.getClient().getSuccess()+" Music can now only be played in "+list.get(0).getAsMention());
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2018 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.admin;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class SkipratioCmd extends AdminCommand
{
public SkipratioCmd(Bot bot)
{
super(bot);
this.name = "setskip";
this.help = "sets a server-specific skip percentage";
this.arguments = "<0 - 100>";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
public void doCommand(CommandEvent event)
{
try
{
int val = Integer.parseInt(event.getArgs().endsWith("%") ? event.getArgs().substring(0,event.getArgs().length()-1) : event.getArgs());
if( val < 0 || val > 100)
{
event.replyError("The provided value must be between 0 and 100!");
CommandLogContext.setError("invalid_skip_ratio");
return;
}
Settings s = event.getClient().getSettingsFor(event.getGuild());
double before = s.getSkipRatio();
s.setSkipRatio(val / 100.0);
event.replySuccess("Skip percentage has been set to `" + val + "%` of listeners on *" + event.getGuild().getName() + "*");
CommandLogContext.setMeta(new JSONObject()
.put("before", before)
.put("after", s.getSkipRatio()));
}
catch(NumberFormatException ex)
{
event.replyError("Please include an integer between 0 and 100 (default is 55). This number is the percentage of listening users that must vote to skip a song.");
CommandLogContext.setError("invalid_skip_ratio");
}
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2019 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.dj;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jdautilities.menu.OrderedMenu;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import org.json.JSONObject;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
*
* @author Michaili K.
*/
public class ForceRemoveCmd extends DJCommand
{
public ForceRemoveCmd(Bot bot)
{
super(bot);
this.name = "forceremove";
this.help = "removes all entries by a user from the queue";
this.arguments = "<user>";
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = false;
this.bePlaying = true;
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
}
@Override
public void doCommand(CommandEvent event)
{
if (event.getArgs().isEmpty())
{
event.replyError("You need to mention a user!");
CommandLogContext.setError("missing_user");
return;
}
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
if (handler.getQueue().isEmpty())
{
event.replyError("There is nothing in the queue!");
CommandLogContext.setError("empty_queue");
return;
}
User target;
List<Member> found = FinderUtil.findMembers(event.getArgs(), event.getGuild());
if(found.isEmpty())
{
event.replyError("Unable to find the user!");
CommandLogContext.setError("user_not_found");
return;
}
else if(found.size()>1)
{
OrderedMenu.Builder builder = new OrderedMenu.Builder();
for(int i=0; i<found.size() && i<4; i++)
{
Member member = found.get(i);
builder.addChoice("**"+member.getUser().getName()+"**#"+member.getUser().getDiscriminator());
}
builder
.setSelection((msg, i) -> removeAllEntries(found.get(i-1).getUser(), event))
.setText("Found multiple users:")
.setColor(event.getSelfMember().getColor())
.useNumbers()
.setUsers(event.getAuthor())
.useCancelButton(true)
.setCancel((msg) -> {})
.setEventWaiter(bot.getWaiter())
.setTimeout(1, TimeUnit.MINUTES)
.build().display(event.getChannel());
return;
}
else
{
target = found.get(0).getUser();
}
removeAllEntries(target, event);
}
private void removeAllEntries(User target, CommandEvent event)
{
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
int sizeBefore = handler.getQueue().size();
int count = handler.getQueue().removeAll(target.getIdLong());
if (count == 0)
{
event.replyWarning("**"+target.getName()+"** doesn't have any songs in the queue!");
CommandLogContext.setError("no_entries_for_user");
}
else
{
event.replySuccess("Successfully removed `"+count+"` entries from "+FormatUtil.formatUsername(target)+".");
}
JSONObject meta = new JSONObject()
.put("target_user_id", target.getIdLong())
.put("removed_count", count)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), null, "REMOVE_ALL",
"FORCE", null, null, null, meta.toString());
}
}
@@ -18,8 +18,11 @@ package com.jagrosh.jmusicbot.commands.dj;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.DJCommand;
import net.dv8tion.jda.core.entities.User;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -32,7 +35,7 @@ public class ForceskipCmd extends DJCommand
super(bot);
this.name = "forceskip";
this.help = "skips the current song";
this.aliases = new String[]{"modskip"};
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = true;
}
@@ -40,9 +43,16 @@ public class ForceskipCmd extends DJCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
User u = event.getJDA().getUserById(handler.getRequester());
RequestMetadata rm = handler.getRequestMetadata();
event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title
+"** (requested by "+(u==null ? "someone" : "**"+u.getName()+"**")+")");
+"** "+(rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)"));
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject().put("reason", "FORCE");
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_FORCE", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.getPlayer().stopTrack();
}
}
@@ -6,7 +6,9 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.queue.FairQueue;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.queue.AbstractQueue;
import org.json.JSONObject;
/**
* Command that provides users the ability to move a track in the playlist.
@@ -20,7 +22,7 @@ public class MoveTrackCmd extends DJCommand
this.name = "movetrack";
this.help = "move a track in the current queue to a different position";
this.arguments = "<from> <to>";
this.aliases = new String[]{"move"};
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = true;
}
@@ -34,6 +36,7 @@ public class MoveTrackCmd extends DJCommand
if(parts.length < 2)
{
event.replyError("Please include two valid indexes.");
CommandLogContext.setError("missing_indexes");
return;
}
@@ -46,40 +49,54 @@ public class MoveTrackCmd extends DJCommand
catch (NumberFormatException e)
{
event.replyError("Please provide two valid indexes.");
CommandLogContext.setError("invalid_indexes");
return;
}
if (from == to)
{
event.replyError("Can't move a track to the same position.");
CommandLogContext.setError("same_position");
return;
}
// Validate that from and to are available
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
FairQueue<QueuedTrack> queue = handler.getQueue();
AbstractQueue<QueuedTrack> queue = handler.getQueue();
if (isUnavailablePosition(queue, from))
{
String reply = String.format("`%d` is not a valid position in the queue!", from);
event.replyError(reply);
CommandLogContext.setError("invalid_from_position");
return;
}
if (isUnavailablePosition(queue, to))
{
String reply = String.format("`%d` is not a valid position in the queue!", to);
event.replyError(reply);
CommandLogContext.setError("invalid_to_position");
return;
}
// Move the track
int sizeBefore = queue.size();
QueuedTrack track = queue.moveItem(from - 1, to - 1);
String trackTitle = track.getTrack().getInfo().title;
String reply = String.format("Moved **%s** from position `%d` to `%d`.", trackTitle, from, to);
event.replySuccess(reply);
JSONObject meta = new JSONObject()
.put("position_from", from)
.put("position_to", to)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", queue.size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), track.getTrack(),
"MOVE", null, to, null, null, meta.toString());
}
private static boolean isUnavailablePosition(FairQueue<QueuedTrack> queue, int position)
private static boolean isUnavailablePosition(AbstractQueue<QueuedTrack> queue, int position)
{
return (position < 1 || position > queue.size());
}
}
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -31,6 +33,7 @@ public class PauseCmd extends DJCommand
super(bot);
this.name = "pause";
this.help = "pauses the current song";
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = true;
}
@@ -41,9 +44,16 @@ public class PauseCmd extends DJCommand
if(handler.getPlayer().isPaused())
{
event.replyWarning("The player is already paused! Use `"+event.getClient().getPrefix()+"play` to unpause!");
CommandLogContext.setError("already_paused");
return;
}
handler.getPlayer().setPaused(true);
event.replySuccess("Paused **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**. Type `"+event.getClient().getPrefix()+"play` to unpause!");
JSONObject meta = new JSONObject()
.put("position_ms", handler.getPlayer().getPlayingTrack().getPosition());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"PAUSE", null, null, null, null, meta.toString());
}
}
@@ -19,13 +19,17 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.api.entities.Message;
import org.json.JSONObject;
/**
*
@@ -35,13 +39,14 @@ public class PlaynextCmd extends DJCommand
{
private final String loadingEmoji;
public PlaynextCmd(Bot bot, String loadingEmoji)
public PlaynextCmd(Bot bot)
{
super(bot);
this.loadingEmoji = loadingEmoji;
this.loadingEmoji = bot.getConfig().getLoading();
this.name = "playnext";
this.arguments = "<title|URL>";
this.help = "plays a single song next";
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = false;
}
@@ -49,9 +54,11 @@ public class PlaynextCmd extends DJCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
{
event.replyWarning("Please include a song title or URL!");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "missing_query"));
return;
}
String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">")
@@ -78,14 +85,19 @@ public class PlaynextCmd extends DJCommand
if(bot.getConfig().isTooLong(track))
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
+ TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrackToFront(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "PLAYNEXT", null, event.getArgs());
int pos = handler.addTrackToFront(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "PLAYNEXT", pos, event.getArgs(), null);
String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
+"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
m.editMessage(addMsg).queue();
logCommandEvent(event, "OK", new JSONObject().put("queue_position", pos));
}
@Override
@@ -111,7 +123,10 @@ public class PlaynextCmd extends DJCommand
public void noMatches()
{
if(ytsearch)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_matches"));
}
else
bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+event.getArgs(), new ResultHandler(m,event,true));
}
@@ -123,6 +138,15 @@ public class PlaynextCmd extends DJCommand
m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
else
m.editMessage(event.getClient().getError()+" Error loading track.").queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", throwable.getMessage()));
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -18,7 +18,10 @@ package com.jagrosh.jmusicbot.commands.dj;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -31,37 +34,45 @@ public class RepeatCmd extends DJCommand
super(bot);
this.name = "repeat";
this.help = "re-adds music to the queue when finished";
this.arguments = "[on|off]";
this.arguments = "[off|all|single]";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = true;
}
// override musiccommand's execute because we don't actually care where this is used
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
boolean value;
String args = event.getArgs();
RepeatMode value;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
if(event.getArgs().isEmpty())
RepeatMode before = settings.getRepeatMode();
if(args.isEmpty())
{
value = !settings.getRepeatMode();
if(settings.getRepeatMode() == RepeatMode.OFF)
value = RepeatMode.ALL;
else
value = RepeatMode.OFF;
}
else if(event.getArgs().equalsIgnoreCase("true") || event.getArgs().equalsIgnoreCase("on"))
else if(args.equalsIgnoreCase("false") || args.equalsIgnoreCase("off"))
{
value = true;
value = RepeatMode.OFF;
}
else if(event.getArgs().equalsIgnoreCase("false") || event.getArgs().equalsIgnoreCase("off"))
else if(args.equalsIgnoreCase("true") || args.equalsIgnoreCase("on") || args.equalsIgnoreCase("all"))
{
value = false;
value = RepeatMode.ALL;
}
else if(args.equalsIgnoreCase("one") || args.equalsIgnoreCase("single"))
{
value = RepeatMode.SINGLE;
}
else
{
event.replyError("Valid options are `on` or `off` (or leave empty to toggle)");
event.replyError("Valid options are `off`, `all` or `single` (or leave empty to toggle between `off` and `all`)");
CommandLogContext.setError("invalid_repeat_mode");
return;
}
settings.setRepeatMode(value);
event.replySuccess("Repeat mode is now `"+(value ? "ON" : "OFF")+"`");
event.replySuccess("Repeat mode is now `"+value.getUserFriendlyName()+"`");
CommandLogContext.setMeta(new JSONObject().put("before", before.name()).put("after", value.name()));
}
@Override
public void doCommand(CommandEvent event) { /* Intentionally Empty */ }
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -32,7 +34,7 @@ public class SkiptoCmd extends DJCommand
this.name = "skipto";
this.help = "skips to the specified song";
this.arguments = "<position>";
this.aliases = new String[]{"jumpto"};
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = true;
}
@@ -47,16 +49,27 @@ public class SkiptoCmd extends DJCommand
catch(NumberFormatException e)
{
event.reply(event.getClient().getError()+" `"+event.getArgs()+"` is not a valid integer!");
CommandLogContext.setError("invalid_index");
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
if(index<1 || index>handler.getQueue().size())
{
event.reply(event.getClient().getError()+" Position must be a valid integer between 1 and "+handler.getQueue().size()+"!");
CommandLogContext.setError("index_out_of_range");
return;
}
int sizeBefore = handler.getQueue().size();
handler.getQueue().skip(index-1);
event.reply(event.getClient().getSuccess()+" Skipped to **"+handler.getQueue().get(0).getTrack().getInfo().title+"**");
JSONObject meta = new JSONObject()
.put("position_to", index)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_SKIPTO", null, index, null, null, meta.toString());
handler.getPlayer().stopTrack();
}
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -31,6 +33,7 @@ public class StopCmd extends DJCommand
super(bot);
this.name = "stop";
this.help = "stops the current song and clears the queue";
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = false;
}
@@ -38,6 +41,16 @@ public class StopCmd extends DJCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int sizeBefore = handler.getQueue().size();
if(handler.getPlayer().getPlayingTrack() != null && bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject()
.put("queue_size_before", sizeBefore)
.put("queue_size_after", 0);
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"STOP", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.stopAndClear();
event.getGuild().getAudioManager().closeAudioConnection();
event.reply(event.getClient().getSuccess()+" The player has stopped and the queue has been cleared.");
@@ -19,8 +19,10 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -32,7 +34,7 @@ public class VolumeCmd extends DJCommand
{
super(bot);
this.name = "volume";
this.aliases = new String[]{"vol"};
this.aliases = bot.getConfig().getAliases(this.name);
this.help = "sets or shows volume";
this.arguments = "[0-150]";
}
@@ -46,6 +48,7 @@ public class VolumeCmd extends DJCommand
if(event.getArgs().isEmpty())
{
event.reply(FormatUtil.volumeIcon(volume)+" Current volume is `"+volume+"`");
CommandLogContext.setMeta(new JSONObject().put("current", volume));
}
else
{
@@ -56,12 +59,16 @@ public class VolumeCmd extends DJCommand
nvolume = -1;
}
if(nvolume<0 || nvolume>150)
{
event.reply(event.getClient().getError()+" Volume must be a valid integer between 0 and 150!");
CommandLogContext.setError("invalid_volume");
}
else
{
handler.getPlayer().setVolume(nvolume);
settings.setVolume(nvolume);
event.reply(FormatUtil.volumeIcon(nvolume)+" Volume changed from `"+volume+"` to `"+nvolume+"`");
CommandLogContext.setMeta(new JSONObject().put("before", volume).put("after", nvolume));
}
}
}
@@ -0,0 +1,201 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.general;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.examples.command.AboutCommand;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.JMusicBot;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import java.awt.Color;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDAInfo;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.ApplicationInfo;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import org.json.JSONObject;
public class LoggedAboutCmd extends AboutCommand
{
private final Bot bot;
private final Color color;
private final String description;
private final String[] features;
private final Permission[] perms;
private boolean isAuthor = true;
private String replacementCharacter = "+";
private String oauthLink;
public LoggedAboutCmd(Bot bot, Color color, String description, String[] features, Permission[] perms)
{
super(color, description, features, perms);
this.bot = bot;
this.color = color;
this.description = description;
this.features = features == null ? new String[0] : features.clone();
this.perms = perms == null ? new Permission[0] : perms.clone();
}
@Override
public void setIsAuthor(boolean isAuthor)
{
super.setIsAuthor(isAuthor);
this.isAuthor = isAuthor;
}
@Override
public void setReplacementCharacter(String replacementCharacter)
{
super.setReplacementCharacter(replacementCharacter);
this.replacementCharacter = replacementCharacter;
}
@Override
protected void execute(CommandEvent event)
{
if(oauthLink == null)
oauthLink = resolveOauthLink(event);
EmbedBuilder builder = new EmbedBuilder()
.setColor(event.isFromType(ChannelType.TEXT)
? event.getGuild().getSelfMember().getColor()
: color)
.setAuthor("All about " + event.getSelfUser().getName() + "!",
null,
event.getSelfUser().getAvatarUrl())
.setDescription(buildDescription(event));
addStats(builder, event);
builder.setFooter("Last restart", null)
.setTimestamp(event.getClient().getStartTime());
event.reply(builder.build());
if(bot.getDataLogService() != null && event.getGuild() != null)
{
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), "OK", new JSONObject().put("about", true).toString());
}
}
private String resolveOauthLink(CommandEvent event)
{
try
{
ApplicationInfo info = event.getJDA().retrieveApplicationInfo().complete();
return info.isBotPublic() ? info.getInviteUrl(0, perms) : "";
}
catch(Exception ex)
{
JMusicBot.LOG.error("Could not generate invite link", ex);
return "";
}
}
private String buildDescription(CommandEvent event)
{
String owner = getOwnerDisplay(event);
String links = getLinksSection(event);
StringBuilder builder = new StringBuilder()
.append("Hello! I am **").append(event.getSelfUser().getName()).append("**, ")
.append(description)
.append("\nI ")
.append(isAuthor ? "was written in Java" : "am owned")
.append(" by **").append(owner).append("**")
.append(" using Chew's [Commands Extension](https://github.com/Chew/JDA-Chewtils) (")
.append(OtherUtil.getCommandLibraryVersion())
.append(") and the [JDA library](https://github.com/DV8FromTheWorld/JDA) (")
.append(JDAInfo.VERSION)
.append(")\nType `")
.append(event.getClient().getTextualPrefix())
.append(event.getClient().getHelpWord())
.append("` to see my commands!");
if(!links.isEmpty())
builder.append(links);
builder.append("\n\nSome of my features include: ```css");
for(String feature : features)
{
builder.append("\n")
.append(event.getClient().getSuccess().startsWith("<")
? replacementCharacter
: event.getClient().getSuccess())
.append(" ")
.append(feature);
}
builder.append("```");
return builder.toString();
}
private String getOwnerDisplay(CommandEvent event)
{
User owner = event.getJDA().getUserById(event.getClient().getOwnerId());
return owner == null ? "<@" + event.getClient().getOwnerId() + ">" : owner.getName();
}
private String getLinksSection(CommandEvent event)
{
String serverInvite = event.getClient().getServerInvite();
boolean hasServerInvite = serverInvite != null && !serverInvite.isEmpty();
boolean hasOauthLink = oauthLink != null && !oauthLink.isEmpty();
if(!hasServerInvite && !hasOauthLink)
return "";
StringBuilder builder = new StringBuilder("\n");
if(hasServerInvite)
builder.append("Join my server [`here`](").append(serverInvite).append(")");
else if(hasOauthLink)
builder.append("Please");
if(hasOauthLink)
{
if(hasServerInvite)
builder.append(", or");
builder.append(" [`invite`](").append(oauthLink).append(") me to your server");
}
builder.append("!");
return builder.toString();
}
private void addStats(EmbedBuilder builder, CommandEvent event)
{
JDA jda = event.getJDA();
if(jda.getShardInfo() == JDA.ShardInfo.SINGLE)
{
builder.addField("Stats", jda.getGuilds().size() + " servers\n1 shard", true)
.addField("Users",
jda.getUsers().size() + " unique\n"
+ jda.getGuilds().stream().mapToInt(Guild::getMemberCount).sum() + " total",
true)
.addField("Channels",
jda.getTextChannels().size() + " Text\n" + jda.getVoiceChannels().size() + " Voice",
true);
return;
}
builder.addField("Stats",
event.getClient().getTotalGuilds() + " Servers\nShard "
+ (jda.getShardInfo().getShardId() + 1) + "/" + jda.getShardInfo().getShardTotal(),
true)
.addField("This shard", jda.getUsers().size() + " Users\n" + jda.getGuilds().size() + " Servers", true)
.addField("",
jda.getTextChannels().size() + " Text Channels\n" + jda.getVoiceChannels().size() + " Voice Channels",
true);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.general;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.examples.command.PingCommand;
import com.jagrosh.jmusicbot.Bot;
import org.json.JSONObject;
public class LoggedPingCmd extends PingCommand
{
private final Bot bot;
public LoggedPingCmd(Bot bot)
{
super();
this.bot = bot;
}
@Override
protected void execute(CommandEvent event)
{
super.execute(event);
if(bot.getDataLogService() != null && event.getGuild() != null)
{
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), "OK", new JSONObject().put("ping", true).toString());
}
}
}
@@ -15,54 +15,76 @@
*/
package com.jagrosh.jmusicbot.commands.general;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.LoggedCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import net.dv8tion.jda.core.EmbedBuilder;
import net.dv8tion.jda.core.MessageBuilder;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.core.entities.VoiceChannel;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.json.JSONObject;
/**
*
* @author John Grosh <john.a.grosh@gmail.com>
*/
public class SettingsCmd extends Command
public class SettingsCmd extends LoggedCommand
{
private final static String EMOJI = "\uD83C\uDFA7"; // 🎧
public SettingsCmd()
public SettingsCmd(Bot bot)
{
super(bot);
this.name = "settings";
this.help = "shows the bots settings";
this.aliases = new String[]{"status"};
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = true;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
Settings s = event.getClient().getSettingsFor(event.getGuild());
MessageBuilder builder = new MessageBuilder()
.append(EMOJI + " **")
.append(event.getSelfUser().getName())
.append("** settings:");
JSONObject meta = new JSONObject()
.put("repeat_mode", s.getRepeatMode().name())
.put("queue_type", s.getQueueType().name())
.put("has_default_playlist", s.getDefaultPlaylist() != null)
.put("has_custom_prefix", s.getPrefix() != null)
.put("has_text_channel", s.getTextChannel(event.getGuild()) != null)
.put("has_voice_channel", s.getVoiceChannel(event.getGuild()) != null)
.put("has_dj_role", s.getRole(event.getGuild()) != null);
CommandLogContext.setMeta(meta);
MessageCreateBuilder builder = new MessageCreateBuilder()
.addContent(EMOJI + " **")
.addContent(FormatUtil.filter(event.getSelfUser().getName()))
.addContent("** settings:");
TextChannel tchan = s.getTextChannel(event.getGuild());
VoiceChannel vchan = s.getVoiceChannel(event.getGuild());
Role role = s.getRole(event.getGuild());
EmbedBuilder ebuilder = new EmbedBuilder()
.setColor(event.getSelfMember().getColor())
.setDescription("Text Channel: "+(tchan==null ? "Any" : "**#"+tchan.getName()+"**")
+ "\nVoice Channel: "+(vchan==null ? "Any" : "**"+vchan.getName()+"**")
+ "\nDJ Role: "+(role==null ? "None" : "**"+role.getName()+"**")
+ "\nRepeat Mode: **"+(s.getRepeatMode() ? "On" : "Off")+"**"
+ "\nDefault Playlist: "+(s.getDefaultPlaylist()==null ? "None" : "**"+s.getDefaultPlaylist()+"**")
.setDescription("Text Channel: " + (tchan == null ? "Any" : "**#" + tchan.getName() + "**")
+ "\nVoice Channel: " + (vchan == null ? "Any" : vchan.getAsMention())
+ "\nDJ Role: " + (role == null ? "None" : "**" + role.getName() + "**")
+ "\nCustom Prefix: " + (s.getPrefix() == null ? "None" : "`" + s.getPrefix() + "`")
+ "\nRepeat Mode: " + (s.getRepeatMode() == RepeatMode.OFF
? s.getRepeatMode().getUserFriendlyName()
: "**"+s.getRepeatMode().getUserFriendlyName()+"**")
+ "\nQueue Type: " + (s.getQueueType() == QueueType.FAIR
? s.getQueueType().getUserFriendlyName()
: "**"+s.getQueueType().getUserFriendlyName()+"**")
+ "\nDefault Playlist: " + (s.getDefaultPlaylist() == null ? "None" : "**" + s.getDefaultPlaylist() + "**")
)
.setFooter(event.getJDA().getGuilds().size()+" servers | "
+event.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inVoiceChannel()).count()
+" audio connections", null);
event.getChannel().sendMessage(builder.setEmbed(ebuilder.build()).build()).queue();
.setFooter(event.getJDA().getGuilds().size() + " servers | "
+ event.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inAudioChannel()).count()
+ " audio connections", null);
event.getMessage().getChannel().sendMessage(builder.setEmbeds(ebuilder.build()).build()).queue();
}
}
@@ -16,14 +16,14 @@
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jlyrics.Lyrics;
import com.jagrosh.jlyrics.LyricsClient;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import java.util.concurrent.ExecutionException;
import net.dv8tion.jda.core.EmbedBuilder;
import net.dv8tion.jda.core.Permission;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import org.json.JSONObject;
/**
*
@@ -38,25 +38,37 @@ public class LyricsCmd extends MusicCommand
super(bot);
this.name = "lyrics";
this.arguments = "[song name]";
this.help = "shows the lyrics to the currently-playing song";
this.help = "shows the lyrics of a song";
this.aliases = bot.getConfig().getAliases(this.name);
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
this.bePlaying = true;
}
@Override
public void doCommand(CommandEvent event)
{
event.getChannel().sendTyping().queue();
CommandLogContext.setManualLogging();
String title;
if(event.getArgs().isEmpty())
title = ((AudioHandler)event.getGuild().getAudioManager().getSendingHandler()).getPlayer().getPlayingTrack().getInfo().title;
{
AudioHandler sendingHandler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
if (sendingHandler.isMusicPlaying(event.getJDA()))
title = sendingHandler.getPlayer().getPlayingTrack().getInfo().title;
else
{
event.replyError("There must be music playing to use that!");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_music_playing"));
return;
}
}
else
title = event.getArgs();
event.getChannel().sendTyping().queue();
client.getLyrics(title).thenAccept(lyrics ->
{
if(lyrics == null)
{
event.replyError("Lyrics for `" + title + "` could not be found!");
event.replyError("Lyrics for `" + title + "` could not be found!" + (event.getArgs().isEmpty() ? " Try entering the song name manually (`lyrics [song name]`)" : ""));
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "lyrics_not_found"));
return;
}
@@ -88,6 +100,15 @@ public class LyricsCmd extends MusicCommand
}
else
event.reply(eb.setDescription(lyrics.getContent()).build());
logCommandEvent(event, "OK", new JSONObject().put("title", lyrics.getTitle()));
});
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -19,8 +19,10 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Message;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import org.json.JSONObject;
/**
*
@@ -33,7 +35,7 @@ public class NowplayingCmd extends MusicCommand
super(bot);
this.name = "nowplaying";
this.help = "shows the song that is currently playing";
this.aliases = new String[]{"np","current"};
this.aliases = bot.getConfig().getAliases(this.name);
this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
}
@@ -41,15 +43,19 @@ public class NowplayingCmd extends MusicCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
Message m = handler.getNowPlaying(event.getJDA());
MessageCreateData m = handler.getNowPlaying(event.getJDA());
if(m==null)
{
event.reply(handler.getNoMusicPlaying(event.getJDA()));
event.getMessage().getChannel().sendMessage(handler.getNoMusicPlaying(event.getJDA())).queue();
bot.getNowplayingHandler().clearLastNPMessage(event.getGuild());
CommandLogContext.setMeta(new JSONObject().put("playing", false));
}
else
{
event.reply(m, msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
event.getMessage().getChannel().sendMessage(m).queue(msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
CommandLogContext.setMeta(new JSONObject()
.put("playing", true)
.put("title", handler.getPlayer().getPlayingTrack() == null ? JSONObject.NULL : handler.getPlayer().getPlayingTrack().getInfo().title));
}
}
}
@@ -15,6 +15,8 @@
*/
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
@@ -26,15 +28,16 @@ import com.jagrosh.jdautilities.menu.ButtonMenu;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import java.util.concurrent.TimeUnit;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.core.exceptions.PermissionException;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.exceptions.PermissionException;
import org.json.JSONObject;
/**
*
@@ -47,13 +50,14 @@ public class PlayCmd extends MusicCommand
private final String loadingEmoji;
public PlayCmd(Bot bot, String loadingEmoji)
public PlayCmd(Bot bot)
{
super(bot);
this.loadingEmoji = loadingEmoji;
this.loadingEmoji = bot.getConfig().getLoading();
this.name = "play";
this.arguments = "<title|URL|subcommand>";
this.help = "plays the provided song";
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = false;
this.children = new Command[]{new PlaylistCmd(bot)};
@@ -62,24 +66,29 @@ public class PlayCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
if(handler.getPlayer().getPlayingTrack()!=null && handler.getPlayer().isPaused())
{
boolean isDJ = event.getMember().hasPermission(Permission.MANAGE_SERVER);
if(!isDJ)
isDJ = event.isOwner();
Settings settings = event.getClient().getSettingsFor(event.getGuild());
Role dj = settings.getRole(event.getGuild());
if(!isDJ && dj!=null)
isDJ = event.getMember().getRoles().contains(dj);
if(!isDJ)
event.replyError("Only DJs can unpause the player!");
else
if(DJCommand.checkDJPermission(event))
{
handler.getPlayer().setPaused(false);
event.replySuccess("Resumed **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**.");
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject()
.put("position_ms", handler.getPlayer().getPlayingTrack().getPosition());
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"RESUME", null, null, null, null, meta.toString());
logCommandEvent(event, getName(), event.getArgs(), "OK", meta);
}
}
else
{
event.replyError("Only DJs can unpause the player!");
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "not_dj"));
}
return;
}
@@ -89,6 +98,7 @@ public class PlayCmd extends MusicCommand
for(Command cmd: children)
builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ").append(cmd.getName()).append(" ").append(cmd.getArguments()).append("` - ").append(cmd.getHelp());
event.reply(builder.toString());
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject().put("help", true));
return;
}
String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">")
@@ -115,13 +125,17 @@ public class PlayCmd extends MusicCommand
if(bot.getConfig().isTooLong(track))
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+FormatUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
+ TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "PLAY", null, event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "PLAY", pos, event.getArgs(), null);
String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
+"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
if(playlist==null || !event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ADD_REACTION))
m.editMessage(addMsg).queue();
else
@@ -142,6 +156,7 @@ public class PlayCmd extends MusicCommand
try{ m.clearReactions().queue(); }catch(PermissionException ignore) {}
}).build().display(m);
}
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject().put("queue_position", pos));
}
private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude)
@@ -151,7 +166,10 @@ public class PlayCmd extends MusicCommand
if(!bot.getConfig().isTooLong(track) && !track.equals(exclude))
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
handler.addTrack(new QueuedTrack(track, event.getAuthor()));
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "PLAYLIST", playlist == null ? null : playlist.getName(), event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "PLAYLIST", pos, event.getArgs(), playlist == null ? null : playlist.getName());
count[0]++;
}
});
@@ -180,10 +198,17 @@ public class PlayCmd extends MusicCommand
else
{
int count = loadPlaylist(playlist, null);
if(count==0)
if(playlist.getTracks().size() == 0)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" The playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName()
+"**) ")+" could not be loaded or contained 0 entries")).queue();
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "playlist_empty"));
}
else if(count==0)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" All entries in this playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName()
+"**) ")+"were longer than the allowed maximum (`"+bot.getConfig().getMaxTime()+"`)")).queue();
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "playlist_entries_too_long"));
}
else
{
@@ -192,6 +217,7 @@ public class PlayCmd extends MusicCommand
+ playlist.getTracks().size()+"` entries; added to the queue!"
+ (count<playlist.getTracks().size() ? "\n"+event.getClient().getWarning()+" Tracks longer than the allowed maximum (`"
+ bot.getConfig().getMaxTime()+"`) have been omitted." : ""))).queue();
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject().put("playlist_name", playlist.getName()).put("added_count", count));
}
}
}
@@ -200,7 +226,13 @@ public class PlayCmd extends MusicCommand
public void noMatches()
{
if(ytsearch)
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", "no_matches").toString());
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "no_matches"));
}
else
bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+event.getArgs(), new ResultHandler(m,event,true));
}
@@ -212,6 +244,10 @@ public class PlayCmd extends MusicCommand
m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
else
m.editMessage(event.getClient().getError()+" Error loading track.").queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", throwable.getMessage()).toString());
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", throwable.getMessage()));
}
}
@@ -231,21 +267,30 @@ public class PlayCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a playlist name.");
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "missing_playlist"));
return;
}
Playlist playlist = bot.getPlaylistLoader().getPlaylist(event.getArgs());
if(playlist==null)
{
event.replyError("I could not find `"+event.getArgs()+".txt` in the Playlists folder.");
logCommandEvent(event, getName(), event.getArgs(), "ERROR", new JSONObject().put("error_reason", "playlist_not_found"));
return;
}
event.getChannel().sendMessage(loadingEmoji+" Loading playlist **"+event.getArgs()+"**... ("+playlist.getItems().size()+" items)").queue(m ->
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
playlist.loadTracks(bot.getPlayerManager(), (at)->handler.addTrack(new QueuedTrack(at, event.getAuthor())), () -> {
playlist.loadTracks(bot.getPlayerManager(), (at) ->
{
RequestMetadata rm = RequestMetadata.fromResultHandler(at, event, "PLAYLIST", event.getArgs(), event.getArgs());
int pos = handler.addTrack(new QueuedTrack(at, rm))+1;
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), at, "PLAYLIST", pos, event.getArgs(), event.getArgs());
}, () -> {
StringBuilder builder = new StringBuilder(playlist.getTracks().isEmpty()
? event.getClient().getWarning()+" No tracks were loaded!"
: event.getClient().getSuccess()+" Loaded **"+playlist.getTracks().size()+"** tracks!");
@@ -256,8 +301,25 @@ public class PlayCmd extends MusicCommand
if(str.length()>2000)
str = str.substring(0,1994)+" (...)";
m.editMessage(FormatUtil.filter(str)).queue();
if(bot.getDataLogService() != null)
{
playlist.getErrors().forEach(err ->
bot.getDataLogService().logPlaylistError(event.getGuild(), event.getAuthor(), event.getArgs(), err.getItem(), err.getReason()));
}
logCommandEvent(event, getName(), event.getArgs(), "OK", new JSONObject()
.put("playlist_name", event.getArgs())
.put("loaded_count", playlist.getTracks().size())
.put("error_count", playlist.getErrors().size()));
});
});
}
}
private void logCommandEvent(CommandEvent event, String commandName, String args, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), commandName,
args, result, meta == null ? null : meta.toString());
}
}
@@ -19,6 +19,8 @@ import java.util.List;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -31,7 +33,7 @@ public class PlaylistsCmd extends MusicCommand
super(bot);
this.name = "playlists";
this.help = "shows the available playlists";
this.aliases = new String[]{"pls"};
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = true;
this.beListening = false;
this.beListening = false;
@@ -45,19 +47,27 @@ public class PlaylistsCmd extends MusicCommand
if(!bot.getPlaylistLoader().folderExists())
{
event.reply(event.getClient().getWarning()+" Playlists folder does not exist and could not be created!");
CommandLogContext.setError("playlist_folder_missing");
return;
}
List<String> list = bot.getPlaylistLoader().getPlaylistNames();
if(list==null)
{
event.reply(event.getClient().getError()+" Failed to load available playlists!");
CommandLogContext.setError("playlist_list_failed");
}
else if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" There are no playlists in the Playlists folder!");
CommandLogContext.setMeta(new JSONObject().put("count", 0));
}
else
{
StringBuilder builder = new StringBuilder(event.getClient().getSuccess()+" Available playlists:\n");
list.forEach(str -> builder.append("`").append(str).append("` "));
builder.append("\nType `").append(event.getClient().getTextualPrefix()).append("play playlist <name>` to play a playlist");
event.reply(builder.toString());
CommandLogContext.setMeta(new JSONObject().put("count", list.size()));
}
}
}
@@ -20,16 +20,21 @@ import java.util.concurrent.TimeUnit;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.menu.Paginator;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.JMusicBot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.core.MessageBuilder;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.core.exceptions.PermissionException;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.exceptions.PermissionException;
import org.json.JSONObject;
/**
*
@@ -37,8 +42,6 @@ import net.dv8tion.jda.core.exceptions.PermissionException;
*/
public class QueueCmd extends MusicCommand
{
private final static String REPEAT = "\uD83D\uDD01"; // 🔁
private final Paginator.Builder builder;
public QueueCmd(Bot bot)
@@ -47,7 +50,7 @@ public class QueueCmd extends MusicCommand
this.name = "queue";
this.help = "shows the current queue";
this.arguments = "[pagenum]";
this.aliases = new String[]{"list"};
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = true;
this.botPermissions = new Permission[]{Permission.MESSAGE_ADD_REACTION,Permission.MESSAGE_EMBED_LINKS};
builder = new Paginator.Builder()
@@ -57,6 +60,7 @@ public class QueueCmd extends MusicCommand
.waitOnSinglePage(false)
.useNumberedItems(true)
.showPageNumbers(true)
.wrapPageEnds(true)
.setEventWaiter(bot.getWaiter())
.setTimeout(1, TimeUnit.MINUTES);
}
@@ -74,16 +78,17 @@ public class QueueCmd extends MusicCommand
List<QueuedTrack> list = ah.getQueue().getList();
if(list.isEmpty())
{
Message nowp = ah.getNowPlaying(event.getJDA());
Message nonowp = ah.getNoMusicPlaying(event.getJDA());
Message built = new MessageBuilder()
MessageCreateData nowp = ah.getNowPlaying(event.getJDA());
MessageCreateData nonowp = ah.getNoMusicPlaying(event.getJDA());
MessageCreateData built = new MessageCreateBuilder()
.setContent(event.getClient().getWarning() + " There is no music in the queue!")
.setEmbed((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build();
event.reply(built, m ->
.setEmbeds((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build();
event.getMessage().getChannel().sendMessage(built).queue(m ->
{
if(nowp!=null)
bot.getNowplayingHandler().setLastNPMessage(m);
});
CommandLogContext.setMeta(new JSONObject().put("queue_size", 0));
return;
}
String[] songs = new String[list.size()];
@@ -95,7 +100,12 @@ public class QueueCmd extends MusicCommand
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
long fintotal = total;
builder.setText((i1,i2) -> getQueueTitle(ah, event.getClient().getSuccess(), songs.length, fintotal, settings.getRepeatMode()))
CommandLogContext.setMeta(new JSONObject()
.put("queue_size", list.size())
.put("total_ms", total)
.put("repeat_mode", settings.getRepeatMode().name())
.put("queue_type", settings.getQueueType().name()));
builder.setText((i1,i2) -> getQueueTitle(ah, event.getClient().getSuccess(), songs.length, fintotal, settings.getRepeatMode(), settings.getQueueType()))
.setItems(songs)
.setUsers(event.getAuthor())
.setColor(event.getSelfMember().getColor())
@@ -103,16 +113,17 @@ public class QueueCmd extends MusicCommand
builder.build().paginate(event.getChannel(), pagenum);
}
private String getQueueTitle(AudioHandler ah, String success, int songslength, long total, boolean repeatmode)
private String getQueueTitle(AudioHandler ah, String success, int songslength, long total, RepeatMode repeatmode, QueueType queueType)
{
StringBuilder sb = new StringBuilder();
if(ah.getPlayer().getPlayingTrack()!=null)
{
sb.append(ah.getPlayer().isPaused() ? JMusicBot.PAUSE_EMOJI : JMusicBot.PLAY_EMOJI).append(" **")
sb.append(ah.getStatusEmoji()).append(" **")
.append(ah.getPlayer().getPlayingTrack().getInfo().title).append("**\n");
}
return FormatUtil.filter(sb.append(success).append(" Current Queue | ").append(songslength)
.append(" entries | `").append(FormatUtil.formatTime(total)).append("` ")
.append(repeatmode ? "| " + REPEAT : "").toString());
.append(" entries | `").append(TimeUtil.formatTime(total)).append("` ")
.append("| ").append(queueType.getEmoji()).append(" `").append(queueType.getUserFriendlyName()).append('`')
.append(repeatmode.getEmoji() != null ? " | "+repeatmode.getEmoji() : "").toString());
}
}
@@ -20,9 +20,11 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.User;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.User;
import org.json.JSONObject;
/**
*
@@ -36,7 +38,7 @@ public class RemoveCmd extends MusicCommand
this.name = "remove";
this.help = "removes a song from the queue";
this.arguments = "<position|ALL>";
this.aliases = new String[]{"delete"};
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = true;
}
@@ -48,15 +50,28 @@ public class RemoveCmd extends MusicCommand
if(handler.getQueue().isEmpty())
{
event.replyError("There is nothing in the queue!");
CommandLogContext.setError("empty_queue");
return;
}
if(event.getArgs().equalsIgnoreCase("all"))
{
int sizeBefore = handler.getQueue().size();
int count = handler.getQueue().removeAll(event.getAuthor().getIdLong());
if(count==0)
{
event.replyWarning("You don't have any songs in the queue!");
CommandLogContext.setError("no_entries_for_user");
}
else
event.replySuccess("Successfully removed your "+count+" entries.");
JSONObject meta = new JSONObject()
.put("removed_count", count)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), null,
"REMOVE_ALL", "SELF", null, null, null, meta.toString());
return;
}
int pos;
@@ -68,6 +83,7 @@ public class RemoveCmd extends MusicCommand
if(pos<1 || pos>handler.getQueue().size())
{
event.replyError("Position must be a valid integer between 1 and "+handler.getQueue().size()+"!");
CommandLogContext.setError("invalid_position");
return;
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
@@ -79,6 +95,11 @@ public class RemoveCmd extends MusicCommand
{
handler.getQueue().remove(pos-1);
event.replySuccess("Removed **"+qt.getTrack().getInfo().title+"** from the queue");
JSONObject meta = new JSONObject().put("position", pos);
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), qt.getTrack(),
"REMOVE", "SELF", pos, null, null, meta.toString());
}
else if(isDJ)
{
@@ -91,10 +112,16 @@ public class RemoveCmd extends MusicCommand
}
event.replySuccess("Removed **"+qt.getTrack().getInfo().title
+"** from the queue (requested by "+(u==null ? "someone" : "**"+u.getName()+"**")+")");
JSONObject meta = new JSONObject().put("position", pos).put("removed_by_dj", true);
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), qt.getTrack(),
"REMOVE", "DJ", pos, null, null, meta.toString());
}
else
{
event.replyError("You cannot remove **"+qt.getTrack().getInfo().title+"** because you didn't add it!");
CommandLogContext.setError("not_owner");
}
}
}
@@ -23,12 +23,12 @@ import com.jagrosh.jmusicbot.Bot;
*/
public class SCSearchCmd extends SearchCmd
{
public SCSearchCmd(Bot bot, String searchingEmoji)
public SCSearchCmd(Bot bot)
{
super(bot, searchingEmoji);
super(bot);
this.searchPrefix = "scsearch:";
this.name = "scsearch";
this.help = "searches Soundcloud for a provided query";
this.aliases = new String[]{};
this.aliases = bot.getConfig().getAliases(this.name);
}
}
@@ -15,6 +15,8 @@
*/
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
@@ -27,9 +29,11 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.core.Permission;
import net.dv8tion.jda.core.entities.Message;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import org.json.JSONObject;
/**
*
@@ -41,12 +45,12 @@ public class SearchCmd extends MusicCommand
private final OrderedMenu.Builder builder;
private final String searchingEmoji;
public SearchCmd(Bot bot, String searchingEmoji)
public SearchCmd(Bot bot)
{
super(bot);
this.searchingEmoji = searchingEmoji;
this.searchingEmoji = bot.getConfig().getSearching();
this.name = "search";
this.aliases = new String[]{"ytsearch"};
this.aliases = bot.getConfig().getAliases(this.name);
this.arguments = "<query>";
this.help = "searches Youtube for a provided query";
this.beListening = true;
@@ -62,9 +66,11 @@ public class SearchCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
if(event.getArgs().isEmpty())
{
event.replyError("Please include a query.");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "missing_query"));
return;
}
event.reply(searchingEmoji+" Searching... `["+event.getArgs()+"]`",
@@ -88,14 +94,22 @@ public class SearchCmd extends MusicCommand
if(bot.getConfig().isTooLong(track))
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue();
+ TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue();
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1;
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "SEARCH", null, event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
{
bot.getDataLogService().logSearchSelection(event.getGuild(), event.getAuthor(), event.getArgs(), 1, track);
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "SEARCH", pos, event.getArgs(), null);
}
m.editMessage(FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing"
+"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing"
: " to the queue at position "+pos))).queue();
logCommandEvent(event, "OK", new JSONObject().put("queue_position", pos).put("selected_index", 1));
}
@Override
@@ -110,13 +124,20 @@ public class SearchCmd extends MusicCommand
if(bot.getConfig().isTooLong(track))
{
event.replyWarning("This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+FormatUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`");
+ TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "track_too_long"));
return;
}
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int pos = handler.addTrack(new QueuedTrack(track, event.getAuthor()))+1;
event.replySuccess("Added **"+track.getInfo().title
+"** (`"+FormatUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing"
RequestMetadata rm = RequestMetadata.fromResultHandler(track, event, "SEARCH", null, event.getArgs());
int pos = handler.addTrack(new QueuedTrack(track, rm))+1;
if(bot.getDataLogService() != null)
{
bot.getDataLogService().logSearchSelection(event.getGuild(), event.getAuthor(), event.getArgs(), i, track);
bot.getDataLogService().logQueueAdd(event.getGuild(), event.getAuthor(), track, "SEARCH", pos, event.getArgs(), null);
}
event.replySuccess("Added **" + FormatUtil.filter(track.getInfo().title)
+ "** (`" + TimeUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing"
: " to the queue at position "+pos));
})
.setCancel((msg) -> {})
@@ -125,15 +146,20 @@ public class SearchCmd extends MusicCommand
for(int i=0; i<4 && i<playlist.getTracks().size(); i++)
{
AudioTrack track = playlist.getTracks().get(i);
builder.addChoices("`["+FormatUtil.formatTime(track.getDuration())+"]` [**"+track.getInfo().title+"**]("+track.getInfo().uri+")");
builder.addChoices("`["+ TimeUtil.formatTime(track.getDuration())+"]` [**"+track.getInfo().title+"**]("+track.getInfo().uri+")");
}
builder.build().display(m);
logCommandEvent(event, "OK", new JSONObject().put("result_count", playlist.getTracks().size()));
}
@Override
public void noMatches()
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", "no_matches").toString());
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_matches"));
}
@Override
@@ -143,6 +169,18 @@ public class SearchCmd extends MusicCommand
m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
else
m.editMessage(event.getClient().getError()+" Error loading track.").queue();
if(bot.getDataLogService() != null)
bot.getDataLogService().logSearchEventWithMeta(event.getGuild(), event.getAuthor(), event.getArgs(), null, null,
new JSONObject().put("error_reason", throwable.getMessage()).toString());
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", throwable.getMessage()));
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2020 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.DJCommand;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.json.JSONObject;
/**
* @author Whew., Inc.
*/
public class SeekCmd extends MusicCommand
{
private final static Logger LOG = LoggerFactory.getLogger("Seeking");
public SeekCmd(Bot bot)
{
super(bot);
this.name = "seek";
this.help = "seeks the current song";
this.arguments = "[+ | -] <HH:MM:SS | MM:SS | SS>|<0h0m0s | 0m0s | 0s>";
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = true;
}
@Override
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
AudioTrack playingTrack = handler.getPlayer().getPlayingTrack();
if (!playingTrack.isSeekable())
{
event.replyError("This track is not seekable.");
CommandLogContext.setError("not_seekable");
return;
}
if (!DJCommand.checkDJPermission(event) && playingTrack.getUserData(RequestMetadata.class).getOwner() != event.getAuthor().getIdLong())
{
event.replyError("You cannot seek **" + playingTrack.getInfo().title + "** because you didn't add it!");
CommandLogContext.setError("not_owner");
return;
}
String args = event.getArgs();
TimeUtil.SeekTime seekTime = TimeUtil.parseTime(args);
if (seekTime == null)
{
event.replyError("Invalid seek! Expected format: " + arguments + "\nExamples: `1:02:23` `+1:10` `-90`, `1h10m`, `+90s`");
CommandLogContext.setError("invalid_seek");
return;
}
long currentPosition = playingTrack.getPosition();
long trackDuration = playingTrack.getDuration();
long seekMilliseconds = seekTime.relative ? currentPosition + seekTime.milliseconds : seekTime.milliseconds;
if (seekMilliseconds > trackDuration)
{
event.replyError("Cannot seek to `" + TimeUtil.formatTime(seekMilliseconds) + "` because the current track is `" + TimeUtil.formatTime(trackDuration) + "` long!");
CommandLogContext.setError("seek_out_of_bounds");
return;
}
try
{
playingTrack.setPosition(seekMilliseconds);
}
catch (Exception e)
{
event.replyError("An error occurred while trying to seek: " + e.getMessage());
LOG.warn("Failed to seek track " + playingTrack.getIdentifier(), e);
CommandLogContext.setError("seek_failed");
return;
}
event.replySuccess("Successfully seeked to `" + TimeUtil.formatTime(playingTrack.getPosition()) + "/" + TimeUtil.formatTime(playingTrack.getDuration()) + "`!");
JSONObject meta = new JSONObject()
.put("seek_from_ms", currentPosition)
.put("seek_to_ms", playingTrack.getPosition());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), playingTrack,
"SEEK", null, null, null, null, meta.toString());
}
}
@@ -19,6 +19,8 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -31,6 +33,7 @@ public class ShuffleCmd extends MusicCommand
super(bot);
this.name = "shuffle";
this.help = "shuffles songs you have added";
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = true;
}
@@ -39,17 +42,28 @@ public class ShuffleCmd extends MusicCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
int sizeBefore = handler.getQueue().size();
int s = handler.getQueue().shuffle(event.getAuthor().getIdLong());
switch (s)
{
case 0:
event.replyError("You don't have any music in the queue to shuffle!");
CommandLogContext.setError("no_entries_to_shuffle");
break;
case 1:
event.replyWarning("You only have one song in the queue!");
CommandLogContext.setMeta(new JSONObject().put("shuffled_count", 1));
break;
default:
event.replySuccess("You successfully shuffled your "+s+" entries.");
JSONObject meta = new JSONObject()
.put("shuffled_count", s)
.put("queue_size_before", sizeBefore)
.put("queue_size_after", handler.getQueue().size());
CommandLogContext.setMeta(meta);
if(bot.getDataLogService() != null)
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), null,
"SHUFFLE", null, null, null, null, meta.toString());
break;
}
}
@@ -18,8 +18,11 @@ package com.jagrosh.jmusicbot.commands.music;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import net.dv8tion.jda.core.entities.User;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -32,7 +35,7 @@ public class SkipCmd extends MusicCommand
super(bot);
this.name = "skip";
this.help = "votes to skip the current song";
this.aliases = new String[]{"voteskip"};
this.aliases = bot.getConfig().getAliases(this.name);
this.beListening = true;
this.bePlaying = true;
}
@@ -41,9 +44,21 @@ public class SkipCmd extends MusicCommand
public void doCommand(CommandEvent event)
{
AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
if(event.getAuthor().getIdLong()==handler.getRequester())
RequestMetadata rm = handler.getRequestMetadata();
double skipRatio = bot.getSettingsManager().getSettings(event.getGuild()).getSkipRatio();
if(skipRatio == -1) {
skipRatio = bot.getConfig().getSkipRatio();
}
if(event.getAuthor().getIdLong() == rm.getOwner() || skipRatio == 0)
{
event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**");
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject().put("reason", "REQUESTER");
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_REQUESTER", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.getPlayer().stopTrack();
}
else
@@ -57,16 +72,36 @@ public class SkipCmd extends MusicCommand
{
msg = event.getClient().getSuccess()+" You voted to skip the song `[";
handler.getVotes().add(event.getAuthor().getId());
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject().put("vote", "added");
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_VOTE", null, null, null, null, meta.toString());
}
}
int skippers = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream()
.filter(m -> handler.getVotes().contains(m.getUser().getId())).count();
int required = (int)Math.ceil(listeners * .55);
msg+= skippers+" votes, "+required+"/"+listeners+" needed]`";
int required = (int)Math.ceil(listeners * skipRatio);
msg += skippers + " votes, " + required + "/" + listeners + " needed]`";
CommandLogContext.setMeta(new JSONObject()
.put("votes", skippers)
.put("required", required)
.put("listeners", listeners));
if(skippers>=required)
{
User u = event.getJDA().getUserById(handler.getRequester());
msg+="\n"+event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title
+"**"+(handler.getRequester()==0 ? "" : " (requested by "+(u==null ? "someone" : "**"+u.getName()+"**")+")");
msg += "\n" + event.getClient().getSuccess() + " Skipped **" + handler.getPlayer().getPlayingTrack().getInfo().title
+ "** " + (rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)");
if(bot.getDataLogService() != null)
{
JSONObject meta = new JSONObject()
.put("reason", "VOTE")
.put("votes", skippers)
.put("required", required)
.put("listeners", listeners);
bot.getDataLogService().logQueueEventWithMeta(event.getGuild(), event.getAuthor(), handler.getPlayer().getPlayingTrack(),
"SKIP_VOTE_ACTION", null, null, null, null, meta.toString());
CommandLogContext.setMeta(meta);
}
handler.getPlayer().stopTrack();
}
event.reply(msg);
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -26,42 +28,45 @@ import com.jagrosh.jmusicbot.settings.Settings;
*/
public class AutoplaylistCmd extends OwnerCommand
{
private final Bot bot;
public AutoplaylistCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.guildOnly = true;
this.name = "autoplaylist";
this.arguments = "<name|NONE>";
this.help = "sets the default playlist for the server";
this.aliases = bot.getConfig().getAliases(this.name);
}
@Override
public void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.reply(event.getClient().getError()+" Please include a playlist name or NONE");
CommandLogContext.setError("missing_playlist");
return;
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
String before = settings.getDefaultPlaylist();
if(event.getArgs().equalsIgnoreCase("none"))
{
Settings settings = event.getClient().getSettingsFor(event.getGuild());
settings.setDefaultPlaylist(null);
event.reply(event.getClient().getSuccess()+" Cleared the default playlist for **"+event.getGuild().getName()+"**");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", JSONObject.NULL));
return;
}
String pname = event.getArgs().replaceAll("\\s+", "_");
if(bot.getPlaylistLoader().getPlaylist(pname)==null)
{
event.reply(event.getClient().getError()+" Could not find `"+pname+".txt`!");
CommandLogContext.setError("playlist_not_found");
}
else
{
Settings settings = event.getClient().getSettingsFor(event.getGuild());
settings.setDefaultPlaylist(pname);
event.reply(event.getClient().getSuccess()+" The default playlist for **"+event.getGuild().getName()+"** is now `"+pname+"`");
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", pname));
}
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2017 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary;
import net.dv8tion.jda.api.JDAInfo;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.utils.FileUpload;
import org.json.JSONObject;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class DebugCmd extends OwnerCommand
{
private final static String[] PROPERTIES = {"java.version", "java.vm.name", "java.vm.specification.version",
"java.runtime.name", "java.runtime.version", "java.specification.version", "os.arch", "os.name"};
public DebugCmd(Bot bot)
{
super(bot);
this.name = "debug";
this.help = "shows debug info";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = false;
}
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setMeta(new JSONObject().put("channel_type", event.getChannelType().name()));
StringBuilder sb = new StringBuilder();
sb.append("```\nSystem Properties:");
for(String key: PROPERTIES)
sb.append("\n ").append(key).append(" = ").append(System.getProperty(key));
sb.append("\n\nJMusicBot Information:")
.append("\n Version = ").append(OtherUtil.getCurrentVersion())
.append("\n Owner = ").append(bot.getConfig().getOwnerId())
.append("\n Prefix = ").append(bot.getConfig().getPrefix())
.append("\n AltPrefix = ").append(bot.getConfig().getAltPrefix())
.append("\n MaxSeconds = ").append(bot.getConfig().getMaxSeconds())
.append("\n NPImages = ").append(bot.getConfig().useNPImages())
.append("\n SongInStatus = ").append(bot.getConfig().getSongInStatus())
.append("\n StayInChannel = ").append(bot.getConfig().getStay())
.append("\n UseEval = ").append(bot.getConfig().useEval())
.append("\n UpdateAlerts = ").append(bot.getConfig().useUpdateAlerts());
sb.append("\n\nDependency Information:")
.append("\n JDA Version = ").append(JDAInfo.VERSION)
.append("\n JDA-Utilities Version = ").append(OtherUtil.getCommandLibraryVersion())
.append("\n Lavaplayer Version = ").append(PlayerLibrary.VERSION);
long total = Runtime.getRuntime().totalMemory() / 1024 / 1024;
long used = total - (Runtime.getRuntime().freeMemory() / 1024 / 1024);
sb.append("\n\nRuntime Information:")
.append("\n Total Memory = ").append(total)
.append("\n Used Memory = ").append(used);
sb.append("\n\nDiscord Information:")
.append("\n ID = ").append(event.getJDA().getSelfUser().getId())
.append("\n Guilds = ").append(event.getJDA().getGuildCache().size())
.append("\n Users = ").append(event.getJDA().getUserCache().size());
sb.append("\n```");
if(event.isFromType(ChannelType.PRIVATE)
|| event.getSelfMember().hasPermission(event.getGuildChannel(), Permission.MESSAGE_ATTACH_FILES))
event.getChannel().sendFiles(FileUpload.fromData(sb.toString().getBytes(), "debug_information.txt")).queue();
else
event.reply("Debug Information: " + sb.toString());
}
}
@@ -20,6 +20,9 @@ import javax.script.ScriptEngineManager;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import org.json.JSONObject;
/**
*
@@ -27,33 +30,55 @@ import com.jagrosh.jmusicbot.commands.OwnerCommand;
*/
public class EvalCmd extends OwnerCommand
{
private final Bot bot;
private final String engine;
public EvalCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.name = "eval";
this.help = "evaluates nashorn code";
this.aliases = bot.getConfig().getAliases(this.name);
this.engine = bot.getConfig().getEvalEngine();
this.guildOnly = false;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
ScriptEngine se = new ScriptEngineManager().getEngineByName("Nashorn");
CommandLogContext.setManualLogging();
ScriptEngine se = new ScriptEngineManager().getEngineByName(engine);
if(se == null)
{
event.replyError("The eval engine provided in the config (`"+engine+"`) doesn't exist. This could be due to an invalid "
+ "engine name, or the engine not existing in your version of java (`"+System.getProperty("java.version")+"`).");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "engine_not_found"));
return;
}
se.put("bot", bot);
se.put("event", event);
se.put("jda", event.getJDA());
se.put("guild", event.getGuild());
se.put("channel", event.getChannel());
if (event.getChannelType() != ChannelType.PRIVATE) {
se.put("guild", event.getGuild());
se.put("channel", event.getChannel());
}
try
{
event.reply(event.getClient().getSuccess()+" Evaluated Successfully:\n```\n"+se.eval(event.getArgs())+" ```");
logCommandEvent(event, "OK", new JSONObject().put("invoked", true));
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" An exception was thrown:\n```\n"+e+" ```");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "eval_exception"));
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
null, result, meta == null ? null : meta.toString());
}
}
@@ -17,12 +17,12 @@ package com.jagrosh.jmusicbot.commands.owner;
import java.io.IOException;
import java.util.List;
import com.jagrosh.jdautilities.command.Command;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.commands.owner.AutoplaylistCmd;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import org.json.JSONObject;
/**
*
@@ -30,14 +30,14 @@ import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
*/
public class PlaylistCmd extends OwnerCommand
{
private final Bot bot;
public PlaylistCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.guildOnly = false;
this.name = "playlist";
this.arguments = "<append|delete|make|setdefault>";
this.help = "playlist management";
this.aliases = bot.getConfig().getAliases(this.name);
this.children = new OwnerCommand[]{
new ListCmd(),
new AppendlistCmd(),
@@ -48,19 +48,21 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
public void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Playlist Management Commands:\n");
for(Command cmd: this.children)
for(com.jagrosh.jdautilities.command.Command cmd: this.children)
builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ").append(cmd.getName())
.append(" ").append(cmd.getArguments()==null ? "" : cmd.getArguments()).append("` - ").append(cmd.getHelp());
event.reply(builder.toString());
CommandLogContext.setMeta(new JSONObject().put("subcommands", this.children.length));
}
public class MakelistCmd extends OwnerCommand
{
public MakelistCmd()
{
super(PlaylistCmd.this.bot);
this.name = "make";
this.aliases = new String[]{"create"};
this.help = "makes a new playlist";
@@ -69,23 +71,34 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String pname = event.getArgs().replaceAll("\\s+", "_");
if(bot.getPlaylistLoader().getPlaylist(pname)==null)
pname = pname.replaceAll("[*?|\\/\":<>]", "");
if(pname == null || pname.isEmpty())
{
event.replyError("Please provide a name for the playlist!");
CommandLogContext.setError("missing_playlist_name");
}
else if(bot.getPlaylistLoader().getPlaylist(pname) == null)
{
try
{
bot.getPlaylistLoader().createPlaylist(pname);
event.reply(event.getClient().getSuccess()+" Successfully created playlist `"+pname+"`!");
CommandLogContext.setMeta(new JSONObject().put("playlist_name", pname));
}
catch(IOException e)
{
event.reply(event.getClient().getError()+" I was unable to create the playlist: "+e.getLocalizedMessage());
CommandLogContext.setError("create_playlist_failed");
}
}
else
{
event.reply(event.getClient().getError()+" Playlist `"+pname+"` already exists!");
CommandLogContext.setError("playlist_exists");
}
}
}
@@ -93,6 +106,7 @@ public class PlaylistCmd extends OwnerCommand
{
public DeletelistCmd()
{
super(PlaylistCmd.this.bot);
this.name = "delete";
this.aliases = new String[]{"remove"};
this.help = "deletes an existing playlist";
@@ -101,21 +115,26 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String pname = event.getArgs().replaceAll("\\s+", "_");
if(bot.getPlaylistLoader().getPlaylist(pname)==null)
{
event.reply(event.getClient().getError()+" Playlist `"+pname+"` doesn't exist!");
CommandLogContext.setError("playlist_not_found");
}
else
{
try
{
bot.getPlaylistLoader().deletePlaylist(pname);
event.reply(event.getClient().getSuccess()+" Successfully deleted playlist `"+pname+"`!");
CommandLogContext.setMeta(new JSONObject().put("playlist_name", pname));
}
catch(IOException e)
{
event.reply(event.getClient().getError()+" I was unable to delete the playlist: "+e.getLocalizedMessage());
CommandLogContext.setError("delete_playlist_failed");
}
}
}
@@ -125,6 +144,7 @@ public class PlaylistCmd extends OwnerCommand
{
public AppendlistCmd()
{
super(PlaylistCmd.this.bot);
this.name = "append";
this.aliases = new String[]{"add"};
this.help = "appends songs to an existing playlist";
@@ -133,18 +153,22 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String[] parts = event.getArgs().split("\\s+", 2);
if(parts.length<2)
{
event.reply(event.getClient().getError()+" Please include a playlist name and URLs to add!");
CommandLogContext.setError("missing_append_args");
return;
}
String pname = parts[0];
Playlist playlist = bot.getPlaylistLoader().getPlaylist(pname);
if(playlist==null)
{
event.reply(event.getClient().getError()+" Playlist `"+pname+"` doesn't exist!");
CommandLogContext.setError("playlist_not_found");
}
else
{
StringBuilder builder = new StringBuilder();
@@ -161,10 +185,12 @@ public class PlaylistCmd extends OwnerCommand
{
bot.getPlaylistLoader().writePlaylist(pname, builder.toString());
event.reply(event.getClient().getSuccess()+" Successfully added "+urls.length+" items to playlist `"+pname+"`!");
CommandLogContext.setMeta(new JSONObject().put("playlist_name", pname).put("added_count", urls.length));
}
catch(IOException e)
{
event.reply(event.getClient().getError()+" I was unable to append to the playlist: "+e.getLocalizedMessage());
CommandLogContext.setError("append_playlist_failed");
}
}
}
@@ -186,6 +212,7 @@ public class PlaylistCmd extends OwnerCommand
{
public ListCmd()
{
super(PlaylistCmd.this.bot);
this.name = "all";
this.aliases = new String[]{"available","list"};
this.help = "lists all available playlists";
@@ -193,25 +220,33 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(!bot.getPlaylistLoader().folderExists())
bot.getPlaylistLoader().createFolder();
if(!bot.getPlaylistLoader().folderExists())
{
event.reply(event.getClient().getWarning()+" Playlists folder does not exist and could not be created!");
CommandLogContext.setError("playlist_folder_missing");
return;
}
List<String> list = bot.getPlaylistLoader().getPlaylistNames();
if(list==null)
{
event.reply(event.getClient().getError()+" Failed to load available playlists!");
CommandLogContext.setError("playlist_list_failed");
}
else if(list.isEmpty())
{
event.reply(event.getClient().getWarning()+" There are no playlists in the Playlists folder!");
CommandLogContext.setMeta(new JSONObject().put("count", 0));
}
else
{
StringBuilder builder = new StringBuilder(event.getClient().getSuccess()+" Available playlists:\n");
list.forEach(str -> builder.append("`").append(str).append("` "));
event.reply(builder.toString());
CommandLogContext.setMeta(new JSONObject().put("count", list.size()));
}
}
}
@@ -18,9 +18,12 @@ package com.jagrosh.jmusicbot.commands.owner;
import java.io.IOException;
import java.io.InputStream;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import net.dv8tion.jda.core.entities.Icon;
import net.dv8tion.jda.api.entities.Icon;
import org.json.JSONObject;
/**
*
@@ -28,17 +31,20 @@ import net.dv8tion.jda.core.entities.Icon;
*/
public class SetavatarCmd extends OwnerCommand
{
public SetavatarCmd()
public SetavatarCmd(Bot bot)
{
super(bot);
this.name = "setavatar";
this.help = "sets the avatar of the bot";
this.arguments = "<url>";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = false;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
String url;
if(event.getArgs().isEmpty())
if(!event.getMessage().getAttachments().isEmpty() && event.getMessage().getAttachments().get(0).isImage())
@@ -51,16 +57,33 @@ public class SetavatarCmd extends OwnerCommand
if(s==null)
{
event.reply(event.getClient().getError()+" Invalid or missing URL");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "invalid_avatar_url"));
}
else
{
try {
event.getSelfUser().getManager().setAvatar(Icon.from(s)).queue(
v -> event.reply(event.getClient().getSuccess()+" Successfully changed avatar."),
t -> event.reply(event.getClient().getError()+" Failed to set avatar."));
v -> {
event.reply(event.getClient().getSuccess()+" Successfully changed avatar.");
logCommandEvent(event, "OK", new JSONObject()
.put("source", event.getArgs().isEmpty() ? "attachment" : "url"));
},
t -> {
event.reply(event.getClient().getError()+" Failed to set avatar.");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "avatar_set_failed"));
});
} catch(IOException e) {
event.reply(event.getClient().getError()+" Could not load from provided URL.");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "avatar_load_failed"));
}
}
}
private void logCommandEvent(CommandEvent event, String result, JSONObject meta)
{
if(bot.getDataLogService() == null || event == null || event.getGuild() == null)
return;
bot.getDataLogService().logCommandEvent(event.getGuild(), event.getAuthor(), getName(),
event.getArgs(), result, meta == null ? null : meta.toString());
}
}
@@ -16,8 +16,11 @@
package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import net.dv8tion.jda.core.entities.Game;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.entities.Activity;
import org.json.JSONObject;
/**
*
@@ -25,11 +28,13 @@ import net.dv8tion.jda.core.entities.Game;
*/
public class SetgameCmd extends OwnerCommand
{
public SetgameCmd()
public SetgameCmd(Bot bot)
{
super(bot);
this.name = "setgame";
this.help = "sets the game the bot is playing";
this.arguments = "[action] [game]";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = false;
this.children = new OwnerCommand[]{
new SetlistenCmd(),
@@ -39,18 +44,22 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String title = event.getArgs().toLowerCase().startsWith("playing") ? event.getArgs().substring(7).trim() : event.getArgs();
try
{
event.getJDA().getPresence().setGame(title.isEmpty() ? null : Game.playing(title));
event.getJDA().getPresence().setActivity(title.isEmpty() ? null : Activity.playing(title));
event.reply(event.getClient().getSuccess()+" **"+event.getSelfUser().getName()
+"** is "+(title.isEmpty() ? "no longer playing anything." : "now playing `"+title+"`"));
CommandLogContext.setMeta(new JSONObject()
.put("activity_type", "PLAYING")
.put("title", title.isEmpty() ? JSONObject.NULL : title));
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
@@ -58,6 +67,7 @@ public class SetgameCmd extends OwnerCommand
{
private SetstreamCmd()
{
super(SetgameCmd.this.bot);
this.name = "stream";
this.aliases = new String[]{"twitch","streaming"};
this.help = "sets the game the bot is playing to a stream";
@@ -66,23 +76,29 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String[] parts = event.getArgs().split("\\s+", 2);
if(parts.length<2)
{
event.replyError("Please include a twitch username and the name of the game to 'stream'");
CommandLogContext.setError("missing_stream_args");
return;
}
try
{
event.getJDA().getPresence().setGame(Game.streaming(parts[1], "https://twitch.tv/"+parts[0]));
event.getJDA().getPresence().setActivity(Activity.streaming(parts[1], "https://twitch.tv/"+parts[0]));
event.replySuccess("**"+event.getSelfUser().getName()
+"** is now streaming `"+parts[1]+"`");
CommandLogContext.setMeta(new JSONObject()
.put("activity_type", "STREAMING")
.put("title", parts[1])
.put("user", parts[0]));
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
}
@@ -91,6 +107,7 @@ public class SetgameCmd extends OwnerCommand
{
private SetlistenCmd()
{
super(SetgameCmd.this.bot);
this.name = "listen";
this.aliases = new String[]{"listening"};
this.help = "sets the game the bot is listening to";
@@ -99,20 +116,23 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.replyError("Please include a title to listen to!");
CommandLogContext.setError("missing_listen_title");
return;
}
String title = event.getArgs().toLowerCase().startsWith("to") ? event.getArgs().substring(2).trim() : event.getArgs();
try
{
event.getJDA().getPresence().setGame(Game.listening(title));
event.getJDA().getPresence().setActivity(Activity.listening(title));
event.replySuccess("**"+event.getSelfUser().getName()+"** is now listening to `"+title+"`");
CommandLogContext.setMeta(new JSONObject().put("activity_type", "LISTENING").put("title", title));
} catch(Exception e) {
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
}
@@ -121,6 +141,7 @@ public class SetgameCmd extends OwnerCommand
{
private SetwatchCmd()
{
super(SetgameCmd.this.bot);
this.name = "watch";
this.aliases = new String[]{"watching"};
this.help = "sets the game the bot is watching";
@@ -129,20 +150,23 @@ public class SetgameCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
if(event.getArgs().isEmpty())
{
event.replyError("Please include a title to watch!");
CommandLogContext.setError("missing_watch_title");
return;
}
String title = event.getArgs();
try
{
event.getJDA().getPresence().setGame(Game.watching(title));
event.getJDA().getPresence().setActivity(Activity.watching(title));
event.replySuccess("**"+event.getSelfUser().getName()+"** is now watching `"+title+"`");
CommandLogContext.setMeta(new JSONObject().put("activity_type", "WATCHING").put("title", title));
} catch(Exception e) {
event.reply(event.getClient().getError()+" The game could not be set!");
CommandLogContext.setError("set_game_failed");
}
}
}
@@ -18,7 +18,9 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import net.dv8tion.jda.core.exceptions.RateLimitedException;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.exceptions.RateLimitedException;
import org.json.JSONObject;
/**
*
@@ -26,30 +28,35 @@ import net.dv8tion.jda.core.exceptions.RateLimitedException;
*/
public class SetnameCmd extends OwnerCommand
{
public SetnameCmd()
public SetnameCmd(Bot bot)
{
super(bot);
this.name = "setname";
this.help = "sets the name of the bot";
this.arguments = "<name>";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = false;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
try
{
String oldname = event.getSelfUser().getName();
event.getSelfUser().getManager().setName(event.getArgs()).complete(false);
event.reply(event.getClient().getSuccess()+" Name changed from `"+oldname+"` to `"+event.getArgs()+"`");
CommandLogContext.setMeta(new JSONObject().put("before", oldname).put("after", event.getArgs()));
}
catch(RateLimitedException e)
{
event.reply(event.getClient().getError()+" Name can only be changed twice per hour!");
CommandLogContext.setError("rate_limited");
}
catch(Exception e)
{
event.reply(event.getClient().getError()+" That name is not valid!");
CommandLogContext.setError("invalid_name");
}
}
}
@@ -16,8 +16,11 @@
package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import net.dv8tion.jda.core.OnlineStatus;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.OnlineStatus;
import org.json.JSONObject;
/**
*
@@ -25,30 +28,38 @@ import net.dv8tion.jda.core.OnlineStatus;
*/
public class SetstatusCmd extends OwnerCommand
{
public SetstatusCmd()
public SetstatusCmd(Bot bot)
{
super(bot);
this.name = "setstatus";
this.help = "sets the status the bot displays";
this.arguments = "<status>";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = false;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
try {
OnlineStatus status = OnlineStatus.fromKey(event.getArgs());
if(status==OnlineStatus.UNKNOWN)
{
event.replyError("Please include one of the following statuses: `ONLINE`, `IDLE`, `DND`, `INVISIBLE`");
CommandLogContext.setError("invalid_status");
}
else
{
OnlineStatus before = event.getJDA().getPresence().getStatus();
event.getJDA().getPresence().setStatus(status);
event.replySuccess("Set the status to `"+status.getKey().toUpperCase()+"`");
CommandLogContext.setMeta(new JSONObject()
.put("before", before == null ? JSONObject.NULL : before.getKey())
.put("after", status.getKey()));
}
} catch(Exception e) {
event.reply(event.getClient().getError()+" The status could not be set!");
CommandLogContext.setError("set_status_failed");
}
}
}
@@ -18,6 +18,8 @@ package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
/**
*
@@ -25,19 +27,19 @@ import com.jagrosh.jmusicbot.commands.OwnerCommand;
*/
public class ShutdownCmd extends OwnerCommand
{
private final Bot bot;
public ShutdownCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.name = "shutdown";
this.help = "safely shuts down";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = false;
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
CommandLogContext.setMeta(new JSONObject().put("invoked", true));
event.replyWarning("Shutting down...");
bot.shutdown();
}
@@ -0,0 +1,84 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.datalog;
import org.json.JSONObject;
public final class CommandLogContext
{
private static final ThreadLocal<CommandLogEntry> CONTEXT = new ThreadLocal<>();
private CommandLogContext()
{
}
public static void clear()
{
CONTEXT.remove();
}
public static void setMeta(JSONObject meta)
{
getOrCreate().meta = meta;
}
public static void setResult(String result)
{
getOrCreate().result = result;
}
public static void setError(String reason)
{
CommandLogEntry entry = getOrCreate();
entry.result = "ERROR";
if(reason != null && !reason.isEmpty())
{
if(entry.meta == null)
entry.meta = new JSONObject();
entry.meta.put("error_reason", reason);
}
}
public static void setManualLogging()
{
getOrCreate().manual = true;
}
public static CommandLogEntry consume()
{
CommandLogEntry entry = CONTEXT.get();
CONTEXT.remove();
return entry;
}
private static CommandLogEntry getOrCreate()
{
CommandLogEntry entry = CONTEXT.get();
if(entry == null)
{
entry = new CommandLogEntry();
CONTEXT.set(entry);
}
return entry;
}
public static final class CommandLogEntry
{
public JSONObject meta;
public String result;
public boolean manual;
}
}
@@ -0,0 +1,629 @@
/*
* Copyright 2026
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.datalog;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import java.sql.*;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataLogService
{
private static final Logger LOG = LoggerFactory.getLogger("DataLog");
private final Bot bot;
private final Connection connection;
private final ExecutorService executor;
private final Map<Long, Long> currentPlaySessionId;
private final Map<Long, Long> currentPlayStartAt;
private final Map<Long, Map<Long, Long>> activeListeners;
public DataLogService(Bot bot, String jdbcUrl, String user, String password) throws SQLException
{
this.bot = bot;
this.executor = Executors.newSingleThreadExecutor();
this.currentPlaySessionId = new ConcurrentHashMap<>();
this.currentPlayStartAt = new ConcurrentHashMap<>();
this.activeListeners = new ConcurrentHashMap<>();
if(user != null && !user.isEmpty())
this.connection = DriverManager.getConnection(jdbcUrl, user, password == null ? "" : password);
else
this.connection = DriverManager.getConnection(jdbcUrl);
this.connection.setAutoCommit(true);
initSchema();
}
public void shutdown()
{
executor.shutdownNow();
try
{
connection.close();
}
catch(SQLException ex)
{
LOG.warn("Failed to close DataLogService connection: {}", ex.getMessage());
}
}
private void initSchema()
{
submit(() ->
{
try(Statement st = connection.createStatement())
{
st.execute("CREATE TABLE IF NOT EXISTS guilds (guild_id INTEGER PRIMARY KEY, name TEXT, first_seen_at INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS users (user_id INTEGER PRIMARY KEY)");
st.execute("CREATE TABLE IF NOT EXISTS user_profiles (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, guild_id INTEGER, username TEXT, discrim TEXT, avatar_url TEXT, nickname TEXT, seen_at INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS tracks (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT, identifier TEXT, uri TEXT, title TEXT, author TEXT, length_ms INTEGER, is_stream INTEGER)");
st.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_tracks_source_identifier ON tracks(source, identifier)");
st.execute("CREATE TABLE IF NOT EXISTS play_sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, voice_channel_id INTEGER, track_id INTEGER, requested_by INTEGER, queue_source TEXT, search_query TEXT, playlist_name TEXT, started_at INTEGER, ended_at INTEGER, end_reason TEXT, duration_ms INTEGER, listeners_at_start INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS listener_sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, play_session_id INTEGER, user_id INTEGER, joined_at INTEGER, left_at INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS queue_events (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, track_id INTEGER, event_type TEXT, source TEXT, position INTEGER, query TEXT, playlist_name TEXT, event_meta TEXT, created_at INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS search_events (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, query TEXT, selected_index INTEGER, track_id INTEGER, event_meta TEXT, created_at INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS command_events (id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, user_id INTEGER, command TEXT, args TEXT, result TEXT, event_meta TEXT, created_at INTEGER)");
st.execute("CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE)");
st.execute("CREATE TABLE IF NOT EXISTS track_tags (track_id INTEGER, tag_id INTEGER, added_at INTEGER, added_by_user_id INTEGER)");
st.execute("CREATE INDEX IF NOT EXISTS idx_play_sessions_guild_started ON play_sessions(guild_id, started_at)");
st.execute("CREATE INDEX IF NOT EXISTS idx_queue_events_guild_created ON queue_events(guild_id, created_at)");
st.execute("CREATE INDEX IF NOT EXISTS idx_search_events_guild_created ON search_events(guild_id, created_at)");
st.execute("CREATE INDEX IF NOT EXISTS idx_listener_sessions_play ON listener_sessions(play_session_id)");
st.execute("CREATE INDEX IF NOT EXISTS idx_command_events_guild_created ON command_events(guild_id, created_at)");
st.execute("CREATE INDEX IF NOT EXISTS idx_command_events_command_created ON command_events(command, created_at)");
ensureColumnExists("queue_events", "event_meta", "TEXT");
ensureColumnExists("search_events", "event_meta", "TEXT");
}
catch(SQLException ex)
{
LOG.warn("Failed to init datalog schema: {}", ex.getMessage());
}
});
}
public void logQueueAdd(Guild guild, User user, AudioTrack track, String source, Integer position, String query, String playlistName)
{
logQueueEventWithMeta(guild, user, track, "ADD", source, position, query, playlistName, null);
}
public void logSearchSelection(Guild guild, User user, String query, int selectedIndex, AudioTrack track)
{
logSearchEventWithMeta(guild, user, query, selectedIndex, track, null);
}
public void logPlayStart(Guild guild, AudioTrack track, RequestMetadata rm, String queueSource, String searchQuery, String playlistName)
{
if(guild == null || track == null)
return;
submit(() ->
{
try
{
ensureGuild(guild);
long trackId = ensureTrack(track);
long requestedBy = rm == null ? 0L : rm.getOwner();
if(rm != null && rm.user != null)
ensureUserAndProfile(guild, null, rm);
AudioManager am = guild.getAudioManager();
AudioChannelUnion channel = am.getConnectedChannel();
long channelId = channel == null ? 0L : channel.getIdLong();
int listeners = channel == null ? 0 : (int)channel.getMembers().stream()
.filter(m -> !m.getUser().isBot() && !m.getVoiceState().isDeafened())
.count();
try(PreparedStatement ps = connection.prepareStatement(
"INSERT INTO play_sessions (guild_id, voice_channel_id, track_id, requested_by, queue_source, search_query, playlist_name, started_at, listeners_at_start) VALUES (?,?,?,?,?,?,?,?,?)",
Statement.RETURN_GENERATED_KEYS))
{
ps.setLong(1, guild.getIdLong());
if(channelId == 0L)
ps.setNull(2, Types.BIGINT);
else
ps.setLong(2, channelId);
ps.setLong(3, trackId);
ps.setLong(4, requestedBy);
ps.setString(5, queueSource);
ps.setString(6, searchQuery);
ps.setString(7, playlistName);
long startedAt = now();
ps.setLong(8, startedAt);
ps.setInt(9, listeners);
ps.executeUpdate();
try(ResultSet rs = ps.getGeneratedKeys())
{
if(rs.next())
{
long sessionId = rs.getLong(1);
currentPlaySessionId.put(guild.getIdLong(), sessionId);
currentPlayStartAt.put(guild.getIdLong(), startedAt);
activeListeners.put(guild.getIdLong(), new ConcurrentHashMap<>());
if(channel != null)
{
channel.getMembers().forEach(m ->
{
if(m.getUser().isBot() || m.getVoiceState().isDeafened())
return;
startListenerSession(guild, sessionId, m.getUser().getIdLong());
});
}
}
}
}
}
catch(SQLException ex)
{
LOG.debug("logPlayStart failed: {}", ex.getMessage());
}
});
}
public void logPlayEnd(Guild guild, String endReason)
{
if(guild == null)
return;
submit(() ->
{
Long sessionId = currentPlaySessionId.remove(guild.getIdLong());
Long startedAt = currentPlayStartAt.remove(guild.getIdLong());
if(sessionId == null || startedAt == null)
return;
long endedAt = now();
long duration = Math.max(0L, endedAt - startedAt);
try(PreparedStatement ps = connection.prepareStatement(
"UPDATE play_sessions SET ended_at=?, end_reason=?, duration_ms=? WHERE id=?"))
{
ps.setLong(1, endedAt);
ps.setString(2, endReason);
ps.setLong(3, duration);
ps.setLong(4, sessionId);
ps.executeUpdate();
}
catch(SQLException ex)
{
LOG.debug("logPlayEnd failed: {}", ex.getMessage());
}
closeAllListenerSessions(guild.getIdLong(), sessionId);
});
}
public void logSkipVote(Guild guild, User user, AudioTrack track)
{
logQueueEventWithMeta(guild, user, track, "SKIP_VOTE", null, null, null, null, null);
}
public void logSkipAction(Guild guild, User user, AudioTrack track, String reason)
{
logQueueEventWithMeta(guild, user, track, "SKIP_"+reason, null, null, null, null, null);
}
public void logCommandEvent(Guild guild, User user, String command, String args, String result, String metaJson)
{
if(guild == null || command == null)
return;
submit(() ->
{
try
{
ensureGuild(guild);
if(user != null)
ensureUserAndProfile(guild, user, null);
try(PreparedStatement ps = connection.prepareStatement(
"INSERT INTO command_events (guild_id, user_id, command, args, result, event_meta, created_at) VALUES (?,?,?,?,?,?,?)"))
{
ps.setLong(1, guild.getIdLong());
ps.setLong(2, user == null ? 0L : user.getIdLong());
ps.setString(3, command);
ps.setString(4, args);
ps.setString(5, result);
ps.setString(6, metaJson);
ps.setLong(7, now());
ps.executeUpdate();
}
}
catch(SQLException ex)
{
LOG.debug("logCommandEvent failed: {}", ex.getMessage());
}
});
}
public void logQueueEventWithMeta(Guild guild, User user, AudioTrack track, String eventType, String source, Integer position, String query, String playlistName, String metaJson)
{
if(guild == null || eventType == null)
return;
submit(() ->
{
try
{
ensureGuild(guild);
if(user != null)
ensureUserAndProfile(guild, user, null);
Long trackId = null;
if(track != null)
trackId = ensureTrack(track);
try(PreparedStatement ps = connection.prepareStatement(
"INSERT INTO queue_events (guild_id, user_id, track_id, event_type, source, position, query, playlist_name, event_meta, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)"))
{
ps.setLong(1, guild.getIdLong());
ps.setLong(2, user == null ? 0L : user.getIdLong());
if(trackId == null) ps.setNull(3, Types.INTEGER); else ps.setLong(3, trackId);
ps.setString(4, eventType);
ps.setString(5, source);
if(position == null) ps.setNull(6, Types.INTEGER); else ps.setInt(6, position);
ps.setString(7, query);
ps.setString(8, playlistName);
ps.setString(9, metaJson);
ps.setLong(10, now());
ps.executeUpdate();
}
}
catch(SQLException ex)
{
LOG.debug("logQueueEvent failed: {}", ex.getMessage());
}
});
}
public void logSearchEventWithMeta(Guild guild, User user, String query, Integer selectedIndex, AudioTrack track, String metaJson)
{
if(guild == null)
return;
submit(() ->
{
try
{
ensureGuild(guild);
if(user != null)
ensureUserAndProfile(guild, user, null);
Long trackId = null;
if(track != null)
trackId = ensureTrack(track);
try(PreparedStatement ps = connection.prepareStatement(
"INSERT INTO search_events (guild_id, user_id, query, selected_index, track_id, event_meta, created_at) VALUES (?,?,?,?,?,?,?)"))
{
ps.setLong(1, guild.getIdLong());
ps.setLong(2, user == null ? 0L : user.getIdLong());
ps.setString(3, query);
if(selectedIndex == null) ps.setNull(4, Types.INTEGER); else ps.setInt(4, selectedIndex);
if(trackId == null) ps.setNull(5, Types.INTEGER); else ps.setLong(5, trackId);
ps.setString(6, metaJson);
ps.setLong(7, now());
ps.executeUpdate();
}
}
catch(SQLException ex)
{
LOG.debug("logSearchEvent failed: {}", ex.getMessage());
}
});
}
public void logPlaylistError(Guild guild, User user, String playlistName, String item, String reason)
{
JSONObject meta = new JSONObject();
if(playlistName != null)
meta.put("playlist_name", playlistName);
if(item != null)
meta.put("playlist_item", item);
if(reason != null)
meta.put("error_reason", reason);
logQueueEventWithMeta(guild, user, null, "PLAYLIST_ERROR", "PLAYLIST", null, null, playlistName,
meta.length() == 0 ? null : meta.toString());
}
public void onVoiceUpdate(GuildVoiceUpdateEvent event)
{
if(event == null)
return;
Guild guild = event.getGuild();
Long sessionId = currentPlaySessionId.get(guild.getIdLong());
if(sessionId == null)
return;
Member member = event.getMember();
if(member.getUser().isBot())
return;
AudioManager am = guild.getAudioManager();
AudioChannelUnion channel = am.getConnectedChannel();
if(channel == null)
return;
boolean inChannel = member.getVoiceState().inAudioChannel() && channel.equals(member.getVoiceState().getChannel());
boolean listening = inChannel && !member.getVoiceState().isDeafened();
long userId = member.getUser().getIdLong();
Map<Long, Long> listenerMap = activeListeners.computeIfAbsent(guild.getIdLong(), id -> new ConcurrentHashMap<>());
if(listening && !listenerMap.containsKey(userId))
{
submit(() -> startListenerSession(guild, sessionId, userId));
}
else if(!listening && listenerMap.containsKey(userId))
{
submit(() -> endListenerSession(guild.getIdLong(), sessionId, userId));
}
}
private void startListenerSession(Guild guild, long sessionId, long userId)
{
Map<Long, Long> listenerMap = activeListeners.computeIfAbsent(guild.getIdLong(), id -> new ConcurrentHashMap<>());
if(listenerMap.containsKey(userId))
return;
try(PreparedStatement ps = connection.prepareStatement(
"INSERT INTO listener_sessions (play_session_id, user_id, joined_at) VALUES (?,?,?)",
Statement.RETURN_GENERATED_KEYS))
{
ps.setLong(1, sessionId);
ps.setLong(2, userId);
ps.setLong(3, now());
ps.executeUpdate();
try(ResultSet rs = ps.getGeneratedKeys())
{
if(rs.next())
{
listenerMap.put(userId, rs.getLong(1));
}
}
}
catch(SQLException ex)
{
LOG.debug("startListenerSession failed: {}", ex.getMessage());
}
}
private void endListenerSession(long guildId, long sessionId, long userId)
{
Map<Long, Long> listenerMap = activeListeners.computeIfAbsent(guildId, id -> new ConcurrentHashMap<>());
Long listenerSessionId = listenerMap.remove(userId);
if(listenerSessionId == null)
return;
try(PreparedStatement ps = connection.prepareStatement(
"UPDATE listener_sessions SET left_at=? WHERE id=?"))
{
ps.setLong(1, now());
ps.setLong(2, listenerSessionId);
ps.executeUpdate();
}
catch(SQLException ex)
{
LOG.debug("endListenerSession failed: {}", ex.getMessage());
}
}
private void closeAllListenerSessions(long guildId, long sessionId)
{
Map<Long, Long> listenerMap = activeListeners.remove(guildId);
if(listenerMap == null)
return;
listenerMap.forEach((userId, listenerSessionId) ->
{
try(PreparedStatement ps = connection.prepareStatement(
"UPDATE listener_sessions SET left_at=? WHERE id=?"))
{
ps.setLong(1, now());
ps.setLong(2, listenerSessionId);
ps.executeUpdate();
}
catch(SQLException ex)
{
LOG.debug("closeListenerSession failed: {}", ex.getMessage());
}
});
}
private void ensureGuild(Guild guild) throws SQLException
{
try(PreparedStatement ps = connection.prepareStatement("UPDATE guilds SET name=? WHERE guild_id=?"))
{
ps.setString(1, guild.getName());
ps.setLong(2, guild.getIdLong());
int updated = ps.executeUpdate();
if(updated == 0)
{
try(PreparedStatement ins = connection.prepareStatement(
"INSERT INTO guilds (guild_id, name, first_seen_at) VALUES (?,?,?)"))
{
ins.setLong(1, guild.getIdLong());
ins.setString(2, guild.getName());
ins.setLong(3, now());
ins.executeUpdate();
}
}
}
}
private void ensureColumnExists(String table, String column, String type) throws SQLException
{
if(table == null || column == null || type == null)
return;
try(Statement st = connection.createStatement();
ResultSet rs = st.executeQuery("PRAGMA table_info(" + table + ")"))
{
while(rs.next())
{
String name = rs.getString("name");
if(column.equalsIgnoreCase(name))
return;
}
}
try(Statement st = connection.createStatement())
{
st.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + type);
}
}
private void ensureUserAndProfile(Guild guild, User user, RequestMetadata rm) throws SQLException
{
long userId = user != null ? user.getIdLong() : rm.user.id;
try(PreparedStatement ps = connection.prepareStatement("INSERT INTO users (user_id) VALUES (?)"))
{
ps.setLong(1, userId);
ps.executeUpdate();
}
catch(SQLException ignore)
{
// ignore duplicates
}
String username = user != null ? user.getName() : rm.user.username;
String discrim = user != null ? user.getDiscriminator() : rm.user.discrim;
String avatar = user != null ? user.getEffectiveAvatarUrl() : rm.user.avatar;
String nickname = null;
if(user != null)
{
Member member = guild.getMember(user);
nickname = member == null ? null : member.getNickname();
}
long now = now();
try(PreparedStatement ps = connection.prepareStatement(
"SELECT username, discrim, avatar_url, nickname FROM user_profiles WHERE user_id=? AND guild_id=? ORDER BY seen_at DESC LIMIT 1"))
{
ps.setLong(1, userId);
ps.setLong(2, guild.getIdLong());
try(ResultSet rs = ps.executeQuery())
{
boolean changed = true;
if(rs.next())
{
changed = !safeEquals(rs.getString(1), username)
|| !safeEquals(rs.getString(2), discrim)
|| !safeEquals(rs.getString(3), avatar)
|| !safeEquals(rs.getString(4), nickname);
}
if(changed)
{
try(PreparedStatement ins = connection.prepareStatement(
"INSERT INTO user_profiles (user_id, guild_id, username, discrim, avatar_url, nickname, seen_at) VALUES (?,?,?,?,?,?,?)"))
{
ins.setLong(1, userId);
ins.setLong(2, guild.getIdLong());
ins.setString(3, username);
ins.setString(4, discrim);
ins.setString(5, avatar);
ins.setString(6, nickname);
ins.setLong(7, now);
ins.executeUpdate();
}
}
}
}
}
private long ensureTrack(AudioTrack track) throws SQLException
{
AudioTrackInfo info = track.getInfo();
String source = inferSource(info);
try(PreparedStatement ps = connection.prepareStatement(
"SELECT id FROM tracks WHERE source=? AND identifier=?"))
{
ps.setString(1, source);
ps.setString(2, track.getIdentifier());
try(ResultSet rs = ps.executeQuery())
{
if(rs.next())
return rs.getLong(1);
}
}
try(PreparedStatement ins = connection.prepareStatement(
"INSERT INTO tracks (source, identifier, uri, title, author, length_ms, is_stream) VALUES (?,?,?,?,?,?,?)",
Statement.RETURN_GENERATED_KEYS))
{
ins.setString(1, source);
ins.setString(2, track.getIdentifier());
ins.setString(3, info.uri);
ins.setString(4, info.title);
ins.setString(5, info.author);
ins.setLong(6, info.length);
ins.setInt(7, info.isStream ? 1 : 0);
ins.executeUpdate();
try(ResultSet rs = ins.getGeneratedKeys())
{
if(rs.next())
return rs.getLong(1);
}
}
catch(SQLException ignore)
{
// duplicate or insert failure; fall through to select
}
try(PreparedStatement ps = connection.prepareStatement(
"SELECT id FROM tracks WHERE source=? AND identifier=?"))
{
ps.setString(1, source);
ps.setString(2, track.getIdentifier());
try(ResultSet rs = ps.executeQuery())
{
if(rs.next())
return rs.getLong(1);
}
}
return -1L;
}
private String inferSource(AudioTrackInfo info)
{
if(info == null || info.uri == null)
return "unknown";
String uri = info.uri.toLowerCase();
if(uri.contains("youtube.com") || uri.contains("youtu.be"))
return "youtube";
if(uri.contains("soundcloud.com"))
return "soundcloud";
if(uri.contains("bandcamp.com"))
return "bandcamp";
if(uri.contains("vimeo.com"))
return "vimeo";
if(uri.contains("twitch.tv"))
return "twitch";
return "other";
}
private void submit(Runnable r)
{
executor.submit(r);
}
private static long now()
{
return Instant.now().toEpochMilli();
}
private static boolean safeEquals(String a, String b)
{
if(a == null && b == null)
return true;
if(a == null || b == null)
return false;
return a.equals(b);
}
}
@@ -30,6 +30,7 @@ public class Prompt
private final String noguiMessage;
private boolean nogui;
private boolean noprompt;
private Scanner scanner;
public Prompt(String title)
@@ -39,14 +40,15 @@ public class Prompt
public Prompt(String title, String noguiMessage)
{
this(title, noguiMessage, "true".equalsIgnoreCase(System.getProperty("nogui")));
this(title, noguiMessage, "true".equalsIgnoreCase(System.getProperty("nogui")), "true".equalsIgnoreCase(System.getProperty("noprompt")));
}
public Prompt(String title, String noguiMessage, boolean nogui)
public Prompt(String title, String noguiMessage, boolean nogui, boolean noprompt)
{
this.title = title;
this.noguiMessage = noguiMessage == null ? "Switching to nogui mode. You can manually start in nogui mode by including the -Dnogui=true flag." : noguiMessage;
this.nogui = nogui;
this.noprompt = noprompt;
}
public boolean isNoGUI()
@@ -108,6 +110,8 @@ public class Prompt
public String prompt(String content)
{
if(noprompt)
return null;
if(nogui)
{
if(scanner==null)
@@ -16,6 +16,7 @@
package com.jagrosh.jmusicbot.playlist;
import com.jagrosh.jmusicbot.BotConfig;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
@@ -24,7 +25,6 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -46,14 +46,14 @@ public class PlaylistLoader
{
if(folderExists())
{
File folder = new File(config.getPlaylistsFolder());
File folder = new File(OtherUtil.getPath(config.getPlaylistsFolder()).toString());
return Arrays.asList(folder.listFiles((pathname) -> pathname.getName().endsWith(".txt")))
.stream().map(f -> f.getName().substring(0,f.getName().length()-4)).collect(Collectors.toList());
}
else
{
createFolder();
return Collections.EMPTY_LIST;
return Collections.emptyList();
}
}
@@ -61,29 +61,29 @@ public class PlaylistLoader
{
try
{
Files.createDirectory(Paths.get(config.getPlaylistsFolder()));
Files.createDirectory(OtherUtil.getPath(config.getPlaylistsFolder()));
}
catch (IOException ignore) {}
}
public boolean folderExists()
{
return Files.exists(Paths.get(config.getPlaylistsFolder()));
return Files.exists(OtherUtil.getPath(config.getPlaylistsFolder()));
}
public void createPlaylist(String name) throws IOException
{
Files.createFile(Paths.get(config.getPlaylistsFolder()+File.separator+name+".txt"));
Files.createFile(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"));
}
public void deletePlaylist(String name) throws IOException
{
Files.delete(Paths.get(config.getPlaylistsFolder()+File.separator+name+".txt"));
Files.delete(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"));
}
public void writePlaylist(String name, String text) throws IOException
{
Files.write(Paths.get(config.getPlaylistsFolder()+File.separator+name+".txt"), text.trim().getBytes());
Files.write(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"), text.trim().getBytes());
}
public Playlist getPlaylist(String name)
@@ -96,7 +96,7 @@ public class PlaylistLoader
{
boolean[] shuffle = {false};
List<String> list = new ArrayList<>();
Files.readAllLines(Paths.get(config.getPlaylistsFolder()+File.separator+name+".txt")).forEach(str ->
Files.readAllLines(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt")).forEach(str ->
{
String s = str.trim();
if(s.isEmpty())
@@ -157,101 +157,90 @@ public class PlaylistLoader
public void loadTracks(AudioPlayerManager manager, Consumer<AudioTrack> consumer, Runnable callback)
{
if(!loaded)
if(loaded)
return;
loaded = true;
for(int i=0; i<items.size(); i++)
{
loaded = true;
for(int i=0; i<items.size(); i++)
boolean last = i+1 == items.size();
int index = i;
manager.loadItemOrdered(name, items.get(i), new AudioLoadResultHandler()
{
boolean last = i+1==items.size();
int index = i;
manager.loadItemOrdered(name, items.get(i), new AudioLoadResultHandler()
private void done()
{
@Override
public void trackLoaded(AudioTrack at)
if(last)
{
if(config.isTooLong(at))
errors.add(new PlaylistLoadError(index, items.get(index), "This track is longer than the allowed maximum"));
else
{
at.setUserData(0L);
tracks.add(at);
consumer.accept(at);
}
if(last && callback!=null)
callback.run();
}
@Override
public void playlistLoaded(AudioPlaylist ap)
{
if(ap.isSearchResult())
{
if(config.isTooLong(ap.getTracks().get(0)))
errors.add(new PlaylistLoadError(index, items.get(index), "This track is longer than the allowed maximum"));
else
{
ap.getTracks().get(0).setUserData(0L);
tracks.add(ap.getTracks().get(0));
consumer.accept(ap.getTracks().get(0));
}
}
else if(ap.getSelectedTrack()!=null)
{
if(config.isTooLong(ap.getSelectedTrack()))
errors.add(new PlaylistLoadError(index, items.get(index), "This track is longer than the allowed maximum"));
else
{
ap.getSelectedTrack().setUserData(0L);
tracks.add(ap.getSelectedTrack());
consumer.accept(ap.getSelectedTrack());
}
}
else
{
List<AudioTrack> loaded = new ArrayList<>(ap.getTracks());
if(shuffle)
for(int first =0; first<loaded.size(); first++)
{
int second = (int)(Math.random()*loaded.size());
AudioTrack tmp = loaded.get(first);
loaded.set(first, loaded.get(second));
loaded.set(second, tmp);
}
loaded.removeIf(track -> config.isTooLong(track));
loaded.forEach(at -> at.setUserData(0L));
tracks.addAll(loaded);
loaded.forEach(at -> consumer.accept(at));
}
if(last && callback!=null)
if(shuffle)
shuffleTracks();
if(callback != null)
callback.run();
}
}
@Override
public void noMatches()
@Override
public void trackLoaded(AudioTrack at)
{
if(config.isTooLong(at))
errors.add(new PlaylistLoadError(index, items.get(index), "This track is longer than the allowed maximum"));
else
{
errors.add(new PlaylistLoadError(index, items.get(index), "No matches found."));
if(last && callback!=null)
callback.run();
at.setUserData(0L);
tracks.add(at);
consumer.accept(at);
}
done();
}
@Override
public void loadFailed(FriendlyException fe)
@Override
public void playlistLoaded(AudioPlaylist ap)
{
if(ap.isSearchResult())
{
errors.add(new PlaylistLoadError(index, items.get(index), "Failed to load track: "+fe.getLocalizedMessage()));
if(last && callback!=null)
callback.run();
trackLoaded(ap.getTracks().get(0));
}
});
}
else if(ap.getSelectedTrack()!=null)
{
trackLoaded(ap.getSelectedTrack());
}
else
{
List<AudioTrack> loaded = new ArrayList<>(ap.getTracks());
if(shuffle)
for(int first =0; first<loaded.size(); first++)
{
int second = (int)(Math.random()*loaded.size());
AudioTrack tmp = loaded.get(first);
loaded.set(first, loaded.get(second));
loaded.set(second, tmp);
}
loaded.removeIf(track -> config.isTooLong(track));
loaded.forEach(at -> at.setUserData(0L));
tracks.addAll(loaded);
loaded.forEach(at -> consumer.accept(at));
}
done();
}
@Override
public void noMatches()
{
errors.add(new PlaylistLoadError(index, items.get(index), "No matches found."));
done();
}
@Override
public void loadFailed(FriendlyException fe)
{
errors.add(new PlaylistLoadError(index, items.get(index), "Failed to load track: "+fe.getLocalizedMessage()));
done();
}
});
}
if(shuffle)
shuffleTracks();
}
public void shuffleTracks()
{
if(tracks!=null)
shuffle(tracks);
shuffle(tracks);
}
public String getName()
@@ -0,0 +1,130 @@
/*
* Copyright 2022 John Grosh (jagrosh).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.queue;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
*
* @author Wolfgang Schwendtbauer
* @param <T>
*/
public abstract class AbstractQueue<T extends Queueable>
{
protected AbstractQueue(AbstractQueue<T> queue)
{
this.list = queue != null ? queue.getList() : new LinkedList<>();
}
protected final List<T> list;
public abstract int add(T item);
public void addAt(int index, T item)
{
if(index >= list.size())
list.add(item);
else
list.add(index, item);
}
public int size() {
return list.size();
}
public T pull() {
return list.remove(0);
}
public boolean isEmpty()
{
return list.isEmpty();
}
public List<T> getList()
{
return list;
}
public T get(int index) {
return list.get(index);
}
public T remove(int index)
{
return list.remove(index);
}
public int removeAll(long identifier)
{
int count = 0;
for(int i=list.size()-1; i>=0; i--)
{
if(list.get(i).getIdentifier()==identifier)
{
list.remove(i);
count++;
}
}
return count;
}
public void clear()
{
list.clear();
}
public int shuffle(long identifier)
{
List<Integer> iset = new ArrayList<>();
for(int i=0; i<list.size(); i++)
{
if(list.get(i).getIdentifier()==identifier)
iset.add(i);
}
for(int j=0; j<iset.size(); j++)
{
int first = iset.get(j);
int second = iset.get((int)(Math.random()*iset.size()));
T temp = list.get(first);
list.set(first, list.get(second));
list.set(second, temp);
}
return iset.size();
}
public void skip(int number)
{
if (number > 0) {
list.subList(0, number).clear();
}
}
/**
* Move an item to a different position in the list
* @param from The position of the item
* @param to The new position of the item
* @return the moved item
*/
public T moveItem(int from, int to)
{
T item = list.remove(from);
list.add(to, item);
return item;
}
}
@@ -15,9 +15,7 @@
*/
package com.jagrosh.jmusicbot.queue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
@@ -25,15 +23,21 @@ import java.util.Set;
* @author John Grosh (jagrosh)
* @param <T>
*/
public class FairQueue<T extends Queueable> {
private final List<T> list = new ArrayList<>();
private final Set<Long> set = new HashSet<>();
public class FairQueue<T extends Queueable> extends AbstractQueue<T>
{
public FairQueue(AbstractQueue<T> queue)
{
super(queue);
}
protected final Set<Long> set = new HashSet<>();
@Override
public int add(T item)
{
int lastIndex;
for(lastIndex=list.size()-1; lastIndex>-1; lastIndex--)
if(list.get(lastIndex).getIdentifier()==item.getIdentifier())
if(list.get(lastIndex).getIdentifier() == item.getIdentifier())
break;
lastIndex++;
set.clear();
@@ -46,99 +50,5 @@ public class FairQueue<T extends Queueable> {
list.add(lastIndex, item);
return lastIndex;
}
public void addAt(int index, T item)
{
if(index >= list.size())
list.add(item);
else
list.add(index, item);
}
public int size()
{
return list.size();
}
public T pull()
{
return list.remove(0);
}
public boolean isEmpty()
{
return list.isEmpty();
}
public List<T> getList()
{
return list;
}
public T get(int index)
{
return list.get(index);
}
public T remove(int index)
{
return list.remove(index);
}
public int removeAll(long identifier)
{
int count = 0;
for(int i=list.size()-1; i>=0; i--)
{
if(list.get(i).getIdentifier()==identifier)
{
list.remove(i);
count++;
}
}
return count;
}
public void clear()
{
list.clear();
}
public int shuffle(long identifier)
{
List<Integer> iset = new ArrayList<>();
for(int i=0; i<list.size(); i++)
{
if(list.get(i).getIdentifier()==identifier)
iset.add(i);
}
for(int j=0; j<iset.size(); j++)
{
int first = iset.get(j);
int second = iset.get((int)(Math.random()*iset.size()));
T temp = list.get(first);
list.set(first, list.get(second));
list.set(second, temp);
}
return iset.size();
}
public void skip(int number)
{
for(int i=0; i<number; i++)
list.remove(0);
}
/**
* Move an item to a different position in the list
* @param from The position of the item
* @param to The new position of the item
* @return the moved item
*/
public T moveItem(int from, int to)
{
T item = list.remove(from);
list.add(to, item);
return item;
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2022 John Grosh (jagrosh).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.queue;
/**
*
* @author Wolfgang Schwendtbauer
* @param <T>
*/
public class LinearQueue<T extends Queueable> extends AbstractQueue<T>
{
public LinearQueue(AbstractQueue<T> queue)
{
super(queue);
}
@Override
public int add(T item)
{
list.add(item);
return list.size() - 1;
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2022 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.queue;
/**
*
* @author Wolfgang Schwendtbauer
*/
@FunctionalInterface
public interface QueueSupplier
{
<T extends Queueable> AbstractQueue<T> apply(AbstractQueue<T> queue);
}
@@ -0,0 +1,69 @@
/*
* Copyright 2022 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.settings;
import com.jagrosh.jmusicbot.queue.AbstractQueue;
import com.jagrosh.jmusicbot.queue.FairQueue;
import com.jagrosh.jmusicbot.queue.LinearQueue;
import com.jagrosh.jmusicbot.queue.Queueable;
import com.jagrosh.jmusicbot.queue.QueueSupplier;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* @author Wolfgang Schwendtbauer
*/
public enum QueueType
{
LINEAR("\u23E9", "Linear", LinearQueue::new), // ⏩
FAIR("\uD83D\uDD22", "Fair", FairQueue::new); // 🔢
private final String userFriendlyName;
private final String emoji;
private final QueueSupplier supplier;
QueueType(final String emoji, final String userFriendlyName, QueueSupplier supplier)
{
this.userFriendlyName = userFriendlyName;
this.emoji = emoji;
this.supplier = supplier;
}
public static List<String> getNames()
{
return Arrays.stream(QueueType.values())
.map(type -> type.name().toLowerCase())
.collect(Collectors.toList());
}
public <T extends Queueable> AbstractQueue<T> createInstance(AbstractQueue<T> previous)
{
return supplier.apply(previous);
}
public String getUserFriendlyName()
{
return userFriendlyName;
}
public String getEmoji()
{
return emoji;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2020 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.settings;
/**
*
* @author Michaili K
*/
public enum RepeatMode
{
OFF(null, "Off"),
ALL("\uD83D\uDD01", "All"), // 🔁
SINGLE("\uD83D\uDD02", "Single"); // 🔂
private final String emoji;
private final String userFriendlyName;
private RepeatMode(String emoji, String userFriendlyName)
{
this.emoji = emoji;
this.userFriendlyName = userFriendlyName;
}
public String getEmoji()
{
return emoji;
}
public String getUserFriendlyName()
{
return userFriendlyName;
}
}
@@ -16,10 +16,12 @@
package com.jagrosh.jmusicbot.settings;
import com.jagrosh.jdautilities.command.GuildSettingsProvider;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.core.entities.VoiceChannel;
import java.util.Collection;
import java.util.Collections;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
*
@@ -33,9 +35,12 @@ public class Settings implements GuildSettingsProvider
protected long roleId;
private int volume;
private String defaultPlaylist;
private boolean repeatMode;
public Settings(SettingsManager manager, String textId, String voiceId, String roleId, int volume, String defaultPlaylist, boolean repeatMode)
private RepeatMode repeatMode;
private QueueType queueType;
private String prefix;
private double skipRatio;
public Settings(SettingsManager manager, String textId, String voiceId, String roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType)
{
this.manager = manager;
try
@@ -65,9 +70,12 @@ public class Settings implements GuildSettingsProvider
this.volume = volume;
this.defaultPlaylist = defaultPlaylist;
this.repeatMode = repeatMode;
this.prefix = prefix;
this.skipRatio = skipRatio;
this.queueType = queueType;
}
public Settings(SettingsManager manager, long textId, long voiceId, long roleId, int volume, String defaultPlaylist, boolean repeatMode)
public Settings(SettingsManager manager, long textId, long voiceId, long roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType)
{
this.manager = manager;
this.textId = textId;
@@ -76,6 +84,9 @@ public class Settings implements GuildSettingsProvider
this.volume = volume;
this.defaultPlaylist = defaultPlaylist;
this.repeatMode = repeatMode;
this.prefix = prefix;
this.skipRatio = skipRatio;
this.queueType = queueType;
}
// Getters
@@ -104,11 +115,32 @@ public class Settings implements GuildSettingsProvider
return defaultPlaylist;
}
public boolean getRepeatMode()
public RepeatMode getRepeatMode()
{
return repeatMode;
}
public String getPrefix()
{
return prefix;
}
public double getSkipRatio()
{
return skipRatio;
}
public QueueType getQueueType()
{
return queueType;
}
@Override
public Collection<String> getPrefixes()
{
return prefix == null ? Collections.emptySet() : Collections.singleton(prefix);
}
// Setters
public void setTextChannel(TextChannel tc)
{
@@ -140,9 +172,27 @@ public class Settings implements GuildSettingsProvider
this.manager.writeSettings();
}
public void setRepeatMode(boolean mode)
public void setRepeatMode(RepeatMode mode)
{
this.repeatMode = mode;
this.manager.writeSettings();
}
public void setPrefix(String prefix)
{
this.prefix = prefix;
this.manager.writeSettings();
}
public void setSkipRatio(double skipRatio)
{
this.skipRatio = skipRatio;
this.manager.writeSettings();
}
public void setQueueType(QueueType queueType)
{
this.queueType = queueType;
this.manager.writeSettings();
}
}
@@ -16,46 +16,71 @@
package com.jagrosh.jmusicbot.settings;
import com.jagrosh.jdautilities.command.GuildSettingsManager;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.NoSuchFileException;
import java.util.HashMap;
import net.dv8tion.jda.core.entities.Guild;
import net.dv8tion.jda.api.entities.Guild;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author John Grosh (john.a.grosh@gmail.com)
*/
public class SettingsManager implements GuildSettingsManager
public class SettingsManager implements GuildSettingsManager<Settings>
{
private final static Logger LOG = LoggerFactory.getLogger("Settings");
private final static String SETTINGS_FILE = "serversettings.json";
private final HashMap<Long,Settings> settings;
public SettingsManager()
{
this.settings = new HashMap<>();
try {
JSONObject loadedSettings = new JSONObject(new String(Files.readAllBytes(Paths.get("serversettings.json"))));
JSONObject loadedSettings = new JSONObject(new String(Files.readAllBytes(OtherUtil.getPath(SETTINGS_FILE))));
loadedSettings.keySet().forEach((id) -> {
JSONObject o = loadedSettings.getJSONObject(id);
// Legacy version support: On versions 0.3.3 and older, the repeat mode was represented as a boolean.
if (!o.has("repeat_mode") && o.has("repeat") && o.getBoolean("repeat"))
o.put("repeat_mode", RepeatMode.ALL);
settings.put(Long.parseLong(id), new Settings(this,
o.has("text_channel_id") ? o.getString("text_channel_id") : null,
o.has("voice_channel_id")? o.getString("voice_channel_id"): null,
o.has("dj_role_id") ? o.getString("dj_role_id") : null,
o.has("volume") ? o.getInt("volume") : 100,
o.has("default_playlist")? o.getString("default_playlist"): null,
o.has("repeat") ? o.getBoolean("repeat") : false));
o.has("text_channel_id") ? o.getString("text_channel_id") : null,
o.has("voice_channel_id")? o.getString("voice_channel_id") : null,
o.has("dj_role_id") ? o.getString("dj_role_id") : null,
o.has("volume") ? o.getInt("volume") : 100,
o.has("default_playlist")? o.getString("default_playlist") : null,
o.has("repeat_mode") ? o.getEnum(RepeatMode.class, "repeat_mode"): RepeatMode.OFF,
o.has("prefix") ? o.getString("prefix") : null,
o.has("skip_ratio") ? o.getDouble("skip_ratio") : -1,
o.has("queue_type") ? o.getEnum(QueueType.class, "queue_type") : QueueType.FAIR));
});
} catch (NoSuchFileException e) {
// create an empty json file
try {
LOG.info("serversettings.json will be created in " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
Files.write(OtherUtil.getPath("serversettings.json"), new JSONObject().toString(4).getBytes());
} catch(IOException ex) {
LOG.warn("Failed to create new settings file: "+ex);
}
return;
} catch(IOException | JSONException e) {
LoggerFactory.getLogger("Settings").warn("Failed to load server settings (this is normal if no settings have been set yet): "+e);
LOG.warn("Failed to load server settings: "+e);
}
LOG.info("serversettings.json loaded from " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
}
/**
* Gets non-null settings for a Guild
*
*
* @param guild the guild to get settings for
* @return the existing settings, or new settings for that guild
*/
@@ -64,17 +89,17 @@ public class SettingsManager implements GuildSettingsManager
{
return getSettings(guild.getIdLong());
}
public Settings getSettings(long guildId)
{
return settings.computeIfAbsent(guildId, id -> createDefaultSettings());
}
private Settings createDefaultSettings()
{
return new Settings(this, 0, 0, 0, 100, null, false);
return new Settings(this, 0, 0, 0, 100, null, RepeatMode.OFF, null, -1, QueueType.FAIR);
}
protected void writeSettings()
{
JSONObject obj = new JSONObject();
@@ -89,16 +114,22 @@ public class SettingsManager implements GuildSettingsManager
o.put("dj_role_id", Long.toString(s.roleId));
if(s.getVolume()!=100)
o.put("volume",s.getVolume());
if(s.getDefaultPlaylist()!=null)
if(s.getDefaultPlaylist() != null)
o.put("default_playlist", s.getDefaultPlaylist());
if(s.getRepeatMode())
o.put("repeat", true);
if(s.getRepeatMode()!=RepeatMode.OFF)
o.put("repeat_mode", s.getRepeatMode());
if(s.getPrefix() != null)
o.put("prefix", s.getPrefix());
if(s.getSkipRatio() != -1)
o.put("skip_ratio", s.getSkipRatio());
if(s.getQueueType() != QueueType.FAIR)
o.put("queue_type", s.getQueueType().name());
obj.put(Long.toString(key), o);
});
try {
Files.write(Paths.get("serversettings.json"), obj.toString(4).getBytes());
Files.write(OtherUtil.getPath(SETTINGS_FILE), obj.toString(4).getBytes());
} catch(IOException ex){
LoggerFactory.getLogger("Settings").warn("Failed to write to file: "+ex);
LOG.warn("Failed to write to file: "+ex);
}
}
}
@@ -15,29 +15,41 @@
*/
package com.jagrosh.jmusicbot.utils;
import com.jagrosh.jmusicbot.audio.RequestMetadata.UserInfo;
import java.util.List;
import net.dv8tion.jda.core.entities.Role;
import net.dv8tion.jda.core.entities.TextChannel;
import net.dv8tion.jda.core.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
*
* @author John Grosh <john.a.grosh@gmail.com>
*/
public class FormatUtil {
public static String formatTime(long duration)
public static String formatUsername(String username, String discrim)
{
if(duration == Long.MAX_VALUE)
return "LIVE";
long seconds = Math.round(duration/1000.0);
long hours = seconds/(60*60);
seconds %= 60*60;
long minutes = seconds/60;
seconds %= 60;
return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds);
if(discrim == null || discrim.equals("0000"))
{
return username;
}
else
{
return username + "#" + discrim;
}
}
public static String formatUsername(UserInfo userinfo)
{
return formatUsername(userinfo.username, userinfo.discrim);
}
public static String formatUsername(User user)
{
return formatUsername(user.getName(), user.getDiscriminator());
}
public static String progressBar(double percent)
{
String str = "";
@@ -74,7 +86,7 @@ public class FormatUtil {
{
String out = " Multiple voice channels found matching \""+query+"\":";
for(int i=0; i<6 && i<list.size(); i++)
out+="\n - "+list.get(i).getName()+" (ID:"+list.get(i).getId()+")";
out+="\n - "+list.get(i).getAsMention()+" (ID:"+list.get(i).getId()+")";
if(list.size()>6)
out+="\n**And "+(list.size()-6)+" more...**";
return out;
@@ -82,7 +94,7 @@ public class FormatUtil {
public static String listOfRoles(List<Role> list, String query)
{
String out = " Multiple text channels found matching \""+query+"\":";
String out = " Multiple roles found matching \""+query+"\":";
for(int i=0; i<6 && i<list.size(); i++)
out+="\n - "+list.get(i).getName()+" (ID:"+list.get(i).getId()+")";
if(list.size()>6)
@@ -92,6 +104,9 @@ public class FormatUtil {
public static String filter(String input)
{
return input.replace("@everyone", "@\u0435veryone").replace("@here", "@h\u0435re").trim(); // cyrillic letter e
return input.replace("\u202E","")
.replace("@everyone", "@\u0435veryone") // cyrillic letter e
.replace("@here", "@h\u0435re") // cyrillic letter e
.trim();
}
}
@@ -15,13 +15,22 @@
*/
package com.jagrosh.jmusicbot.utils;
import com.jagrosh.jdautilities.commons.JDAUtilitiesInfo;
import com.jagrosh.jmusicbot.JMusicBot;
import com.jagrosh.jmusicbot.entities.Prompt;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import net.dv8tion.jda.core.OnlineStatus;
import net.dv8tion.jda.core.entities.Game;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.ApplicationInfo;
import net.dv8tion.jda.api.entities.User;
import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;
@@ -36,8 +45,43 @@ public class OtherUtil
public final static String NEW_VERSION_AVAILABLE = "There is a new version of JMusicBot available!\n"
+ "Current version: %s\n"
+ "New Version: %s\n\n"
+ "Please visit https://github.com/jagrosh/MusicBot/releases/latest to get the latest release.";
+ "Please visit https://github.com/chrisb09/MusicBot/releases to get the latest release."
+ "If you are using docker (or docker-compose), please pull the new image with:"
+ "Docker: docker pull chrisb09/jmusicbot"
+ "Docker-compose: docker-compose pull"+
" and don't forget to restart the container.";
private final static String WINDOWS_INVALID_PATH = "c:\\windows\\system32\\";
/**
* gets a Path from a String
* also fixes the windows tendency to try to start in system32
* any time the bot tries to access this path, it will instead start in the location of the jar file
*
* @param path the string path
* @return the Path object
*/
public static Path getPath(String path)
{
Path result = Paths.get(path);
// special logic to prevent trying to access system32
if(result.toAbsolutePath().toString().toLowerCase().startsWith(WINDOWS_INVALID_PATH))
{
try
{
result = Paths.get(new File(JMusicBot.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getPath() + File.separator + path);
}
catch(URISyntaxException ignored) {}
}
return result;
}
/**
* Loads a resource from the jar as a string
*
* @param clazz class base object
* @param name name of resource
* @return string containing the contents of the resource
*/
public static String loadResource(Object clazz, String name)
{
try(BufferedReader reader = new BufferedReader(new InputStreamReader(clazz.getClass().getResourceAsStream(name))))
@@ -46,12 +90,18 @@ public class OtherUtil
reader.lines().forEach(line -> sb.append("\r\n").append(line));
return sb.toString().trim();
}
catch(IOException ex)
catch(IOException ignored)
{
return null;
}
}
/**
* Loads image data from a URL
*
* @param url url of image
* @return inputstream of url
*/
public static InputStream imageFromUrl(String url)
{
if(url==null)
@@ -67,28 +117,39 @@ public class OtherUtil
return null;
}
public static Game parseGame(String game)
/**
* Parses an activity from a string
*
* @param game the game, including the action such as 'playing' or 'watching'
* @return the parsed activity
*/
public static Activity parseGame(String game)
{
if(game==null || game.trim().isEmpty() || game.trim().equalsIgnoreCase("default"))
return null;
String lower = game.toLowerCase();
if(lower.startsWith("playing"))
return Game.playing(game.substring(7).trim());
return Activity.playing(makeNonEmpty(game.substring(7).trim()));
if(lower.startsWith("listening to"))
return Game.listening(game.substring(12).trim());
return Activity.listening(makeNonEmpty(game.substring(12).trim()));
if(lower.startsWith("listening"))
return Game.listening(game.substring(9).trim());
return Activity.listening(makeNonEmpty(game.substring(9).trim()));
if(lower.startsWith("watching"))
return Game.watching(game.substring(8).trim());
return Activity.watching(makeNonEmpty(game.substring(8).trim()));
if(lower.startsWith("streaming"))
{
String[] parts = game.substring(9).trim().split("\\s+", 2);
if(parts.length == 2)
{
return Game.streaming(parts[1], "https://twitch.tv/"+parts[0]);
return Activity.streaming(makeNonEmpty(parts[1]), "https://twitch.tv/"+parts[0]);
}
}
return Game.playing(game);
return Activity.playing(game);
}
public static String makeNonEmpty(String str)
{
return str == null || str.isEmpty() ? "\u200B" : str;
}
public static OnlineStatus parseStatus(String status)
@@ -99,7 +160,14 @@ public class OtherUtil
return st == null ? OnlineStatus.ONLINE : st;
}
public static String checkVersion(Prompt prompt)
public static void checkJavaVersion(Prompt prompt)
{
if(!System.getProperty("java.vm.name").contains("64"))
prompt.alert(Prompt.Level.WARNING, "Java Version",
"It appears that you may not be using a supported Java version. Please use 64-bit java.");
}
public static void checkVersion(Prompt prompt)
{
// Get current version number
String version = getCurrentVersion();
@@ -109,11 +177,8 @@ public class OtherUtil
if(latestVersion!=null && !latestVersion.equals(version))
{
prompt.alert(Prompt.Level.WARNING, "Version", String.format(NEW_VERSION_AVAILABLE, version, latestVersion));
prompt.alert(Prompt.Level.WARNING, "JMusicBot Version", String.format(NEW_VERSION_AVAILABLE, version, latestVersion));
}
// Return the current version
return version;
}
public static String getCurrentVersion()
@@ -123,13 +188,33 @@ public class OtherUtil
else
return "UNKNOWN";
}
public static String getCommandLibraryVersion()
{
try(InputStream input = OtherUtil.class.getResourceAsStream("/com/jagrosh/jmusicbot/versions.properties"))
{
if(input != null)
{
Properties properties = new Properties();
properties.load(input);
String version = properties.getProperty("jda-chewtils.version");
if(version != null && !version.trim().isEmpty())
return version.trim();
}
}
catch(IOException ignored) {}
if(JDAUtilitiesInfo.VERSION_REVISION != null && !JDAUtilitiesInfo.VERSION_REVISION.startsWith("@"))
return JDAUtilitiesInfo.VERSION;
return JDAUtilitiesInfo.VERSION_MAJOR + "." + JDAUtilitiesInfo.VERSION_MINOR;
}
public static String getLatestVersion()
{
try
{
Response response = new OkHttpClient.Builder().build()
.newCall(new Request.Builder().get().url("https://api.github.com/repos/jagrosh/MusicBot/releases/latest").build())
.newCall(new Request.Builder().get().url("https://api.github.com/repos/chrisb09/MusicBot/releases/latest").build())
.execute();
ResponseBody body = response.body();
if(body != null)
@@ -152,4 +237,23 @@ public class OtherUtil
return null;
}
}
/**
* Checks if the bot JMusicBot is being run on is supported & returns the reason if it is not.
* @return A string with the reason, or null if it is supported.
*/
public static String getUnsupportedBotReason(JDA jda)
{
if (jda.getSelfUser().getFlags().contains(User.UserFlag.VERIFIED_BOT))
return "The bot is verified. Using JMusicBot in a verified bot is not supported.";
ApplicationInfo info = jda.retrieveApplicationInfo().complete();
if (info.isBotPublic())
return "\"Public Bot\" is enabled. Using JMusicBot as a public bot is not supported. Please disable it in the "
+ "Developer Dashboard at https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot ."
+ "You may also need to disable all Installation Contexts at https://discord.com/developers/applications/"
+ jda.getSelfUser().getId() + "/installation .";
return null;
}
}
@@ -0,0 +1,138 @@
/*
* Copyright 2020 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jagrosh.jmusicbot.utils;
public class TimeUtil
{
public static String formatTime(long duration)
{
if(duration == Long.MAX_VALUE)
return "LIVE";
long seconds = Math.round(duration/1000.0);
long hours = seconds/(60*60);
seconds %= 60*60;
long minutes = seconds/60;
seconds %= 60;
return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds);
}
/**
* Parses a seek time string into milliseconds and determines if it's relative.
* Supports "colon time" (HH:MM:SS) or "unit time" (1h20m)
* @param args time string
* @return SeekTime object, or null if the string could not be parsed
*/
public static SeekTime parseTime(String args)
{
if (args.length() == 0) return null;
String timestamp = args;
boolean relative = false; // seek forward or backward
boolean isSeekingBackwards = false;
char first = timestamp.charAt(0);
if (first == '+' || first == '-')
{
relative = true;
isSeekingBackwards = first == '-';
timestamp = timestamp.substring(1);
}
long milliseconds = parseColonTime(timestamp);
if(milliseconds == -1) milliseconds = parseUnitTime(timestamp);
if(milliseconds == -1) return null;
milliseconds *= isSeekingBackwards ? -1 : 1;
return new SeekTime(milliseconds, relative);
}
/**
* @param timestamp timestamp formatted as: [+ | -] &lt;HH:MM:SS | MM:SS | SS&gt;
* @return Time in milliseconds
*/
public static long parseColonTime(String timestamp)
{
String[] timestampSplitArray = timestamp.split(":+");
if(timestampSplitArray.length > 3 )
return -1;
double[] timeUnitArray = new double[3]; // hours, minutes, seconds
for(int index = 0; index < timestampSplitArray.length; index++)
{
String unit = timestampSplitArray[index];
if (unit.startsWith("+") || unit.startsWith("-")) return -1;
unit = unit.replace(",", ".");
try
{
timeUnitArray[index + 3 - timestampSplitArray.length] = Double.parseDouble(unit);
}
catch (NumberFormatException e)
{
return -1;
}
}
return Math.round(timeUnitArray[0] * 3600000 + timeUnitArray[1] * 60000 + timeUnitArray[2] * 1000);
}
/**
*
* @param timestr time string formatted as a unit time, e.g. 20m10, 1d5h20m14s or 1h and 20m
* @return Time in milliseconds
*/
public static long parseUnitTime(String timestr)
{
timestr = timestr.replaceAll("(?i)(\\s|,|and)","")
.replaceAll("(?is)(-?\\d+|[a-z]+)", "$1 ")
.trim();
String[] vals = timestr.split("\\s+");
int time = 0;
try
{
for(int j=0; j<vals.length; j+=2)
{
int num = Integer.parseInt(vals[j]);
if(vals.length > j+1)
{
if(vals[j+1].toLowerCase().startsWith("m"))
num*=60;
else if(vals[j+1].toLowerCase().startsWith("h"))
num*=60*60;
else if(vals[j+1].toLowerCase().startsWith("d"))
num*=60*60*24;
}
time+=num*1000;
}
}
catch(Exception ex)
{
return -1;
}
return time;
}
public static class SeekTime
{
public final long milliseconds;
public final boolean relative;
private SeekTime(long milliseconds, boolean relative)
{
this.milliseconds = milliseconds;
this.relative = relative;
}
}
}
@@ -0,0 +1,193 @@
package com.jagrosh.jmusicbot.utils;
import com.sedmelluq.lava.extensions.youtuberotator.planner.*;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
import dev.lavalink.youtube.clients.Web;
import org.json.JSONObject;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.chromium.ChromiumDriverLogLevel;
import org.openqa.selenium.devtools.NetworkInterceptor;
import org.openqa.selenium.os.ExecutableFinder;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.Filter;
import org.openqa.selenium.remote.http.HttpMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.github.bonigarcia.wdm.WebDriverManager;
public class YouTubeUtil {
private final static Logger LOGGER = LoggerFactory.getLogger(YouTubeUtil.class);
public enum RoutingPlanner {
NONE,
ROTATE_ON_BAN,
LOAD_BALANCE,
NANO_SWITCH,
ROTATING_NANO_SWITCH
}
public static IpBlock parseIpBlock(String cidr) {
if (Ipv6Block.isIpv6CidrBlock(cidr))
return new Ipv6Block(cidr);
if (Ipv4Block.isIpv4CidrBlock(cidr))
return new Ipv4Block(cidr);
throw new IllegalArgumentException("Could not parse CIDR " + cidr);
}
public static AbstractRoutePlanner createRouterPlanner(RoutingPlanner routingPlanner, List<IpBlock> ipBlocks) {
switch (routingPlanner) {
case NONE:
return null;
case ROTATE_ON_BAN:
return new RotatingIpRoutePlanner(ipBlocks);
case LOAD_BALANCE:
return new BalancingIpRoutePlanner(ipBlocks);
case NANO_SWITCH:
return new NanoIpRoutePlanner(ipBlocks, true);
case ROTATING_NANO_SWITCH:
return new RotatingNanoIpRoutePlanner(ipBlocks);
default:
throw new IllegalArgumentException("Unknown RoutingPlanner value provided");
}
}
public static void GetPoTokenAndVisitorData(String userChromePath, String userChromeDriverPath, boolean headless)
{
//
// 1. Find a Chrome install & launch the WebDriver
//
WebDriverManager wdm = WebDriverManager.chromedriver();
if (userChromePath != null) {
wdm.browserVersionDetectionCommand(userChromePath + " --version");
}
else if (wdm.getBrowserPath().isEmpty())
{
// try finding a Chromium browser instead (Linux users usually install Chromium instead of Google Chrome)
wdm = WebDriverManager.chromiumdriver();
if (wdm.getBrowserPath().isEmpty()) {
LOGGER.error("Could not obtain a PO token for YouTube playback, because no Chrome browser could be found. Please install Google Chrome or Chromium!");
return;
}
}
if (userChromeDriverPath == null)
// Automatically download & set up ChromeDriver for the Chrome/Chromium version that the user has installed
wdm.setup();
else {
// Find & use the ChromeDriver executable the user provided
String path = new ExecutableFinder().find(userChromeDriverPath);
if (path == null) {
LOGGER.error("Could not obtain a PO token for YouTube playback, because the specified ChromeDriver could not be found. " +
"Please check your config's value of the chromedriverpath, or set it to \"AUTO\" to have JMusicBot automatically download ChromeDriver");
return;
}
LOGGER.info("Using ChromeDriver at {}", path);
System.setProperty("webdriver.chrome.driver", path);
}
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setBinary(userChromePath != null ? userChromePath : wdm.getBrowserPath().get().toString());
if (!headless)
chromeOptions.addArguments("--auto-open-devtools-for-tabs");
else
chromeOptions.addArguments("--headless=new");
ChromeDriverService.Builder chromeDriverBuilder = new ChromeDriverService.Builder();
if (LOGGER.isDebugEnabled())
chromeDriverBuilder.withLogLevel(ChromiumDriverLogLevel.DEBUG);
WebDriver driver = new ChromeDriver(chromeDriverBuilder.build(), chromeOptions);
//
// 2. Setup a network interceptor to intercept the player API request & obtain po token and visitor data
//
// as the network interceptor below runs on a different thread, we'll have it notify us on this/current thread
// whether retrieving the data failed (completes with the Exception) or succeeded (completes with null)
CompletableFuture<Exception> interceptorFuture = new CompletableFuture<>();
NetworkInterceptor interceptor = new NetworkInterceptor(driver,
(Filter) next -> req -> {
if (req.getMethod() != HttpMethod.POST || !req.getUri().contains("/youtubei/v1/player"))
return next.execute(req);
try {
String rawBody = Contents.string(req);
JSONObject body = new JSONObject(rawBody);
String visitorData = body
.getJSONObject("context")
.getJSONObject("client")
.getString("visitorData");
String poToken = body
.getJSONObject("serviceIntegrityDimensions")
.getString("poToken");
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
LOGGER.debug("PO token: {}", poToken);
LOGGER.debug("Visitor Data: {}", visitorData);
if (poToken.length() < 160)
LOGGER.warn("There is a high chance that the retrieved PO token will not work. Try on a different network.");
Web.setPoTokenAndVisitorData(poToken, visitorData);
interceptorFuture.complete(null);
} catch (Exception e) {
interceptorFuture.complete(e);
}
return next.execute(req);
});
//
// 3. Navigate to YouTube & start the player
//
try {
driver.get("https://www.youtube.com/embed/aqz-KE-bpKQ");
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
WebElement moviePlayer = driver.findElement(By.cssSelector("#movie_player"));
moviePlayer.click();
try {
// Wait for the network interceptor to tell us if it got the data
Exception result = interceptorFuture.get(20, TimeUnit.SECONDS);
if (result != null)
throw new Exception("Failed to read YouTube player request body", result);
} catch (TimeoutException e) {
throw new Exception("Timed out waiting for YouTube player request");
}
} catch (Exception e) {
LOGGER.error("Failed to obtain PO token for YouTube playback: {}", e.getMessage());
LOGGER.debug("Exception:", e);
} finally {
interceptor.close();
// Only quit the browser when not in headless mode. Helps with troubleshooting.
if (headless)
driver.quit();
}
}
}
@@ -0,0 +1,91 @@
package com.jagrosh.jmusicbot.utils;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import java.nio.file.Files;
/**
* A logback turbo filter, used retrieve the YouTube OAuth2 refresh token that gets logged once authorized with YouTube.
*
* @author Michaili K. <git@michaili.dev>
*/
public class YoutubeOauth2TokenHandler extends TurboFilter {
public final static org.slf4j.Logger LOGGER = LoggerFactory.getLogger(YoutubeOauth2TokenHandler.class);
private Data data;
public void init()
{
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.addTurboFilter(this);
}
public Data getData()
{
return data;
}
@Override
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t)
{
if (!logger.getName().equals("dev.lavalink.youtube.http.YoutubeOauth2Handler"))
return FilterReply.NEUTRAL;
if (format.equals("OAUTH INTEGRATION: To give youtube-source access to your account, go to {} and enter code {}"))
{
this.data = new Data((String) params[0], (String) params[1]);
return FilterReply.NEUTRAL;
}
if (format.equals("OAUTH INTEGRATION: Token retrieved successfully. Store your refresh token as this can be reused. ({})"))
{
LOGGER.info(
"Authorization successful & retrieved token! Storing the token in {}",
OtherUtil.getPath("youtubetoken.txt").toAbsolutePath()
);
try
{
Files.write(OtherUtil.getPath("youtubetoken.txt"), params[0].toString().getBytes());
}
catch (Exception e)
{
LOGGER.error(
"Failed to write the YouTube OAuth2 refresh token to storage! You will need to authorize again on the next reboot",
e
);
}
return FilterReply.DENY;
}
return FilterReply.NEUTRAL;
}
public static class Data
{
private final String authorisationUrl;
private final String code;
private Data(String authorisationUrl, String code)
{
this.authorisationUrl = authorisationUrl;
this.code = code;
}
public String getCode()
{
return code;
}
public String getAuthorisationUrl()
{
return authorisationUrl;
}
}
}
Binary file not shown.
Binary file not shown.
+161 -3
View File
@@ -28,7 +28,6 @@ owner = 0 // OWNER ID
// The prefix is used to control the commands
// If you use !!, the play command will be !!play
// If you do not set this, the prefix will be a mention of the bot (@Botname play)
// If you make this blank, the bot will not use a prefix
prefix = "@mention"
@@ -49,6 +48,14 @@ game = "DEFAULT"
status = ONLINE
// If you set this to true, the bot will use a Google account to play YouTube tracks.
// This is only needed if you are experiencing issues with YouTube playback.
// Once enabled, instructions for login will be written to the console and DMs.
// DO NOT use your main Google account! Use an alternative account.
youtubeoauth2=false
// If you set this to true, the bot will list the title of the song it is currently playing in its
// "Playing" status. Note that this will ONLY work if the bot is playing music on ONE guild;
// if the bot is playing on multiple guilds, this will not work.
@@ -56,6 +63,13 @@ status = ONLINE
songinstatus=false
// If you set this to true, the bot will maintain a single status message in the text channel
// showing the currently playing song and queue summary. Set this to false to disable
// the status message feature entirely.
statusmessages=true
// If you set this, the bot will also use this prefix in addition to
// the one provided above
@@ -99,6 +113,29 @@ stayinchannel = false
maxtime = 0
// This sets the maximum number of pages of songs that can be loaded from a YouTube
// playlist. Each page can contain up to 100 tracks. Playing a playlist with more
// pages than the maximum will stop loading after the provided number of pages.
// For example, if the max was set to 15 and a playlist contained 1850 tracks,
// only the first 1500 tracks (15 pages) would be loaded. By default, this is
// set to 10 pages (1000 tracks).
maxytplaylistpages = 10
// This sets the ratio of users that must vote to skip the currently playing song.
// Guild owners can define their own skip ratios, but this will be used if a guild
// has not defined their own skip ratio.
skipratio = 0.55
// This sets the amount of seconds the bot will stay alone on a voice channel until it
// automatically leaves the voice channel and clears the queue. If not set or set
// to any number less than or equal to zero, the bot won't leave when alone.
alonetimeuntilstop = 0
// This sets an alternative folder to be used as the Playlists folder
// This can be a relative or absolute path
@@ -113,7 +150,7 @@ updatealerts=true
// Changing this changes the lyrics provider
// Currently available providers: "A-Z Lyrics", "Genius", "MusicMatch"
// Currently available providers: "A-Z Lyrics", "Genius", "MusicMatch", "LyricsFreak"
// At the time of writing, I would recommend sticking with A-Z Lyrics or MusicMatch,
// as Genius tends to have a lot of non-song results and you might get something
// completely unrelated to what you want.
@@ -123,6 +160,126 @@ updatealerts=true
lyrics.default = "A-Z Lyrics"
// These settings allow you to configure custom aliases for all commands.
// Multiple aliases may be given, separated by commas.
//
// Example 1: Giving command "play" the alias "p":
// play = [ p ]
//
// Example 2: Giving command "search" the aliases "yts" and "find":
// search = [ yts, find ]
aliases {
// General commands
settings = [ status ]
// Music commands
lyrics = []
nowplaying = [ np, current ]
play = []
playlists = [ pls ]
queue = [ list ]
remove = [ delete ]
scsearch = []
search = [ ytsearch ]
shuffle = []
skip = [ voteskip ]
// Admin commands
prefix = [ setprefix ]
setdj = []
setskip = [ setskippercent, skippercent, setskipratio ]
settc = []
setvc = []
// DJ Commands
forceremove = [ forcedelete, modremove, moddelete, modelete ]
forceskip = [ modskip ]
movetrack = [ move ]
pause = []
playnext = []
queuetype = []
repeat = []
skipto = [ jumpto ]
stop = [ leave ]
volume = [ vol ]
}
// This sets the logging verbosity.
// Available levels: off, error, warn, info, debug, trace, all
//
// It is recommended to leave this at info. Debug log levels might help with troubleshooting,
// but can contain sensitive data.
loglevel = info
// This sets the executable path of where a Chrome or Chromium installation is located JMusicBot can use.
// JMusicBot may launch a web browser to get around anti-bot checks.
// Set this to "AUTO" to make JMusicBot attempt to find a Chrome/Chromium installation itself.
chromepath = "AUTO"
// This sets the executable path of where a ChromeDriver installation is located JMusicBot can use.
// Set this to "AUTO" to make JMusicBot automatically download the ChromeDriver version
// which matches the installed Chrome version.
chromedriverpath = "AUTO"
// This sets whether JMusicBot should launch Chrome/Chromium instances without a GUI.
// It is recommended to leave this set to true.
// If set to false, JMusicBot will not close the browser automatically (even when finished)
// and will automatically open DevTools for troubleshooting purposes.
chromeheadless = true
// Data logging (analytics) configuration
// Enable to log queue/playback events into a SQL database.
// SQLite example: jdbc:sqlite:musicbot.db
dblog.enabled = false
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
dblog.user = ""
dblog.password = ""
// This configures the routing planner to use for IP rotation on YouTube.
// When set, you need to also set an IP block below.
// The following routing strategies are available:
// NONE = Disable
// ROTATE_ON_BAN = Switch IP when currently used address gets banned.
// LOAD_BALANCE = Selects random IP for each track playback.
// NANO_SWITCH = Selects IPv6 address based on current nanosecond. Recommended for one /64 IPv6 blocks.
// ROTATING_NANO_SWITCH = Same as above, but uses the next /64 IPv6 block in the list in case of a ban.
ytroutingplanner = NONE
// The IPv4 and IPv6 blocks to use when using the above routing planner.
// This does nothing when ytroutingplanner is set to none.
// The list accepts a list of IPv4 or IPv6 CIDR blocks (not a mix of them however)
// IPv4 example: [ "192.0.2.183", "198.51.100.153/29" ]
// IPv6 example: [ "2a02:1234:5678:9abc::/64" ]
ytipblocks = [ ]
// Transforms are used to modify specific play inputs and convert them to different kinds of inputs
// These are quite complicated to use, and have limited use-cases, but in theory allow for rough
// whitelists or blacklists, roundabout loading from some sources, and customization of how things are
// requested.
//
// These are NOT EASY to set up, so if you want to use these, you'll need to look through the code
// for how they work and what fields are needed. Also, it's possible this feature might get entirely
// removed in the future if I find a better way to do this.
transforms = {}
// If you set this to true, it will enable the eval command for the bot owner. This command
// allows the bot owner to run arbitrary code from the bot's account.
//
@@ -135,6 +292,7 @@ lyrics.default = "A-Z Lyrics"
// IF SOMEONE ASKS YOU TO ENABLE THIS, THERE IS AN 11/10 CHANCE THEY ARE TRYING TO SCAM YOU
eval=false
evalengine="Nashorn"
/// END OF JMUSICBOT CONFIG ///
/// END OF JMUSICBOT CONFIG ///
@@ -29,7 +29,7 @@ public class FairQueueTest
@Test
public void differentIdentifierSize()
{
FairQueue<Q> queue = new FairQueue<>();
FairQueue<Q> queue = new FairQueue<>(null);
int size = 100;
for(int i=0; i<size; i++)
queue.add(new Q(i));
@@ -39,7 +39,7 @@ public class FairQueueTest
@Test
public void sameIdentifierSize()
{
FairQueue<Q> queue = new FairQueue<>();
FairQueue<Q> queue = new FairQueue<>(null);
int size = 100;
for(int i=0; i<size; i++)
queue.add(new Q(0));
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more