Compare commits

...
78 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
68 changed files with 3490 additions and 229 deletions

No files matched your search

+8 -1
View File
@@ -53,5 +53,12 @@ body:
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 running the latest version of the bot: ![Release](https://img.shields.io/github/release/jagrosh/MusicBot.svg)"
- 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
+3 -1
View File
@@ -4,4 +4,6 @@
/test/
*.json
*.txt
nb*.xml
nb*.xml
dockerfile
.*
+110 -48
View File
@@ -1,60 +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://dl.circleci.com/status-badge/img/gh/jagrosh/MusicBot/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/jagrosh/MusicBot/tree/master)
[![Build and Test](https://github.com/jagrosh/MusicBot/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/jagrosh/MusicBot/actions/workflows/build-and-test.yml)
[![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://jmusicbot.com/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
* Supports many sites, including Youtube, Soundcloud, and more
* Supports many online radio/streams
* Supports local files
* Playlist support (both web/youtube, and local)
## Supported sources and formats
JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats):
### Sources
* YouTube
* SoundCloud
* Bandcamp
* Vimeo
* Twitch streams
* Local files
* HTTP URLs
### Formats
* MP3
* FLAC
* WAV
* Matroska/WebM (AAC, Opus or Vorbis codecs)
* MP4/M4A (AAC codec)
* OGG streams (Opus, Vorbis and FLAC codecs)
* AAC streams
* Stream playlists (M3U and PLS)
- 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
## Example
![Loading Example...](https://i.imgur.com/kVtTKvS.gif)
## Supported Sources And Formats
## Setup
Please see the [Setup Page](https://jmusicbot.com/setup) to run this bot yourself!
JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats).
## Questions/Suggestions/Bug Reports
**Please read the [Issues List](https://github.com/jagrosh/MusicBot/issues) before suggesting a feature**. If you have a question, need troubleshooting help, or want to brainstorm a new feature, please start a [Discussion](https://github.com/jagrosh/MusicBot/discussions). If you'd like to suggest a feature or report a reproducible bug, please open an [Issue](https://github.com/jagrosh/MusicBot/issues) on this repository. 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)!
Sources:
- YouTube
- SoundCloud
- Bandcamp
- Vimeo
- Twitch streams
- Local files
- HTTP URLs
## 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.
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).
+164 -15
View File
@@ -3,10 +3,15 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.jagrosh</groupId>
<artifactId>JMusicBot</artifactId>
<version>Snapshot</version>
<version>0.6.5</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>m2-chew</id>
<name>Maven Chewtral</name>
<url>https://m2.chew.pro/releases</url>
</repository>
<repository>
<id>dv8tion</id>
<name>m2-dv8tion</name>
@@ -14,14 +19,17 @@
</repository>
<repository>
<id>central</id>
<name>bintray</name>
<url>https://jcenter.bintray.com</url>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<snapshots>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
@@ -35,6 +43,11 @@
<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>
@@ -42,30 +55,118 @@
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>4.4.1_353</version>
<version>6.3.1</version>
</dependency>
<dependency>
<groupId>com.jagrosh</groupId>
<artifactId>jda-utilities</artifactId>
<version>3.0.5</version>
<type>pom</type>
<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>2.2.1</version>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>dev.arbjerg</groupId>
<artifactId>lavaplayer-ext-youtube-rotator</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>dev.lavalink.youtube</groupId>
<artifactId>common</artifactId>
<version>1.4.0</version>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.github.jagrosh</groupId>
<artifactId>JLyrics</artifactId>
<version>master-SNAPSHOT</version>
<version>master</version>
</dependency>
<dependency>
<groupId>com.dunctebot</groupId>
@@ -77,7 +178,17 @@
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.13</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>
@@ -87,8 +198,35 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
<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>
@@ -106,6 +244,16 @@
</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>
@@ -149,6 +297,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jda.chewtils.version>2.2.1</jda.chewtils.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
+1 -1
View File
@@ -9,7 +9,7 @@ DOWNLOAD=true
LOOP=true
download() {
if [ $DOWNLOAD == true ]; then
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/')
@@ -0,0 +1 @@
jda-chewtils.version=${jda.chewtils.version}
+41 -1
View File
@@ -22,10 +22,13 @@ 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 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;
@@ -43,7 +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;
@@ -56,12 +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()
@@ -99,10 +122,25 @@ 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()
{
@@ -145,6 +183,8 @@ public class Bot
}
if(gui!=null)
gui.dispose();
if(dataLogService != null)
dataLogService.shutdown();
System.exit(0);
}
@@ -18,11 +18,16 @@ package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.entities.Prompt;
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.util.List;
import java.util.stream.Collectors;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
@@ -41,8 +46,13 @@ public class BotConfig
private Path path = null;
private String token, prefix, altprefix, helpWord, playlistsFolder, logLevel,
successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji,
evalEngine;
private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots;
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;
@@ -84,8 +94,10 @@ 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");
@@ -96,9 +108,18 @@ public class BotConfig
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;
@@ -247,6 +268,26 @@ public class BotConfig
{
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()
{
@@ -293,6 +334,11 @@ public class BotConfig
return helpWord;
}
public boolean useYoutubeOauth2()
{
return youtubeOauth2;
}
public boolean getStay()
{
return stayInChannel;
@@ -302,6 +348,11 @@ public class BotConfig
{
return songInGame;
}
public boolean useStatusMessages()
{
return statusMessages;
}
public String getPlaylistsFolder()
{
@@ -323,6 +374,31 @@ public class BotConfig
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;
@@ -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,10 +29,13 @@ 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.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;
@@ -40,6 +44,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import static com.jagrosh.jmusicbot.utils.YouTubeUtil.GetPoTokenAndVisitorData;
/**
*
* @author John Grosh (jagrosh)
@@ -47,10 +53,10 @@ import ch.qos.logback.classic.Level;
public class JMusicBot
{
public final static Logger LOG = LoggerFactory.getLogger(JMusicBot.class);
public final static Permission[] RECOMMENDED_PERMS = {Permission.MESSAGE_READ, Permission.MESSAGE_WRITE, Permission.MESSAGE_HISTORY, Permission.MESSAGE_ADD_REACTION,
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.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};
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
@@ -118,10 +124,11 @@ public class JMusicBot
{
JDA jda = JDABuilder.create(config.getToken(), Arrays.asList(INTENTS))
.enableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE)
.disableCache(CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS, CacheFlag.EMOTE, CacheFlag.ONLINE_STATUS)
.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();
@@ -147,11 +154,10 @@ public class JMusicBot
+ "on https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot");
}
}
catch (LoginException ex)
catch(IllegalStateException 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());
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex.getMessage());
LOG.error("Failed to initialize DAVE protocol support", ex);
System.exit(1);
}
catch(IllegalArgumentException ex)
@@ -166,13 +172,31 @@ public class JMusicBot
+ "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
AboutCommand aboutCommand = new AboutCommand(Color.BLUE.brighter(),
"a music bot that is [easy to host yourself!](https://github.com/jagrosh/MusicBot) (v" + OtherUtil.getCurrentVersion() + ")",
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);
@@ -188,7 +212,7 @@ public class JMusicBot
.setLinkedCacheSize(200)
.setGuildSettingsManager(settings)
.addCommands(aboutCommand,
new PingCommand(),
new LoggedPingCmd(bot),
new SettingsCmd(bot),
new LyricsCmd(bot),
@@ -17,15 +17,18 @@ package com.jagrosh.jmusicbot;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import java.util.concurrent.TimeUnit;
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.VoiceChannel;
import net.dv8tion.jda.api.events.ReadyEvent;
import net.dv8tion.jda.api.events.ShutdownEvent;
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.guild.GuildMessageDeleteEvent;
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;
@@ -85,18 +88,45 @@ public class Listener extends ListenerAdapter
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
@@ -20,7 +20,6 @@ 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.jagrosh.jmusicbot.utils.OtherUtil;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
@@ -37,11 +36,12 @@ import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack;
import java.nio.ByteBuffer;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.MessageBuilder;
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.Message;
import net.dv8tion.jda.api.entities.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
@@ -54,6 +54,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
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<>();
@@ -119,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()
@@ -156,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())
@@ -172,6 +176,8 @@ 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 && repeatMode != RepeatMode.OFF)
@@ -188,6 +194,7 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
if(!playFromDefault())
{
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.
@@ -203,8 +210,20 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
}
@Override
public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) {
LoggerFactory.getLogger("AudioHandler").error("Track " + track.getIdentifier() + " has failed to play", exception);
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
@@ -212,18 +231,27 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
{
votes.clear();
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().getAsMention()+"...**"));
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());
RequestMetadata rm = getRequestMetadata();
@@ -264,10 +292,10 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
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...**"))
.setEmbeds(new EmbedBuilder()
.setTitle("No music playing")
@@ -276,6 +304,84 @@ public class AudioHandler extends AudioEventAdapter implements AudioSendHandler
.build()).build();
}
public MessageCreateData getStatusMessage(JDA 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)
{
try
{
eb.setTitle(track.getInfo().title, track.getInfo().uri);
}
catch(Exception e)
{
eb.setTitle(track.getInfo().title);
}
}
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;
@@ -27,9 +27,11 @@ 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.TextChannel;
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;
/**
*
@@ -54,7 +56,7 @@ public class NowplayingHandler
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)
@@ -81,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());
@@ -89,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)
{
@@ -105,7 +107,7 @@ public class NowplayingHandler
// update bot status if applicable
if(bot.getConfig().getSongInStatus())
{
if(track!=null && bot.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inVoiceChannel()).count()<=1)
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();
@@ -17,6 +17,9 @@ 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;
@@ -29,8 +32,21 @@ 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;
/**
*
@@ -38,18 +54,31 @@ import net.dv8tion.jda.api.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()
{
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);
@@ -66,7 +95,42 @@ public class PlayerManager extends DefaultAudioPlayerManager
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;
@@ -29,15 +29,17 @@ import java.util.regex.Pattern;
*/
public class RequestMetadata
{
public static final RequestMetadata EMPTY = new RequestMetadata(null, null);
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)
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()
@@ -47,7 +49,20 @@ public class RequestMetadata
public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event)
{
return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri));
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
@@ -70,10 +85,28 @@ public class RequestMetadata
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
{
@@ -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;
}
}
@@ -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;
import net.dv8tion.jda.api.Permission;
/**
@@ -24,8 +28,11 @@ import net.dv8tion.jda.api.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());
}
}
@@ -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,9 +20,11 @@ import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import org.json.JSONObject;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
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,23 +57,26 @@ 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.getAsMention())+" to use that!");
logCommand(event, "ERROR", new JSONObject().put("error_reason", "not_listening"));
return;
}
@@ -78,10 +84,11 @@ public abstract class MusicCommand extends Command
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().inVoiceChannel())
if(!event.getGuild().getSelfMember().getVoiceState().inAudioChannel())
{
try
{
@@ -90,13 +97,41 @@ public abstract class MusicCommand extends Command
catch(PermissionException ex)
{
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());
}
}
@@ -18,7 +18,9 @@ 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;
/**
*
@@ -28,6 +30,7 @@ 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>";
@@ -35,24 +38,30 @@ public class PrefixCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
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);
}
}
}
@@ -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.AdminCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.settings.QueueType;
import com.jagrosh.jmusicbot.settings.Settings;
import org.json.JSONObject;
/**
*
@@ -30,7 +32,7 @@ public class QueueTypeCmd extends AdminCommand
{
public QueueTypeCmd(Bot bot)
{
super();
super(bot);
this.name = "queuetype";
this.help = "changes the queue type";
this.arguments = "[" + String.join("|", QueueType.getNames()) + "]";
@@ -38,16 +40,18 @@ public class QueueTypeCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
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;
}
@@ -58,6 +62,7 @@ public class QueueTypeCmd extends AdminCommand
catch (IllegalArgumentException e)
{
event.replyError("Invalid queue type. Valid types are: [" + String.join("|", QueueType.getNames()) + "]");
CommandLogContext.setError("invalid_queue_type");
return;
}
@@ -71,5 +76,6 @@ public class QueueTypeCmd extends AdminCommand
}
event.reply(value.getEmoji() + " Queue type was set to `" + value.getUserFriendlyName() + "`.");
CommandLogContext.setMeta(new JSONObject().put("before", before.name()).put("after", value.name()));
}
}
@@ -20,9 +20,11 @@ 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.api.entities.Role;
import org.json.JSONObject;
/**
*
@@ -32,6 +34,7 @@ public class SetdjCmd extends AdminCommand
{
public SetdjCmd(Bot bot)
{
super(bot);
this.name = "setdj";
this.help = "sets the DJ role for certain music commands";
this.arguments = "<rolename|NONE>";
@@ -39,30 +42,40 @@ public class SetdjCmd extends AdminCommand
}
@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()));
}
}
}
@@ -20,9 +20,11 @@ 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.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.json.JSONObject;
/**
*
@@ -32,6 +34,7 @@ public class SettcCmd extends AdminCommand
{
public SettcCmd(Bot bot)
{
super(bot);
this.name = "settc";
this.help = "sets the text channel for music commands";
this.arguments = "<channel|NONE>";
@@ -39,30 +42,41 @@ public class SettcCmd extends AdminCommand
}
@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()));
}
}
}
@@ -20,9 +20,11 @@ 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.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.json.JSONObject;
/**
*
@@ -32,6 +34,7 @@ public class SetvcCmd extends AdminCommand
{
public SetvcCmd(Bot bot)
{
super(bot);
this.name = "setvc";
this.help = "sets the voice channel for playing music";
this.arguments = "<channel|NONE>";
@@ -39,30 +42,41 @@ public class SetvcCmd extends AdminCommand
}
@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).getAsMention());
CommandLogContext.setMeta(new JSONObject().put("before", before).put("after", list.get(0).getId()));
}
}
}
@@ -18,7 +18,9 @@ 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;
/**
*
@@ -28,6 +30,7 @@ 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>";
@@ -35,7 +38,7 @@ public class SkipratioCmd extends AdminCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
try
{
@@ -43,15 +46,21 @@ public class SkipratioCmd extends AdminCommand
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");
}
}
}
@@ -21,10 +21,12 @@ 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;
@@ -53,6 +55,7 @@ public class ForceRemoveCmd extends DJCommand
if (event.getArgs().isEmpty())
{
event.replyError("You need to mention a user!");
CommandLogContext.setError("missing_user");
return;
}
@@ -60,6 +63,7 @@ public class ForceRemoveCmd extends DJCommand
if (handler.getQueue().isEmpty())
{
event.replyError("There is nothing in the queue!");
CommandLogContext.setError("empty_queue");
return;
}
@@ -70,6 +74,7 @@ public class ForceRemoveCmd extends DJCommand
if(found.isEmpty())
{
event.replyError("Unable to find the user!");
CommandLogContext.setError("user_not_found");
return;
}
else if(found.size()>1)
@@ -107,14 +112,26 @@ public class ForceRemoveCmd extends DJCommand
private void removeAllEntries(User target, CommandEvent event)
{
int count = ((AudioHandler) event.getGuild().getAudioManager().getSendingHandler()).getQueue().removeAll(target.getIdLong());
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());
}
}
@@ -20,7 +20,9 @@ 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.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -44,6 +46,13 @@ public class ForceskipCmd extends DJCommand
RequestMetadata rm = handler.getRequestMetadata();
event.reply(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", "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.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.
@@ -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,12 +49,14 @@ 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;
}
@@ -62,24 +67,36 @@ public class MoveTrackCmd extends DJCommand
{
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(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;
/**
*
@@ -42,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());
}
}
@@ -21,6 +21,7 @@ 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;
@@ -28,6 +29,7 @@ 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.api.entities.Message;
import org.json.JSONObject;
/**
*
@@ -52,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(">")
@@ -82,13 +86,18 @@ public class PlaynextCmd extends DJCommand
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+ 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, RequestMetadata.fromResultHandler(track, event)))+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
+"** (`"+ 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
@@ -114,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));
}
@@ -126,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,8 +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;
/**
*
@@ -37,13 +39,13 @@ public class RepeatCmd extends DJCommand
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)
{
String args = event.getArgs();
RepeatMode value;
Settings settings = event.getClient().getSettingsFor(event.getGuild());
RepeatMode before = settings.getRepeatMode();
if(args.isEmpty())
{
if(settings.getRepeatMode() == RepeatMode.OFF)
@@ -66,12 +68,11 @@ public class RepeatCmd extends DJCommand
else
{
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.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;
/**
*
@@ -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;
/**
*
@@ -39,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;
/**
*
@@ -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,29 +15,32 @@
*/
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 com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
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(Bot bot)
{
super(bot);
this.name = "settings";
this.help = "shows the bots settings";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -45,13 +48,22 @@ public class SettingsCmd extends Command
}
@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(FormatUtil.filter(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());
@@ -70,9 +82,9 @@ public class SettingsCmd extends Command
+ "\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()
+ event.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inAudioChannel()).count()
+ " audio connections", null);
event.getChannel().sendMessage(builder.setEmbeds(ebuilder.build()).build()).queue();
event.getMessage().getChannel().sendMessage(builder.setEmbeds(ebuilder.build()).build()).queue();
}
}
@@ -20,8 +20,10 @@ import com.jagrosh.jlyrics.LyricsClient;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import org.json.JSONObject;
/**
*
@@ -44,6 +46,7 @@ public class LyricsCmd extends MusicCommand
@Override
public void doCommand(CommandEvent event)
{
CommandLogContext.setManualLogging();
String title;
if(event.getArgs().isEmpty())
{
@@ -53,6 +56,7 @@ public class LyricsCmd extends MusicCommand
else
{
event.replyError("There must be music playing to use that!");
logCommandEvent(event, "ERROR", new JSONObject().put("error_reason", "no_music_playing"));
return;
}
}
@@ -64,6 +68,7 @@ public class LyricsCmd extends MusicCommand
if(lyrics == null)
{
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;
}
@@ -95,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 com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import org.json.JSONObject;
/**
*
@@ -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));
}
}
}
@@ -30,12 +30,14 @@ 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.utils.FormatUtil;
import java.util.concurrent.TimeUnit;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.exceptions.PermissionException;
import org.json.JSONObject;
/**
*
@@ -64,6 +66,7 @@ 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();
@@ -73,9 +76,20 @@ public class PlayCmd extends MusicCommand
{
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;
}
StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Play Commands:\n");
@@ -84,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(">")
@@ -111,10 +126,14 @@ public class PlayCmd extends MusicCommand
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+ 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, RequestMetadata.fromResultHandler(track, event)))+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
+"** (`"+ 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))
@@ -137,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)
@@ -146,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, RequestMetadata.fromResultHandler(track, event)));
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]++;
}
});
@@ -179,11 +202,13 @@ public class PlayCmd extends MusicCommand
{
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, RequestMetadata.fromResultHandler(at, event))), () -> {
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;
/**
*
@@ -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()));
}
}
}
@@ -23,15 +23,18 @@ 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.QueueType;
import com.jagrosh.jmusicbot.settings.RepeatMode;
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.jagrosh.jmusicbot.utils.TimeUtil;
import net.dv8tion.jda.api.MessageBuilder;
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;
/**
*
@@ -75,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!")
.setEmbeds((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build();
event.reply(built, m ->
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()];
@@ -96,6 +100,11 @@ public class QueueCmd extends MusicCommand
}
Settings settings = event.getClient().getSettingsFor(event.getGuild());
long fintotal = total;
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())
@@ -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.api.Permission;
import net.dv8tion.jda.api.entities.User;
import org.json.JSONObject;
/**
*
@@ -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");
}
}
}
@@ -29,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.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import org.json.JSONObject;
/**
*
@@ -64,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()+"]`",
@@ -91,13 +95,21 @@ public class SearchCmd extends MusicCommand
{
m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+ 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, RequestMetadata.fromResultHandler(track, event)))+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
+"** (`"+ 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
@@ -113,10 +125,17 @@ public class SearchCmd extends MusicCommand
{
event.replyWarning("This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
+ 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, RequestMetadata.fromResultHandler(track, event)))+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(), 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));
@@ -130,12 +149,17 @@ public class SearchCmd extends MusicCommand
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
@@ -145,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());
}
}
@@ -18,10 +18,15 @@ 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;
/**
@@ -29,6 +34,8 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
*/
public class SeekCmd extends MusicCommand
{
private final static Logger LOG = LoggerFactory.getLogger("Seeking");
public SeekCmd(Bot bot)
{
super(bot);
@@ -48,13 +55,15 @@ public class SeekCmd extends MusicCommand
if (!playingTrack.isSeekable())
{
event.replyError("This track is not seekable.");
CommandLogContext.setError("not_seekable");
return;
}
if (!DJCommand.checkDJPermission(event) && playingTrack.getUserData(Long.class) != event.getAuthor().getIdLong())
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;
}
@@ -63,6 +72,7 @@ public class SeekCmd extends MusicCommand
if (seekTime == null)
{
event.replyError("Invalid seek! Expected format: " + arguments + "\nExamples: `1:02:23` `+1:10` `-90`, `1h10m`, `+90s`");
CommandLogContext.setError("invalid_seek");
return;
}
@@ -73,20 +83,28 @@ public class SeekCmd extends MusicCommand
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;
}
else
try
{
try
{
playingTrack.setPosition(seekMilliseconds);
}
catch (Exception e)
{
event.replyError("An error occurred while trying to seek!");
e.printStackTrace();
return;
}
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;
/**
*
@@ -40,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;
}
}
@@ -20,7 +20,9 @@ import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.MusicCommand;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import org.json.JSONObject;
/**
*
@@ -50,6 +52,13 @@ public class SkipCmd extends MusicCommand
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
@@ -63,15 +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 * skipRatio);
msg += skippers + " votes, " + required + "/" + listeners + " needed]`";
CommandLogContext.setMeta(new JSONObject()
.put("votes", skippers)
.put("required", required)
.put("listeners", listeners));
if(skippers>=required)
{
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,11 +28,9 @@ 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>";
@@ -39,30 +39,34 @@ public class AutoplaylistCmd extends OwnerCommand
}
@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));
}
}
}
@@ -16,14 +16,16 @@
package com.jagrosh.jmusicbot.commands.owner;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.commons.JDAUtilitiesInfo;
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.ChannelType;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.utils.FileUpload;
import org.json.JSONObject;
/**
*
@@ -34,11 +36,9 @@ 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"};
private final Bot bot;
public DebugCmd(Bot bot)
{
this.bot = bot;
super(bot);
this.name = "debug";
this.help = "shows debug info";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -46,8 +46,9 @@ public class DebugCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
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)
@@ -65,7 +66,7 @@ public class DebugCmd extends OwnerCommand
.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(JDAUtilitiesInfo.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);
@@ -79,8 +80,8 @@ public class DebugCmd extends OwnerCommand
sb.append("\n```");
if(event.isFromType(ChannelType.PRIVATE)
|| event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ATTACH_FILES))
event.getChannel().sendFile(sb.toString().getBytes(), "debug_information.txt").queue();
|| 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,7 +20,9 @@ import javax.script.ScriptEngineManager;
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.OwnerCommand;
import net.dv8tion.jda.api.entities.ChannelType;
import com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import org.json.JSONObject;
/**
*
@@ -28,12 +30,11 @@ import net.dv8tion.jda.api.entities.ChannelType;
*/
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);
@@ -42,13 +43,15 @@ public class EvalCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
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);
@@ -61,11 +64,21 @@ public class EvalCmd extends OwnerCommand
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,11 +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.datalog.CommandLogContext;
import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist;
import org.json.JSONObject;
/**
*
@@ -29,10 +30,9 @@ 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>";
@@ -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,13 +71,14 @@ public class PlaylistCmd extends OwnerCommand
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
String pname = event.getArgs().replaceAll("\\s+", "_");
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)
{
@@ -83,14 +86,19 @@ public class PlaylistCmd extends OwnerCommand
{
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");
}
}
}
@@ -98,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";
@@ -106,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");
}
}
}
@@ -130,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";
@@ -138,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();
@@ -166,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");
}
}
}
@@ -191,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";
@@ -198,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()));
}
}
}
@@ -20,8 +20,10 @@ 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.api.entities.Icon;
import org.json.JSONObject;
/**
*
@@ -31,6 +33,7 @@ public class SetavatarCmd extends OwnerCommand
{
public SetavatarCmd(Bot bot)
{
super(bot);
this.name = "setavatar";
this.help = "sets the avatar of the bot";
this.arguments = "<url>";
@@ -39,8 +42,9 @@ public class SetavatarCmd extends OwnerCommand
}
@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())
@@ -53,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());
}
}
@@ -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 net.dv8tion.jda.api.entities.Activity;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SetgameCmd extends OwnerCommand
{
public SetgameCmd(Bot bot)
{
super(bot);
this.name = "setgame";
this.help = "sets the game the bot is playing";
this.arguments = "[action] [game]";
@@ -41,7 +44,7 @@ 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
@@ -49,10 +52,14 @@ public class SetgameCmd extends OwnerCommand
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");
}
}
@@ -60,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";
@@ -68,12 +76,13 @@ 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
@@ -81,10 +90,15 @@ public class SetgameCmd extends OwnerCommand
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");
}
}
}
@@ -93,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";
@@ -101,11 +116,12 @@ 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();
@@ -113,8 +129,10 @@ public class SetgameCmd extends OwnerCommand
{
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");
}
}
}
@@ -123,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";
@@ -131,11 +150,12 @@ 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();
@@ -143,8 +163,10 @@ public class SetgameCmd extends OwnerCommand
{
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 com.jagrosh.jmusicbot.datalog.CommandLogContext;
import net.dv8tion.jda.api.exceptions.RateLimitedException;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SetnameCmd extends OwnerCommand
{
public SetnameCmd(Bot bot)
{
super(bot);
this.name = "setname";
this.help = "sets the name of the bot";
this.arguments = "<name>";
@@ -36,21 +39,24 @@ public class SetnameCmd extends OwnerCommand
}
@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");
}
}
}
@@ -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 net.dv8tion.jda.api.OnlineStatus;
import org.json.JSONObject;
/**
*
@@ -28,6 +30,7 @@ public class SetstatusCmd extends OwnerCommand
{
public SetstatusCmd(Bot bot)
{
super(bot);
this.name = "setstatus";
this.help = "sets the status the bot displays";
this.arguments = "<status>";
@@ -36,21 +39,27 @@ public class SetstatusCmd extends OwnerCommand
}
@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,11 +27,9 @@ 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);
@@ -37,8 +37,9 @@ public class ShutdownCmd extends OwnerCommand
}
@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);
}
}
@@ -20,8 +20,8 @@ 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.TextChannel;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
*
@@ -24,6 +24,7 @@ import java.util.HashMap;
import net.dv8tion.jda.api.entities.Guild;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
@@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory;
*/
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;
@@ -63,17 +65,17 @@ public class SettingsManager implements GuildSettingsManager<Settings>
} catch (NoSuchFileException e) {
// create an empty json file
try {
LoggerFactory.getLogger("Settings").info("serversettings.json will be created in " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
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) {
LoggerFactory.getLogger("Settings").warn("Failed to create new settings file: "+ex);
LOG.warn("Failed to create new settings file: "+ex);
}
return;
} catch(IOException | JSONException e) {
LoggerFactory.getLogger("Settings").warn("Failed to load server settings: "+e);
LOG.warn("Failed to load server settings: "+e);
}
LoggerFactory.getLogger("Settings").info("serversettings.json loaded from " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
LOG.info("serversettings.json loaded from " + OtherUtil.getPath("serversettings.json").toAbsolutePath());
}
/**
@@ -127,7 +129,7 @@ public class SettingsManager implements GuildSettingsManager<Settings>
try {
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);
}
}
}
@@ -18,9 +18,9 @@ package com.jagrosh.jmusicbot.utils;
import com.jagrosh.jmusicbot.audio.RequestMetadata.UserInfo;
import java.util.List;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
/**
*
@@ -94,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)
@@ -15,6 +15,7 @@
*/
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.*;
@@ -23,6 +24,7 @@ import java.net.URL;
import java.net.URLConnection;
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;
@@ -43,7 +45,11 @@ 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\\";
/**
@@ -182,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)
@@ -224,7 +250,9 @@ public class OtherUtil
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.";
+ "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,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;
}
}
}
+68
View File
@@ -48,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.
@@ -55,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
@@ -200,6 +215,59 @@ aliases {
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