Compare commits

...
12 Commits
Author SHA1 Message Date
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
57 changed files with 2635 additions and 194 deletions

No files matched your search

+2 -1
View File
@@ -5,4 +5,5 @@
*.json
*.txt
nb*.xml
dockerfile
dockerfile
.*
+84 -109
View File
@@ -1,147 +1,122 @@
<img align="right" src="https://i.imgur.com/zrE80HY.png" height="200" width="200">
# JMusicBot Fork
A self-hosted Discord music bot fork based on JMusicBot, updated to keep working on modern Discord and JDA versions.
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.
## Demo
If you only want to temporarily join the demo server, use this link: [https://discord.gg/4cp9CvjwaW](https://discord.gg/4cp9CvjwaW)
Temporary invite: [discord.gg/4cp9CvjwaW](https://discord.gg/4cp9CvjwaW)
Permanent invite: [discord.gg/hK95396uB3](https://discord.gg/hK95396uB3)
If you want to join the demo server permanently, use this link: [https://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
# This fork
## Quick Links
We had to update to JDA 6, switch to jda-chewtils to make the bot work again.
Also, we somewhat regularly bump [lavaplayer](https://mvnrepository.com/artifact/dev.arbjerg/lavaplayer)/[lavalink](https://maven.lavalink.dev/#/releases/dev/lavalink/youtube/common) to their new versions. I will try to update this automatically in the future.
- 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)
Additionally, this fork includes the following PRs from other people:
## Features
### Support for logging into your (burner) youtube account:
- 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
[PR-1670](https://github.com/jagrosh/MusicBot/pull/1670)
## Supported Sources And Formats
### To allow building the project from source
[PR-1703](https://github.com/jagrosh/MusicBot/pull/1703)
JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats).
### Add YouTube PO token & IP rotation
https://github.com/jagrosh/MusicBot/pull/1772
Sources:
- YouTube
- SoundCloud
- Bandcamp
- Vimeo
- Twitch streams
- Local files
- HTTP URLs
Formats:
- MP3
- FLAC
- WAV
- Matroska/WebM
- MP4/M4A
- OGG streams
- AAC streams
- Stream playlists such as M3U and PLS
### For PO_Tokens: In your bot folder, edit "config.txt" and add
```
ytpotoken = "PO_TOKEN_HERE"
ytvisitordata = "VISITOR_DATA_HERE"
```
## Setup Notes
To get po_token and visitor_data, use [youtube-trusted-session-generator](https://github.com/iv-org/youtube-trusted-session-generator)
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.
If you have problems with this fork, you could also check out [this other fork](https://github.com/SeVile/MusicBot).
For the detailed version, see [docs/setup.md](/git/MusicBot/docs/setup.md).
## Requirements for 0.5+ and above
## Docker
Since we updated JDA from version 4 to 6 to fix various issues, we had to enable the MESSAGE_CONTENT intent. If you let your bot join more than 100 servers(guilds), you now need to enable this intent in the [Discord Developer Portal](https://discord.com/developers/applications).
More information can be found in the [JDA documentation](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/).
Note, however, that this is a privileged intent and will require a valid use-case for your bot to be verified in over 75 servers (it will only show up in the portal when on at least 76 servers according to the documentation).
A Docker image is published here: [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
## Easy deployment via docker
For compose examples and deployment notes, see [docs/docker.md](/git/MusicBot/docs/docker.md).
Additionally, i thought a corresponding docker image would make things better:
## YouTube Login
The image can be found on [dockerhub](https://hub.docker.com/repository/docker/chrisb09/jmusicbot/general)
This fork supports Google account login for YouTube playback workarounds.
Docker-compose:
```yaml
version: '2.2'
services:
jmusicbot:
image: chrisb09/jmusicbot:latest
container_name: jmusicbot
restart: unless-stopped
volumes:
- ./config:/jmb/config
```
although, obviously you should change `./config` to whatever path is right for your setup.
Source code for the docker image is also on [Github](https://github.com/chrisb09/jmb-container).
### Windows
If you want to run the bot on a windows host without docker, you obviously need to install java, and if you want to run it in the background something like [nssm](http://nssm.cc/) can be helpful.
## YouTube-Login
Obviously you still need to log in if you want to use youtube properly, hence you need to enable this in the config.
If you are using a pre-existing config, simply add:
```
/ 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.
Enable:
```conf
youtubeoauth2=true
```
If you start with a clean slate this line should already be present, albeit with `false` being the default.
As the author of the PR reiterates multiple times, do not use your main google account as using your account for a bot is most certainly against the ToS and thus you are endangering said account.
Do not use your main Google account. Use a burner or otherwise disposable account instead.
To add the account you simply have to follow the very basic instructions printed either in the console or sent to you from the bot via discord after you have started it, open the linked google page and paste the code.
More details are in [docs/setup.md](/git/MusicBot/docs/setup.md).
# JMusicBot
If you use PO tokens or visitor data for YouTube, those details are also covered in [docs/setup.md](/git/MusicBot/docs/setup.md).
[![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)
## DAVE / Voice Encryption
A cross-platform Discord music bot with a clean interface, and that is easy to set up and run yourself!
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.
[![Setup](http://i.imgur.com/VvXYp5j.png)](https://jmusicbot.com/setup)
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.
## 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)
## Upstream And Related Work
## 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)
This fork incorporates ideas and patches related to:
## Example
![Loading Example...](https://i.imgur.com/kVtTKvS.gif)
- [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)
## Setup
Please see the [Setup Page](https://jmusicbot.com/setup) to run this bot yourself!
If this fork does not fit your needs, another fork you may want to inspect is [SeVile/MusicBot](https://github.com/SeVile/MusicBot).
## 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)!
## Questions And Contributions
## 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.
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).
+115 -14
View File
@@ -3,10 +3,15 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.jagrosh</groupId>
<artifactId>JMusicBot</artifactId>
<version>0.5.1</version>
<version>0.6.3</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>
@@ -47,15 +55,98 @@
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>6.1.1</version>
<version>6.3.1</version>
</dependency>
<dependency>
<groupId>com.github.chrisb09</groupId>
<artifactId>JDA-Chewtils</artifactId>
<!-- <version>fix~components-v2-SNAPSHOT</version> -->
<version>jda-6.1.1-SNAPSHOT</version>
</dependency>
<dependency>
<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>2.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-commons</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-examples</artifactId>
<version>2.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pw.chew</groupId>
<artifactId>jda-chewtils-menu</artifactId>
<version>2.2.1</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>
@@ -75,7 +166,7 @@
<dependency>
<groupId>com.github.jagrosh</groupId>
<artifactId>JLyrics</artifactId>
<version>master-SNAPSHOT</version>
<version>master</version>
</dependency>
<dependency>
<groupId>com.dunctebot</groupId>
@@ -89,6 +180,16 @@
<artifactId>logback-classic</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.1.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
@@ -97,7 +198,7 @@
<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>
@@ -22,6 +22,8 @@ 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;
@@ -44,7 +46,9 @@ 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;
@@ -64,8 +68,23 @@ public class Bot
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()
@@ -103,11 +122,21 @@ 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;
@@ -154,6 +183,8 @@ public class Bot
}
if(gui!=null)
gui.dispose();
if(dataLogService != null)
dataLogService.shutdown();
System.exit(0);
}
@@ -50,6 +50,9 @@ public class BotConfig
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;
@@ -94,6 +97,7 @@ public class BotConfig
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");
@@ -112,6 +116,10 @@ public class BotConfig
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;
@@ -260,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()
{
@@ -320,6 +348,11 @@ public class BotConfig
{
return songInGame;
}
public boolean useStatusMessages()
{
return statusMessages;
}
public String getPlaylistsFolder()
{
@@ -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;
@@ -124,6 +128,7 @@ public class JMusicBot
.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();
@@ -149,6 +154,12 @@ public class JMusicBot
+ "on https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot");
}
}
catch(IllegalStateException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex.getMessage());
LOG.error("Failed to initialize DAVE protocol support", ex);
System.exit(1);
}
catch(IllegalArgumentException ex)
{
prompt.alert(Prompt.Level.ERROR, "JMusicBot", "Some aspect of the configuration is "
@@ -164,11 +175,27 @@ public class JMusicBot
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(),
LoggedAboutCmd aboutCommand = new LoggedAboutCmd(bot, Color.BLUE.brighter(),
"a music bot that is [easy to host yourself!](https://github.com/jagrosh/MusicBot) (v" + OtherUtil.getCurrentVersion() + ")",
new String[]{"High-quality music playback", "FairQueue™ Technology", "Easy to host yourself"},
RECOMMENDED_PERMS);
@@ -185,7 +212,7 @@ public class JMusicBot
.setLinkedCacheSize(200)
.setGuildSettingsManager(settings)
.addCommands(aboutCommand,
new PingCommand(),
new LoggedPingCmd(bot),
new SettingsCmd(bot),
new LyricsCmd(bot),
@@ -28,6 +28,7 @@ import net.dv8tion.jda.api.events.session.ShutdownEvent;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@@ -108,14 +109,24 @@ public class Listener extends ListenerAdapter
@Override
public void onMessageDelete(MessageDeleteEvent event)
{
if(event.isFromGuild())
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
@@ -40,7 +40,6 @@ 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;
@@ -158,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())
@@ -174,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)
@@ -190,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.
@@ -226,6 +231,15 @@ 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);
}
}
@@ -290,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;
@@ -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,6 +20,8 @@ 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.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
@@ -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,12 +57,14 @@ 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)
@@ -71,6 +76,7 @@ public abstract class MusicCommand extends Command
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,6 +84,7 @@ 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;
}
@@ -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.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,31 +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
{
@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.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,31 +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
{
@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,45 @@
/*
* 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 org.json.JSONObject;
import java.awt.Color;
public class LoggedAboutCmd extends AboutCommand
{
private final Bot bot;
public LoggedAboutCmd(Bot bot, Color color, String description, String[] features, net.dv8tion.jda.api.Permission[] perms)
{
super(color, description, features, perms);
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("about", true).toString());
}
}
}
@@ -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,9 +15,10 @@
*/
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;
@@ -27,17 +28,19 @@ import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.json.JSONObject;
/**
*
* @author John Grosh <john.a.grosh@gmail.com>
*/
public class SettingsCmd extends Command
public class SettingsCmd extends LoggedCommand
{
private final static String EMOJI = "\uD83C\uDFA7"; // 🎧
public SettingsCmd(Bot bot)
{
super(bot);
this.name = "settings";
this.help = "shows the bots settings";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -45,9 +48,18 @@ public class SettingsCmd extends Command
}
@Override
protected void execute(CommandEvent event)
public void doCommand(CommandEvent event)
{
Settings s = event.getClient().getSettingsFor(event.getGuild());
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()))
@@ -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.utils.messages.MessageCreateData;
import org.json.JSONObject;
/**
*
@@ -46,10 +48,14 @@ public class NowplayingCmd extends MusicCommand
{
event.getMessage().getChannel().sendMessage(handler.getNoMusicPlaying(event.getJDA())).queue();
bot.getNowplayingHandler().clearLastNPMessage(event.getGuild());
CommandLogContext.setMeta(new JSONObject().put("playing", false));
}
else
{
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,6 +23,7 @@ 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;
@@ -33,6 +34,7 @@ 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;
/**
*
@@ -86,6 +88,7 @@ public class QueueCmd extends MusicCommand
if(nowp!=null)
bot.getNowplayingHandler().setLastNPMessage(m);
});
CommandLogContext.setMeta(new JSONObject().put("queue_size", 0));
return;
}
String[] songs = new String[list.size()];
@@ -97,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());
}
}
@@ -21,10 +21,12 @@ 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;
/**
@@ -53,6 +55,7 @@ public class SeekCmd extends MusicCommand
if (!playingTrack.isSeekable())
{
event.replyError("This track is not seekable.");
CommandLogContext.setError("not_seekable");
return;
}
@@ -60,6 +63,7 @@ public class SeekCmd extends MusicCommand
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;
}
@@ -68,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;
}
@@ -78,6 +83,7 @@ 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;
}
@@ -89,8 +95,16 @@ public class SeekCmd extends MusicCommand
{
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));
}
}
}
@@ -19,12 +19,14 @@ 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.channel.ChannelType;
import net.dv8tion.jda.api.utils.FileUpload;
import org.json.JSONObject;
/**
*
@@ -35,11 +37,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);
@@ -47,8 +47,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)
@@ -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 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.channel.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);
}
}
+17
View File
@@ -63,6 +63,13 @@ youtubeoauth2=false
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
@@ -230,6 +237,16 @@ chromedriverpath = "AUTO"
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: