Merge pull request #3 from bensherriff/develop
Implemented Rust migration
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
export POSTGRES_USER=
|
DISCORD_TOKEN=
|
||||||
export POSTGRES_PASSWORD=
|
RUST_LOG=warn,siren=info
|
||||||
export POSTGRES_DB=
|
POSTGRES_USER=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
POSTGRES_DB=
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,8 +1,8 @@
|
|||||||
.idea/
|
|
||||||
**/target/
|
|
||||||
**/data/
|
|
||||||
**/app/
|
|
||||||
**/settings.json
|
|
||||||
**/logs/
|
|
||||||
**/audio/
|
|
||||||
.env
|
.env
|
||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
audio/
|
||||||
|
logs/
|
||||||
|
settings.json
|
||||||
|
|||||||
2130
Cargo.lock
generated
Normal file
2130
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
Normal file
34
Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "siren"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
log = "0.4.19"
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
|
||||||
|
[dependencies.serenity]
|
||||||
|
version = "0.11.6"
|
||||||
|
default-features = false
|
||||||
|
features = ["client", "gateway", "rustls_backend", "model", "voice", "cache", "framework", "standard_framework"]
|
||||||
|
|
||||||
|
[dependencies.songbird]
|
||||||
|
version = "0.3.2"
|
||||||
|
features = ["builtin-queue", "yt-dlp"]
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
version = "1.29.1"
|
||||||
|
features = ["macros", "rt-multi-thread"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "0.11.18"
|
||||||
|
default-features = false
|
||||||
|
features = ["json", "rustls-tls"]
|
||||||
26
Dockerfile
26
Dockerfile
@@ -1,10 +1,20 @@
|
|||||||
FROM amazoncorretto:17
|
FROM rust:1.67 as builder
|
||||||
|
WORKDIR /siren
|
||||||
|
RUN apt-get update && apt-get install -y cmake && apt-get auto-remove -y
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release --bin siren
|
||||||
|
|
||||||
ARG VERSION
|
FROM debian:bullseye-slim as packages
|
||||||
|
WORKDIR /packages
|
||||||
|
RUN apt-get update && apt-get install -y curl tar xz-utils
|
||||||
|
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \
|
||||||
|
chmod +x yt-dlp
|
||||||
|
RUN curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \
|
||||||
|
tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz
|
||||||
|
|
||||||
WORKDIR /app
|
FROM debian:bullseye-slim as runtime
|
||||||
|
WORKDIR /siren
|
||||||
ADD https://repo.local.bensherriff.com/artifactory/libs-release/com/bensherriff/siren/${VERSION}/siren-${VERSION}.jar /usr/local/lib/
|
RUN apt-get update && apt-get install -y libopus-dev ffmpeg youtube-dl
|
||||||
RUN mv /usr/local/lib/siren-${VERSION}.jar /usr/local/lib/siren.jar
|
COPY --from=builder /siren/target/release/siren siren
|
||||||
|
COPY --from=packages /packages /usr/bin
|
||||||
ENTRYPOINT ["java", "-jar", "/usr/local/lib/siren.jar"]
|
CMD ["./siren"]
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -3,10 +3,12 @@ include .version
|
|||||||
include .env
|
include .env
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if docker inspect siren > /dev/null 2>&1; then docker rmi siren; fi; docker-compose build
|
# if docker inspect siren > /dev/null 2>&1; then docker rmi siren; fi; docker-compose build
|
||||||
|
docker build -t siren .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker run --rm -it siren:latest bash
|
# docker run --rm -it siren:latest bash
|
||||||
|
docker run --env-file .env -it --rm --name siren siren:latest
|
||||||
|
|
||||||
up:
|
up:
|
||||||
if [[ ! $$(docker images -q siren 2> /dev/null) ]]; then docker-compose build; fi; \
|
if [[ ! $$(docker images -q siren 2> /dev/null) ]]; then docker-compose build; fi; \
|
||||||
|
|||||||
99
README.md
99
README.md
@@ -1,72 +1,45 @@
|
|||||||
# Siren
|
<div align="center">
|
||||||
A Java/Docker Discord music bot
|
<img src="siren.png" alt="drawing" width="200"/>
|
||||||
|
<h1 align="center">Siren</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Siren is a D&D Bot built for Discord, written in Rust. Features include:
|
||||||
|
- Play tracks from Youtube or locally hosted files
|
||||||
|
- Assistant DM tools to be defined later
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
Visit the [Discord Developer Portal](https://discord.com/developers/applications) and create a new application.
|
Visit the [Discord Developer Portal](https://discord.com/developers/applications) and create a new application. Click [here](https://discord.com/developers/docs/intro) for guides and more information.
|
||||||
Guides and more information are available [here](https://discord.com/developers/docs/intro).
|
|
||||||
|
|
||||||
### OAuth2 URL Generator
|
Required Scopes:
|
||||||
The bot requires the following permissions/scopes:
|
- bot
|
||||||
- bot
|
- application.commands
|
||||||
- applications.commands
|
|
||||||
|
|
||||||
|
Example Invite:
|
||||||
```
|
```
|
||||||
https://discord.com/api/oauth2/authorize?client_id=<Client ID>&permissions=1088840792896&scope=applications.commands%20bot
|
https://discord.com/api/oauth2/authorize?client_id=<CLIENT_ID>&permissions=40671259392832&scope=bot%20applications.commands
|
||||||
https://discord.com/api/oauth2/authorize?client_id=<Client ID>&permissions=5469678065984&scope=applications.commands%20bot - bot
|
```
|
||||||
- text permissions
|
- The CLIENT_ID can be found in the General Information tab on the Discord Developer Portal for your application, under `Application ID`
|
||||||
- send messages
|
|
||||||
- create public threads
|
1. Copy `.env.TEMPLATE` to `.env` and fill out the fields
|
||||||
- create private threads
|
2. Start the application with `docker compose up -d`
|
||||||
- send messages in threads
|
- Requires [Docker](https://www.docker.com/)
|
||||||
- send tts messages
|
|
||||||
- embed links
|
## Contributing
|
||||||
- attach files
|
[Rust](https://www.rust-lang.org/) must be installed to run locally. See [serenity-rs/serenity](https://github.com/serenity-rs/serenity) for more information about Rust Discord API Library.
|
||||||
- read message history
|
|
||||||
- mention everyone
|
Furthermore, the following packages must be installed for [serenity-rs/songbird](https://github.com/serenity-rs/songbird). View the repository for additional installation and setup information on other operating systems.
|
||||||
- use external emojis
|
```
|
||||||
- use external stickers
|
sudo apt install libopus-dev
|
||||||
- add reactions
|
sudo apt install ffmpeg
|
||||||
- use slash commands
|
sudo apt apt install youtube-dl
|
||||||
- voice permissions
|
|
||||||
- connect
|
|
||||||
- speak
|
|
||||||
- use voice activity
|
|
||||||
- priority speaker
|
|
||||||
- request to speak
|
|
||||||
- use embedded activities
|
|
||||||
- use soundboard
|
|
||||||
- applications.commands
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Potentially requires [yt-dlp](https://github.com/yt-dlp/yt-dlp#installation) and [yt-dlp FFmpeg Static Auto-Builds](https://github.com/yt-dlp/FFmpeg-Builds).
|
||||||
|
|
||||||
|
Begin the application with `cargo run`
|
||||||
|
|
||||||
|
The application can also be tested from within a Docker container:
|
||||||
```
|
```
|
||||||
make build
|
docker build -t siren .
|
||||||
make up
|
docker run --env-file .env -it --rm --name siren siren:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
|
||||||
Build container
|
|
||||||
|
|
||||||
`make build`
|
|
||||||
|
|
||||||
Run container locally
|
|
||||||
|
|
||||||
```
|
|
||||||
docker container run --name siren_test -d -t siren bash
|
|
||||||
docker exec -it siren_test bash
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
### Play
|
|
||||||
|
|
||||||
### Skip
|
|
||||||
|
|
||||||
### Stop
|
|
||||||
|
|
||||||
### Volume
|
|
||||||
|
|
||||||
### Pause
|
|
||||||
|
|
||||||
### Resume
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
View settings on the current guild.
|
|
||||||
@@ -10,8 +10,9 @@ services:
|
|||||||
args:
|
args:
|
||||||
- VERSION=${SIREN_VERSION}
|
- VERSION=${SIREN_VERSION}
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/app
|
- ./app:/usr/src/siren
|
||||||
environment:
|
environment:
|
||||||
|
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
||||||
DATABASE_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
|
DATABASE_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
|
||||||
DATABASE_USERNAME: ${POSTGRES_USER}
|
DATABASE_USERNAME: ${POSTGRES_USER}
|
||||||
DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
|
DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
|||||||
181
pom.xml
181
pom.xml
@@ -1,181 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<groupId>com.bensherriff</groupId>
|
|
||||||
<artifactId>siren</artifactId>
|
|
||||||
<version>${env.SIREN_VERSION}</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>false</enabled>
|
|
||||||
</snapshots>
|
|
||||||
<id>central</id>
|
|
||||||
<name>libs-release</name>
|
|
||||||
<url>https://repo.local.bensherriff.com/artifactory/libs-release</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<snapshots />
|
|
||||||
<id>snapshots</id>
|
|
||||||
<name>libs-snapshot</name>
|
|
||||||
<url>https://repo.local.bensherriff.com/artifactory/libs-snapshot</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>dv8tion</id>
|
|
||||||
<name>m2-dv8tion</name>
|
|
||||||
<url>https://m2.dv8tion.net/releases</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>jitpack</id>
|
|
||||||
<url>https://jitpack.io</url>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>maven_central</id>
|
|
||||||
<name>Maven Central</name>
|
|
||||||
<url>https://repo.maven.apache.org/maven2/</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
<pluginRepositories>
|
|
||||||
<pluginRepository>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>false</enabled>
|
|
||||||
</snapshots>
|
|
||||||
<id>central</id>
|
|
||||||
<name>libs-release</name>
|
|
||||||
<url>https://repo.local.bensherriff.com/artifactory/libs-release</url>
|
|
||||||
</pluginRepository>
|
|
||||||
<pluginRepository>
|
|
||||||
<snapshots />
|
|
||||||
<id>snapshots</id>
|
|
||||||
<name>libs-snapshot</name>
|
|
||||||
<url>https://repo.local.bensherriff.com/artifactory/libs-snapshot</url>
|
|
||||||
</pluginRepository>
|
|
||||||
</pluginRepositories>
|
|
||||||
<distributionManagement>
|
|
||||||
<repository>
|
|
||||||
<id>central</id>
|
|
||||||
<name>ffc58a58b429-releases</name>
|
|
||||||
<url>https://repo.local.bensherriff.com/artifactory/libs-release</url>
|
|
||||||
</repository>
|
|
||||||
</distributionManagement>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<jda.version>5.0.0-beta.8</jda.version>
|
|
||||||
<lavaplayer.version>1.4.0</lavaplayer.version>
|
|
||||||
<lavaplayer-natives-extra.version>1.3.13</lavaplayer-natives-extra.version>
|
|
||||||
<jackson.version>2.14.2</jackson.version>
|
|
||||||
<theokanning-openai-gpt3.version>0.12.0</theokanning-openai-gpt3.version>
|
|
||||||
<postgresql.version>42.6.0</postgresql.version>
|
|
||||||
<corenlp.version>4.2.0</corenlp.version>
|
|
||||||
<slf4j.version>2.0.7</slf4j.version>
|
|
||||||
<log4j.version>2.20.0</log4j.version>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.dv8tion</groupId>
|
|
||||||
<artifactId>JDA</artifactId>
|
|
||||||
<version>${jda.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.walkyst</groupId>
|
|
||||||
<artifactId>lavaplayer-fork</artifactId>
|
|
||||||
<version>${lavaplayer.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-core</artifactId>
|
|
||||||
<version>${jackson.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-databind</artifactId>
|
|
||||||
<version>${jackson.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.theokanning.openai-gpt3-java</groupId>
|
|
||||||
<artifactId>service</artifactId>
|
|
||||||
<version>${theokanning-openai-gpt3.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.postgresql</groupId>
|
|
||||||
<artifactId>postgresql</artifactId>
|
|
||||||
<version>${postgresql.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>edu.stanford.nlp</groupId>
|
|
||||||
<artifactId>stanford-corenlp</artifactId>
|
|
||||||
<version>${corenlp.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>edu.stanford.nlp</groupId>
|
|
||||||
<artifactId>stanford-corenlp</artifactId>
|
|
||||||
<version>${corenlp.version}</version>
|
|
||||||
<classifier>models</classifier>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Logging -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
|
||||||
<artifactId>log4j-api</artifactId>
|
|
||||||
<version>${log4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
|
||||||
<artifactId>log4j-core</artifactId>
|
|
||||||
<version>${log4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
|
||||||
<artifactId>log4j-slf4j-impl</artifactId>
|
|
||||||
<version>${log4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>1.5</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<!-- <shadedArtifactAttached>true</shadedArtifactAttached>-->
|
|
||||||
<!-- <shadedClassifierName>All</shadedClassifierName>-->
|
|
||||||
<artifactSet>
|
|
||||||
<includes>
|
|
||||||
<include>*:*</include>
|
|
||||||
</includes>
|
|
||||||
</artifactSet>
|
|
||||||
<transformers>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
|
||||||
<resource>reference.conf</resource>
|
|
||||||
</transformer>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<manifestEntries>
|
|
||||||
<Main-Class>com.bensherriff.siren.Main</Main-Class>
|
|
||||||
<Specification-Title>${project.artifactId}</Specification-Title>
|
|
||||||
<Specification-Version>${project.version}</Specification-Version>
|
|
||||||
<Implementation-Title>${project.artifactId}</Implementation-Title>
|
|
||||||
<Implementation-Version>${project.version}</Implementation-Version>
|
|
||||||
<Implementation-Vendor-Id>${project.groupId}</Implementation-Vendor-Id>
|
|
||||||
</manifestEntries>
|
|
||||||
</transformer>
|
|
||||||
</transformers>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
174
src/commands/audio/mod.rs
Normal file
174
src/commands/audio/mod.rs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
use serenity::model::application::interaction::{InteractionResponseType, application_command::ApplicationCommandInteraction};
|
||||||
|
use serenity::model::prelude::{GuildId, ChannelId};
|
||||||
|
use serenity::model::user::User;
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use songbird::{Call, Songbird};
|
||||||
|
use songbird::input::{Restartable, Input, Metadata, error::Error as SongbirdError};
|
||||||
|
|
||||||
|
pub mod pause;
|
||||||
|
pub mod play;
|
||||||
|
pub mod resume;
|
||||||
|
pub mod skip;
|
||||||
|
pub mod stop;
|
||||||
|
pub mod volume;
|
||||||
|
|
||||||
|
/// Joins a Discord voice channel.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - ctx - The context of the command.
|
||||||
|
/// - guild_id_option - The guild ID of the guild to join.
|
||||||
|
/// - user - The user that is requesting to join the voice channel.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result<(), String> - Ok if the bot successfully joined the voice channel, Err if there was an error.
|
||||||
|
pub async fn join(ctx: &Context, guild_id_option: &Option<GuildId>, user: &User) -> Result<(), String> {
|
||||||
|
let guild_id = match guild_id_option {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
return Err(format!("{}", "No guild ID set"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel_id = match find_voice_channel(&ctx, &guild_id, &user) {
|
||||||
|
Ok(channel) => channel,
|
||||||
|
Err(err) => return Err(format!("{}", err))
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("<{}> Joining channel {}", guild_id.0, channel_id);
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
let (_handle_lock, success) = manager.join(guild_id.to_owned(), channel_id.to_owned()).await;
|
||||||
|
match success {
|
||||||
|
Ok(s) => Ok(s),
|
||||||
|
Err(err) => Err(format!("{}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Leaves a Discord voice channel.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - ctx - The context of the command.
|
||||||
|
/// - guild_id_option - The guild ID of the guild to leave.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result<(), String> - Ok if the bot successfully left the voice channel, Err if there was an error.
|
||||||
|
pub async fn leave(ctx: &Context, guild_id_option: &Option<GuildId>) -> Result<(), String> {
|
||||||
|
let guild_id = match guild_id_option {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
return Err(format!("{}", "No guild ID set"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
if manager.get(*guild_id).is_some() {
|
||||||
|
debug!("<{}> Disconnecting from channel", guild_id.0);
|
||||||
|
if let Err(e) = manager.remove(*guild_id).await {
|
||||||
|
return Err(format!("{}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the voice channel that the user is in.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - ctx - The context of the command.
|
||||||
|
/// - guild_id - The guild ID of the guild to search.
|
||||||
|
/// - user - The user to search for.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result<ChannelId, String> - Ok if the user is in a voice channel, Err if the user is not in a voice channel.
|
||||||
|
fn find_voice_channel(ctx: &Context, guild_id: &GuildId, user: &User) -> Result<ChannelId, String> {
|
||||||
|
let guild = match guild_id.to_guild_cached(ctx.cache.to_owned()) {
|
||||||
|
Some(g) => g,
|
||||||
|
None => return Err(format!("Guild not found"))
|
||||||
|
};
|
||||||
|
|
||||||
|
match guild.voice_states.get(&user.id).and_then(|voice_state| voice_state.channel_id) {
|
||||||
|
Some(channel) => Ok(channel),
|
||||||
|
None => return Err(format!("User is not in a voice channel"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a response to an interaction.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - ctx - The context of the command.
|
||||||
|
/// - command - The command that was sent.
|
||||||
|
/// - content - The content of the response.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result<(), SerenityError> - Ok if the response was created successfully, Err if there was an error.
|
||||||
|
pub async fn create_response(ctx: &Context, command: &ApplicationCommandInteraction, content: String) -> Result<(), SerenityError> {
|
||||||
|
command.create_interaction_response(&ctx.http, |response: &mut serenity::builder::CreateInteractionResponse<'_>| {
|
||||||
|
response
|
||||||
|
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
|
.interaction_response_data(|message: &mut serenity::builder::CreateInteractionResponseData<'_>| message.content(content))
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edits a response to an interaction.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - ctx - The context of the command.
|
||||||
|
/// - command - The command that was sent.
|
||||||
|
/// - content - The content of the response.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result<Message, SerenityError> - Ok if the response was edited successfully, Err if there was an error.
|
||||||
|
pub async fn edit_response(ctx: &Context, command: &ApplicationCommandInteraction, content: String) -> Result<serenity::model::channel::Message, SerenityError> {
|
||||||
|
command.edit_original_interaction_response(&ctx.http, |response: &mut serenity::builder::EditInteractionResponse| {
|
||||||
|
response.content(content)
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a song to the queue.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - call - The call to add the song to.
|
||||||
|
/// - url - The URL of the song to add.
|
||||||
|
/// - lazy - Whether or not to lazy load the song.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Result<Metadata, SongbirdError> - Ok if the song was added successfully, Err if there was an error.
|
||||||
|
pub async fn add_song(call: Arc<Mutex<Call>>, url: &str, lazy: bool) -> Result<Metadata, SongbirdError> {
|
||||||
|
let source = if is_valid_url(url) {
|
||||||
|
Restartable::ytdl(url.to_owned(), lazy).await?
|
||||||
|
} else {
|
||||||
|
Restartable::ytdl_search(url, lazy).await?
|
||||||
|
};
|
||||||
|
let mut handler = call.lock().await;
|
||||||
|
let track: Input = source.into();
|
||||||
|
let metadata = *track.metadata.clone();
|
||||||
|
handler.enqueue_source(track);
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a string is a valid URL.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - url - The string to check.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// bool - True if the string is a valid URL, false if it is not.
|
||||||
|
fn is_valid_url(url: &str) -> bool {
|
||||||
|
match url.parse::<reqwest::Url>() {
|
||||||
|
Ok(_) => return true,
|
||||||
|
Err(_) => return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the Songbird voice client.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - ctx - The context of the command.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Arc<Songbird> - The Songbird voice client.
|
||||||
|
pub async fn get_songbird(ctx: &Context) -> Arc<Songbird> {
|
||||||
|
songbird::get(ctx).await.expect("Songbird Voice client placed in at initialization")
|
||||||
|
}
|
||||||
43
src/commands/audio/pause.rs
Normal file
43
src/commands/audio/pause.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||||
|
|
||||||
|
use super::{get_songbird, create_response, edit_response};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) {
|
||||||
|
// Create the initial response
|
||||||
|
if let Err(why) = create_response(&ctx, &command, "Processing command...".to_string()).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild_id = match command.guild_id {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, "Unable to join voice channel".to_string()).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
if let Err(err) = handler.queue().pause() {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Failed to pause: {}", err)).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Paused the track");
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Pausing the track")).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||||
|
command.name("pause").description("Pause the current track")
|
||||||
|
}
|
||||||
129
src/commands/audio/play.rs
Normal file
129
src/commands/audio/play.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use log::{debug, warn, error};
|
||||||
|
|
||||||
|
use serenity::{prelude::*, async_trait};
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||||
|
use songbird::EventHandler;
|
||||||
|
|
||||||
|
use crate::commands::audio::{join, leave, add_song, get_songbird};
|
||||||
|
|
||||||
|
use super::{create_response, edit_response};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) {
|
||||||
|
// Get the track url
|
||||||
|
let track_url = match command.data.options.get(0) {
|
||||||
|
Some(t) => match &t.value {
|
||||||
|
Some(v) => match v.as_str() {
|
||||||
|
Some(s) => s.to_owned(),
|
||||||
|
None => {
|
||||||
|
warn!("Missing track option");
|
||||||
|
if let Err(why) = create_response(&ctx, &command, format!("Track option is missing")).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Missing track option");
|
||||||
|
if let Err(why) = create_response(&ctx, &command, format!("Track option is missing")).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Missing track option");
|
||||||
|
if let Err(why) = create_response(&ctx, &command, format!("Track option is missing")).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the initial response
|
||||||
|
if let Err(why) = create_response(&ctx, &command, format!("Processing command...")).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match join(&ctx, &command.guild_id, &command.user).await {
|
||||||
|
Ok(_) => {
|
||||||
|
let guild_id = match command.guild_id {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, "Unable to join voice channel".to_string()).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("Play command executed with track: {:?}", track_url);
|
||||||
|
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let is_queue_empty = {
|
||||||
|
let call_handler = handler_lock.lock().await;
|
||||||
|
call_handler.queue().is_empty()
|
||||||
|
};
|
||||||
|
match add_song(handler_lock.clone(), &track_url, is_queue_empty).await {
|
||||||
|
Ok(added_song) => {
|
||||||
|
let track_title = added_song.title.unwrap();
|
||||||
|
debug!("Added song: {}", track_title);
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Added song to queue: {}", track_title)).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
let mut handler = handler_lock.lock().await;
|
||||||
|
handler.remove_all_global_events();
|
||||||
|
handler.add_global_event(songbird::Event::Track(songbird::TrackEvent::End), TrackEndNotifier { guild_id, call: manager })
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
warn!("Failed to add song: {}", why);
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Failed to add song: {}", why)).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
if let Err(why) = leave(&ctx, &command.guild_id).await {
|
||||||
|
error!("Failed to leave voice channel: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!("{}", err);
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("{}", err)).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||||
|
command.name("play").description("Plays the given track").create_option(|option| { option
|
||||||
|
.name("track")
|
||||||
|
.description("The track to be played")
|
||||||
|
.kind(serenity::model::prelude::command::CommandOptionType::String)
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackEndNotifier {
|
||||||
|
pub call: std::sync::Arc<songbird::Songbird>,
|
||||||
|
pub guild_id: serenity::model::id::GuildId
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for TrackEndNotifier {
|
||||||
|
async fn act(&self, ctx: &songbird::events::EventContext<'_>) -> Option<songbird::events::Event> {
|
||||||
|
if let songbird::EventContext::Track(_track_list) = ctx {
|
||||||
|
if let Some(call) = self.call.get(self.guild_id) {
|
||||||
|
let mut handler = call.lock().await;
|
||||||
|
if handler.queue().is_empty() {
|
||||||
|
debug!("Queue is empty, leaving voice channel");
|
||||||
|
handler.leave().await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/commands/audio/resume.rs
Normal file
43
src/commands/audio/resume.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||||
|
|
||||||
|
use super::{get_songbird, create_response, edit_response};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) {
|
||||||
|
// Create the initial response
|
||||||
|
if let Err(why) = create_response(&ctx, &command, "Processing command...".to_string()).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild_id = match command.guild_id {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, "Unable to join voice channel".to_string()).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
if let Err(err) = handler.queue().resume() {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Failed to resume: {}", err)).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Resumed the track");
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Resuming the track")).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||||
|
command.name("resume").description("Resume the current track")
|
||||||
|
}
|
||||||
43
src/commands/audio/skip.rs
Normal file
43
src/commands/audio/skip.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||||
|
|
||||||
|
use super::{get_songbird, create_response, edit_response};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) {
|
||||||
|
// Create the initial response
|
||||||
|
if let Err(why) = create_response(&ctx, &command, "Processing command...".to_string()).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild_id = match command.guild_id {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, "Unable to join voice channel".to_string()).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
if let Err(err) = handler.queue().skip() {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Failed to skip: {}", err)).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Skipped the track");
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Skipping the track")).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||||
|
command.name("skip").description("Skip the current track")
|
||||||
|
}
|
||||||
38
src/commands/audio/stop.rs
Normal file
38
src/commands/audio/stop.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||||
|
|
||||||
|
use super::{get_songbird, create_response, edit_response};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) {
|
||||||
|
// Create the initial response
|
||||||
|
if let Err(why) = create_response(&ctx, &command, "Processing command...".to_string()).await {
|
||||||
|
error!("Failed to create response message: {}", why);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild_id = match command.guild_id {
|
||||||
|
Some(g) => g,
|
||||||
|
None => {
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, "Unable to join voice channel".to_string()).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let manager = get_songbird(ctx).await;
|
||||||
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
handler.queue().stop();
|
||||||
|
debug!("Stopped the track");
|
||||||
|
if let Err(why) = edit_response(&ctx, &command, format!("Stopping the tracks")).await {
|
||||||
|
error!("Failed to edit response message: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||||
|
command.name("stop").description("Stop the current track and clear the queue")
|
||||||
|
}
|
||||||
0
src/commands/audio/volume.rs
Normal file
0
src/commands/audio/volume.rs
Normal file
0
src/commands/help.rs
Normal file
0
src/commands/help.rs
Normal file
2
src/commands/mod.rs
Normal file
2
src/commands/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod ping;
|
||||||
|
pub mod audio;
|
||||||
11
src/commands/ping.rs
Normal file
11
src/commands/ping.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use log::debug;
|
||||||
|
use serenity::{model::prelude::interaction::application_command::CommandDataOption, builder::CreateApplicationCommand};
|
||||||
|
|
||||||
|
pub fn run(_options: &[CommandDataOption]) -> String {
|
||||||
|
debug!("Ping command executed");
|
||||||
|
"pong".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||||
|
command.name("ping").description("Replies with pong")
|
||||||
|
}
|
||||||
104
src/main.rs
Normal file
104
src/main.rs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use commands::audio::create_response;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use log::{error, warn, info};
|
||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::framework::StandardFramework;
|
||||||
|
use serenity::model::application::interaction::Interaction;
|
||||||
|
use serenity::model::gateway::Ready;
|
||||||
|
use serenity::http::Http;
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use songbird::SerenityInit;
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
struct Handler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
|
if let Interaction::ApplicationCommand(command) = interaction {
|
||||||
|
match command.data.name.as_str() {
|
||||||
|
"play" => commands::audio::play::run(&ctx, &command).await,
|
||||||
|
"stop" => commands::audio::stop::run(&ctx, &command).await,
|
||||||
|
"pause" => commands::audio::pause::run(&ctx, &command).await,
|
||||||
|
"resume" => commands::audio::resume::run(&ctx, &command).await,
|
||||||
|
"skip" => commands::audio::skip::run(&ctx, &command).await,
|
||||||
|
_ => {
|
||||||
|
let content: String = match command.data.name.as_str() {
|
||||||
|
"ping" => commands::ping::run(&command.data.options),
|
||||||
|
_ => "Unknown command".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(why) = create_response(&ctx, &command, content).await {
|
||||||
|
warn!("Cannot respond to slash command: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ready(&self, ctx: Context, ready: Ready) {
|
||||||
|
if ready.guilds.is_empty() {
|
||||||
|
warn!("No ready guilds found");
|
||||||
|
}
|
||||||
|
for guild in ready.guilds {
|
||||||
|
let commands = guild.id.set_application_commands(&ctx.http, |commands| {
|
||||||
|
commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) })
|
||||||
|
.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) })
|
||||||
|
.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::stop::register(command) })
|
||||||
|
.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::pause::register(command) })
|
||||||
|
.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::resume::register(command) })
|
||||||
|
.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::skip::register(command) })
|
||||||
|
}).await;
|
||||||
|
match commands {
|
||||||
|
Ok(c) => info!("Registered {} commands for guild {}", c.len(), guild.id.0),
|
||||||
|
Err(why) => error!("Could not register commands for guild {}: {:?}", guild.id.0, why)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
dotenv().ok();
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
||||||
|
let intents: GatewayIntents = GatewayIntents::all();
|
||||||
|
|
||||||
|
let http: Http = Http::new(&token);
|
||||||
|
let (owners, _bot_id) = match http.get_current_application_info().await {
|
||||||
|
Ok(info) => {
|
||||||
|
let mut owners: HashSet<serenity::model::id::UserId> = HashSet::new();
|
||||||
|
if let Some(team) = info.team {
|
||||||
|
owners.insert(team.owner_user_id);
|
||||||
|
} else {
|
||||||
|
owners.insert(info.owner.id);
|
||||||
|
}
|
||||||
|
match http.get_current_user().await {
|
||||||
|
Ok(bot) => (owners, bot.id),
|
||||||
|
Err(why) => panic!("Could not access the bot id: {:?}", why)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(why) => panic!("Could not access application info: {:?}", why)
|
||||||
|
};
|
||||||
|
|
||||||
|
let framework = StandardFramework::new()
|
||||||
|
.configure(|c| c
|
||||||
|
.owners(owners)
|
||||||
|
.prefix("!")
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut client = Client::builder(token, intents)
|
||||||
|
.event_handler(Handler)
|
||||||
|
.framework(framework)
|
||||||
|
.register_songbird()
|
||||||
|
.await
|
||||||
|
.expect("Error creating client");
|
||||||
|
|
||||||
|
if let Err(why) = client.start_autosharded().await {
|
||||||
|
error!("An error occurred while running the client: {:?}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package com.bensherriff.siren;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.audio.PlayerManager;
|
|
||||||
import com.bensherriff.siren.commands.*;
|
|
||||||
import com.bensherriff.siren.database.DatabaseManager;
|
|
||||||
import com.bensherriff.siren.exceptions.EmptyVoiceChannelException;
|
|
||||||
import com.bensherriff.siren.ai.OpenAIManager;
|
|
||||||
import com.bensherriff.siren.settings.*;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
|
||||||
import net.dv8tion.jda.api.events.session.ReadyEvent;
|
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
|
||||||
import net.dv8tion.jda.api.managers.AudioManager;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class Listener extends ListenerAdapter {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Listener.class);
|
|
||||||
private final ScheduledExecutorService executor;
|
|
||||||
|
|
||||||
private final Settings settings;
|
|
||||||
private final Map<String, Command> commands = new HashMap<>();
|
|
||||||
private final String owner;
|
|
||||||
private PlayerManager playerManager;
|
|
||||||
private OpenAIManager openAIManager;
|
|
||||||
private JDA jda;
|
|
||||||
|
|
||||||
public Listener(Settings settings) {
|
|
||||||
this.settings = settings;
|
|
||||||
this.executor = Executors.newScheduledThreadPool(this.settings.getThreadPool());
|
|
||||||
this.owner = "@bsherriff";
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScheduledExecutorService getExecutor() {
|
|
||||||
return executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayerManager getPlayerManager() {
|
|
||||||
return playerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Settings getSettings() {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOwner() {
|
|
||||||
return owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JDA getJDA() {
|
|
||||||
return jda;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setJDA(JDA jda) {
|
|
||||||
this.jda = jda;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OpenAIManager getOpenAIManager() {
|
|
||||||
return openAIManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initialize() {
|
|
||||||
this.playerManager = new PlayerManager(this);
|
|
||||||
this.playerManager.initialize();
|
|
||||||
this.openAIManager = new OpenAIManager(this);
|
|
||||||
|
|
||||||
DatabaseManager.createTables();
|
|
||||||
populateAudioTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeAudioConnection(long guildID) {
|
|
||||||
Guild guild = jda.getGuildById(guildID);
|
|
||||||
if (guild != null) {
|
|
||||||
guild.getAudioManager().closeAudioConnection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void connectToVoiceChannel(String userID, AudioManager audioManager) throws EmptyVoiceChannelException {
|
|
||||||
if (!audioManager.isConnected()) {
|
|
||||||
Member member = audioManager.getGuild().getMemberById(userID);
|
|
||||||
if (member != null) {
|
|
||||||
if (member.getVoiceState() != null && member.getVoiceState().inAudioChannel()) {
|
|
||||||
VoiceChannel voiceChannel = Objects.requireNonNull(member.getVoiceState().getChannel()).asVoiceChannel();
|
|
||||||
LOGGER.debug("Connecting to channel {} in guild {}", voiceChannel.getId(), voiceChannel.getGuild().getId());
|
|
||||||
audioManager.openAudioConnection(voiceChannel);
|
|
||||||
} else {
|
|
||||||
throw new EmptyVoiceChannelException("Member {} is not connected to a voice channel");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized AudioHandler getGuildAudioPlayer(Guild guild) throws IOException {
|
|
||||||
long guildId = Long.parseLong(guild.getId());
|
|
||||||
AudioHandler audioHandler;
|
|
||||||
|
|
||||||
if (guild.getAudioManager().getSendingHandler() == null || !settings.getGuildSettings().containsKey(guildId)) {
|
|
||||||
LOGGER.info("Creating Audio Handler for guild {}", guildId);
|
|
||||||
if (!settings.getGuildSettings().containsKey(guildId)) {
|
|
||||||
settings.getGuildSettings().put(guildId, new GuildSettings());
|
|
||||||
SettingsManager.write(settings);
|
|
||||||
}
|
|
||||||
audioHandler = new AudioHandler(playerManager, guildId);
|
|
||||||
guild.getAudioManager().setSendingHandler(audioHandler);
|
|
||||||
} else {
|
|
||||||
audioHandler = (AudioHandler) guild.getAudioManager().getSendingHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void populateAudioTable() {
|
|
||||||
DatabaseManager.clearTable("audio");
|
|
||||||
File directory = new File(SettingsManager.AUDIO_DIRECTORY);
|
|
||||||
try {
|
|
||||||
if (!directory.exists() && !directory.mkdirs()) {
|
|
||||||
LOGGER.error("Failed to create directory at {}", directory.getPath());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LocalTracks tracks = SettingsManager.load(SettingsManager.TRACKS_PATH, LocalTracks.class);
|
|
||||||
int rows = 0;
|
|
||||||
File[] files = directory.listFiles();
|
|
||||||
if (files != null) {
|
|
||||||
for (LocalTrack track : tracks.getTracks()) {
|
|
||||||
for (File file : files) {
|
|
||||||
if (file.exists() && file.getName().equals(track.getFileName())) {
|
|
||||||
rows += DatabaseManager.storeAudio(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGGER.debug("Updated with {} local tracks", rows);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
LOGGER.error("Failed to load local tracks; {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReady(@NotNull ReadyEvent event) {
|
|
||||||
super.onReady(event);
|
|
||||||
commands.put("play", new PlayCommand(this));
|
|
||||||
commands.put("stop", new StopCommand(this));
|
|
||||||
commands.put("skip", new SkipCommand(this));
|
|
||||||
commands.put("volume", new VolumeCommand(this));
|
|
||||||
commands.put("pause", new PauseCommand(this));
|
|
||||||
commands.put("resume", new ResumeCommand(this));
|
|
||||||
// commands.put("image", new ImageCommand(this));
|
|
||||||
jda.getGuilds().forEach(guild -> executor.execute(() -> {
|
|
||||||
LOGGER.debug("Updating commands for \"{}\" <{}>", guild.getName(), guild.getId());
|
|
||||||
guild.updateCommands().addCommands(
|
|
||||||
commands.values().stream().map(Command::getSlashCommandData).collect(Collectors.toList())
|
|
||||||
).queue();
|
|
||||||
}));
|
|
||||||
super.onReady(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
|
|
||||||
String command = event.getName();
|
|
||||||
event.deferReply().queue();
|
|
||||||
|
|
||||||
if (commands.containsKey(command)) {
|
|
||||||
executor.execute(() -> {
|
|
||||||
try {
|
|
||||||
commands.get(command).execute(event);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.error("Failed to execute command; {}", ex.getMessage());
|
|
||||||
event.getHook().sendMessage("An error occurred while processing your command. Please contact " + owner + ".").queue();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
event.getHook().sendMessage("Unexpected command received.").queue();
|
|
||||||
}
|
|
||||||
super.onSlashCommandInteraction(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
|
|
||||||
openAIManager.onMessageReceived(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.bensherriff.siren;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.settings.Settings;
|
|
||||||
import com.bensherriff.siren.settings.SettingsManager;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
|
||||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class Main {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Main.class);
|
|
||||||
private final static GatewayIntent[] INTENTS = {
|
|
||||||
GatewayIntent.DIRECT_MESSAGES, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MESSAGE_REACTIONS,
|
|
||||||
GatewayIntent.GUILD_VOICE_STATES, GatewayIntent.MESSAGE_CONTENT
|
|
||||||
};
|
|
||||||
private final static CacheFlag[] ENABLED_FLAGS = {
|
|
||||||
CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE
|
|
||||||
};
|
|
||||||
private final static CacheFlag[] DISABLED_FLAGS = {
|
|
||||||
CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS, CacheFlag.ONLINE_STATUS, CacheFlag.EMOJI, CacheFlag.STICKER, CacheFlag.SCHEDULED_EVENTS
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
try {
|
|
||||||
start();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.error("Caught unhandled exception; {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void start() throws IOException {
|
|
||||||
Settings settings = SettingsManager.load();
|
|
||||||
Listener listener = new Listener(settings);
|
|
||||||
|
|
||||||
if (settings.getToken() == null || settings.getToken().isEmpty()) {
|
|
||||||
throw new IOException("Token field may not be empty, please set the value in " + SettingsManager.PATH);
|
|
||||||
} else if (settings.getOwner() == null || settings.getOwner().isEmpty()) {
|
|
||||||
throw new IOException("Owner field may not be empty, please set the value in " + SettingsManager.PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
JDA jda = JDABuilder.create(settings.getToken(), Arrays.asList(INTENTS))
|
|
||||||
.enableCache(Arrays.asList(ENABLED_FLAGS))
|
|
||||||
.disableCache(Arrays.asList(DISABLED_FLAGS))
|
|
||||||
.setActivity(Activity.playing("nothing"))
|
|
||||||
.addEventListeners(listener)
|
|
||||||
.setBulkDeleteSplittingEnabled(true)
|
|
||||||
.build();
|
|
||||||
listener.setJDA(jda);
|
|
||||||
listener.initialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.bensherriff.siren.ai;
|
|
||||||
|
|
||||||
public enum ImageSize {
|
|
||||||
SMALL("256x256"),
|
|
||||||
MEDIUM("512x512"),
|
|
||||||
LARGE("1024x1024");
|
|
||||||
|
|
||||||
private final String size;
|
|
||||||
|
|
||||||
ImageSize(String size) {
|
|
||||||
this.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.bensherriff.siren.ai;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CompletionRequest Models:
|
|
||||||
* text-davinci-003, text-davinci-002, text-curie-001, text-babbage-001, text-ada-001
|
|
||||||
* ChatCompletionRequest Models:
|
|
||||||
* gpt-4, gpt-4-0314, gpt-4-32k, gpt-4-32k-0314, gpt-3.5-turbo, gpt-3.5-turbo-0301
|
|
||||||
*/
|
|
||||||
public enum Model {
|
|
||||||
DAVINCI_3("text-davinci-003"),
|
|
||||||
DAVINCI_2("text-davinci-002"),
|
|
||||||
CURIE_1("text-curie-001"),
|
|
||||||
BABBAGE_1("text-babbage-001"),
|
|
||||||
ADA_1("text-ada-001"),
|
|
||||||
GPT_4("gpt-4"),
|
|
||||||
GPT_4_32K("gpt-4-32k"),
|
|
||||||
GPT_35_TURBO("gpt-3.5-turbo"),
|
|
||||||
ADA_EMBEDDING_2("text-embedding-ada-002")
|
|
||||||
;
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
Model(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
package com.bensherriff.siren.ai;
|
|
||||||
|
|
||||||
import edu.stanford.nlp.ling.CoreAnnotations;
|
|
||||||
import edu.stanford.nlp.ling.CoreLabel;
|
|
||||||
import edu.stanford.nlp.pipeline.Annotation;
|
|
||||||
import edu.stanford.nlp.pipeline.StanfordCoreNLP;
|
|
||||||
import edu.stanford.nlp.sentiment.SentimentCoreAnnotations;
|
|
||||||
import edu.stanford.nlp.util.CoreMap;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class NLP {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(NLP.class);
|
|
||||||
private final StanfordCoreNLP pipeline;
|
|
||||||
private final Map<String, List<String>> keywords;
|
|
||||||
|
|
||||||
public NLP() {
|
|
||||||
Properties props = new Properties();
|
|
||||||
props.setProperty("annotators", "tokenize, ssplit, pos, lemma, ner, parse, dcoref, sentiment");
|
|
||||||
pipeline = new StanfordCoreNLP(props);
|
|
||||||
keywords = new HashMap<>();
|
|
||||||
keywords.put("dnd", Arrays.asList("dnd", "dungeons", "dragons", "sorcerer", "warlock", "cleric", "fighter", "rogue", "bard", "wizard", "paladin", "ranger", "druid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CoreMap> getSentences(String text) {
|
|
||||||
Annotation annotation = new Annotation(text);
|
|
||||||
pipeline.annotate(annotation);
|
|
||||||
return annotation.get(CoreAnnotations.SentencesAnnotation.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getTags(String text) {
|
|
||||||
Set<String> tags = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
List<CoreMap> sentences = getSentences(text);
|
|
||||||
for (CoreMap sentence : sentences) {
|
|
||||||
List<CoreLabel> tokens = sentence.get(CoreAnnotations.TokensAnnotation.class);
|
|
||||||
List<CoreLabel> namedEntities = new ArrayList<>();
|
|
||||||
|
|
||||||
for (CoreLabel token : tokens) {
|
|
||||||
String ne = token.get(CoreAnnotations.NamedEntityTagAnnotation.class);
|
|
||||||
if (!ne.equals("0")) {
|
|
||||||
namedEntities.add(token);
|
|
||||||
}
|
|
||||||
if (token.equals(tokens.get(tokens.size() - 1)) && token.word().equals("?") ||
|
|
||||||
(List.of("what", "when", "who", "where", "why", "how").contains(token.word()))) {
|
|
||||||
tags.add("question");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (CoreLabel namedEntity : namedEntities) {
|
|
||||||
String ne = namedEntity.get(CoreAnnotations.NamedEntityTagAnnotation.class);
|
|
||||||
String word = namedEntity.word();
|
|
||||||
if (ne.equals("PERSON") || ne.equals("ORGANIZATION")) {
|
|
||||||
tags.add(word);
|
|
||||||
} else if (ne.equals("LOCATION")) {
|
|
||||||
String[] posTags = word.split("_");
|
|
||||||
for (String posTag : posTags) {
|
|
||||||
if (posTag.startsWith("N")) {
|
|
||||||
tags.add(word);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String pos = namedEntity.get(CoreAnnotations.PartOfSpeechAnnotation.class);
|
|
||||||
if (pos.startsWith("NN")) {
|
|
||||||
tags.add(word);
|
|
||||||
for (String keyword : keywords.keySet()) {
|
|
||||||
if (keywords.get(keyword).contains(word.toLowerCase())) {
|
|
||||||
tags.add(keyword);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> lemmatize(String documentText) {
|
|
||||||
List<String> lemmas = new ArrayList<>();
|
|
||||||
Annotation document = new Annotation(documentText);
|
|
||||||
pipeline.annotate(document);
|
|
||||||
List<CoreMap> sentences = document.get(CoreAnnotations.SentencesAnnotation.class);
|
|
||||||
for (CoreMap sentence : sentences) {
|
|
||||||
for (CoreLabel token : sentence.get(CoreAnnotations.TokensAnnotation.class)) {
|
|
||||||
lemmas.add(token.get(CoreAnnotations.LemmaAnnotation.class));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lemmas;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the sentiment (tone) of each sentence in the text. For example: positive, negative, or neutral
|
|
||||||
* @param text the input text
|
|
||||||
* @return the list of sentiments
|
|
||||||
*/
|
|
||||||
public List<String> sentimentAnalysis(String text) {
|
|
||||||
Annotation document = new Annotation(text);
|
|
||||||
pipeline.annotate(document);
|
|
||||||
List<CoreMap> sentences = document.get(CoreAnnotations.SentencesAnnotation.class);
|
|
||||||
List<String> sentiments = new ArrayList<>();
|
|
||||||
for (CoreMap sentence : sentences) {
|
|
||||||
sentiments.add(sentence.get(SentimentCoreAnnotations.SentimentClass.class));
|
|
||||||
}
|
|
||||||
return sentiments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double cosineSimilarity(double[] vector1, double[] vector2) {
|
|
||||||
double dotProduct = 0.0;
|
|
||||||
double norm1 = 0.0;
|
|
||||||
double norm2 = 0.0;
|
|
||||||
for (int i = 0; i < vector1.length; i++) {
|
|
||||||
dotProduct += vector1[i] * vector2[i];
|
|
||||||
norm1 += Math.pow(vector1[i], 2);
|
|
||||||
norm2 += Math.pow(vector2[i], 2);
|
|
||||||
}
|
|
||||||
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
package com.bensherriff.siren.ai;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import com.bensherriff.siren.database.DatabaseManager;
|
|
||||||
import com.bensherriff.siren.database.MessageData;
|
|
||||||
import com.bensherriff.siren.database.QueryBuilder;
|
|
||||||
import com.bensherriff.siren.settings.*;
|
|
||||||
import com.theokanning.openai.completion.CompletionRequest;
|
|
||||||
import com.theokanning.openai.completion.CompletionResult;
|
|
||||||
import com.theokanning.openai.completion.chat.ChatCompletionRequest;
|
|
||||||
import com.theokanning.openai.completion.chat.ChatCompletionResult;
|
|
||||||
import com.theokanning.openai.completion.chat.ChatMessage;
|
|
||||||
import com.theokanning.openai.embedding.EmbeddingRequest;
|
|
||||||
import com.theokanning.openai.image.CreateImageRequest;
|
|
||||||
import com.theokanning.openai.image.ImageResult;
|
|
||||||
import com.theokanning.openai.service.OpenAiService;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
|
||||||
import net.dv8tion.jda.api.entities.MessageType;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
|
|
||||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class OpenAIManager {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(OpenAIManager.class);
|
|
||||||
private final OpenAiService openAiService;
|
|
||||||
private final Settings settings;
|
|
||||||
private final JDA jda;
|
|
||||||
private final ScheduledExecutorService executor;
|
|
||||||
private final String owner;
|
|
||||||
private final NLP NLP;
|
|
||||||
|
|
||||||
public OpenAIManager(Listener listener) {
|
|
||||||
this.settings = listener.getSettings();
|
|
||||||
this.jda = listener.getJDA();
|
|
||||||
this.executor = listener.getExecutor();
|
|
||||||
this.owner = listener.getOwner();
|
|
||||||
this.NLP = new NLP();
|
|
||||||
|
|
||||||
if (settings.getOpenAISettings().getToken().isEmpty()) {
|
|
||||||
LOGGER.warn("No OpenAI token; OpenAI functionality is disabled");
|
|
||||||
openAiService = null;
|
|
||||||
} else {
|
|
||||||
openAiService = new OpenAiService(settings.getOpenAISettings().getToken(),
|
|
||||||
Duration.ofMillis(listener.getSettings().getOpenAISettings().getTimeout()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle responses when talking to the bot.
|
|
||||||
* @param event The message event received
|
|
||||||
*/
|
|
||||||
public void onMessageReceived(MessageReceivedEvent event) {
|
|
||||||
if (shouldReply(event)) {
|
|
||||||
if (openAiService != null) {
|
|
||||||
executor.execute(() -> sendMessage(event));
|
|
||||||
} else {
|
|
||||||
event.getMessage().reply("OpenAI functionality is disabled. Please contact " + owner + ".").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessage(MessageReceivedEvent event) {
|
|
||||||
String message = parseMessage(event.getMessage().getContentRaw());
|
|
||||||
|
|
||||||
if (message.isEmpty() || message.isBlank()) {
|
|
||||||
event.getMessage().reply("Your message is empty. Please try again").queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
|
||||||
long guildId = event.getGuild().getIdLong();
|
|
||||||
long authorId = event.getAuthor().getIdLong();
|
|
||||||
UserSettings userSettings = new UserSettings();
|
|
||||||
settings.getGuildSettings().get(guildId).getUserSettings().putIfAbsent(authorId, userSettings);
|
|
||||||
SettingsManager.write(settings);
|
|
||||||
userSettings = settings.getGuildSettings().get(guildId).getUserSettings().get(authorId);
|
|
||||||
Model model = userSettings.getModel();
|
|
||||||
ChatMessage chatMessage = createUserMessage(message);
|
|
||||||
|
|
||||||
|
|
||||||
LOGGER.trace("Guild: <{}> User: <{}> Message <{}>: {}", guildId, authorId, event.getMessageId(), message);
|
|
||||||
String query = new QueryBuilder("messages")
|
|
||||||
.where("guild_id = ? AND channel_id = ? AND prompt = ?")
|
|
||||||
.orderBy("timestamp DESC")
|
|
||||||
.build();
|
|
||||||
List<MessageData> messages = DatabaseManager.parseResponse(DatabaseManager.query(query,
|
|
||||||
guildId, event.getChannel().getIdLong(), chatMessage.getContent()));
|
|
||||||
if (!messages.isEmpty()) {
|
|
||||||
stringBuilder.append(messages.get(0).getResponse());
|
|
||||||
} else {
|
|
||||||
// Send OpenAI Message and get response
|
|
||||||
switch (model) {
|
|
||||||
case DAVINCI_3, DAVINCI_2, CURIE_1, BABBAGE_1, ADA_1 -> {
|
|
||||||
CompletionRequest completionRequest = createCompletionRequest(message, event);
|
|
||||||
CompletionResult completionResult = openAiService.createCompletion(completionRequest);
|
|
||||||
completionResult.getChoices().forEach(choice -> stringBuilder.append(choice.getText().trim()));
|
|
||||||
}
|
|
||||||
case GPT_4, GPT_4_32K, GPT_35_TURBO -> {
|
|
||||||
ChatCompletionRequest chatCompletionRequest = createCompletionRequest(chatMessage, event);
|
|
||||||
ChatCompletionResult chatCompletionResult = openAiService.createChatCompletion(chatCompletionRequest);
|
|
||||||
chatCompletionResult.getChoices().forEach(choice -> stringBuilder.append(choice.getMessage().getContent().trim()));
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
event.getMessage().reply("Unexpected model in settings. Please contact " + owner + ".").queue();
|
|
||||||
LOGGER.warn("Unexpected model in settings for guild {}: {}. Expected one of {}", guildId,
|
|
||||||
model, Arrays.toString(Model.values()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleResponse(chatMessage, event, stringBuilder.toString());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.error("Caught exception while processing message; {}", ex.getMessage());
|
|
||||||
event.getMessage().reply("An error occurred while processing your message. Please contact " + owner + ".").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmbeddingRequest createEmbeddingRequest(List<ChatMessage> chatMessages, MessageReceivedEvent event) {
|
|
||||||
return EmbeddingRequest.builder()
|
|
||||||
.model(Model.ADA_EMBEDDING_2.getName())
|
|
||||||
.user(event.getAuthor().getId())
|
|
||||||
.input(chatMessages.stream().map(ChatMessage::getContent).collect(Collectors.toList()))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImageResult createImage(String prompt, int count, ImageSize imageSize) {
|
|
||||||
LOGGER.trace("Generating {} image(s) of size {} with prompt <{}>", count, imageSize.getSize(), prompt);
|
|
||||||
return openAiService.createImage(CreateImageRequest.builder()
|
|
||||||
.prompt(prompt)
|
|
||||||
.size(imageSize.getSize())
|
|
||||||
.n(count <= 0 ? 1 : Math.min(count, 3))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatCompletionRequest createCompletionRequest(ChatMessage chatMessage, MessageReceivedEvent event) throws SQLException {
|
|
||||||
UserSettings userSettings = settings.getGuildSettings().get(event.getGuild().getIdLong())
|
|
||||||
.getUserSettings().get(event.getAuthor().getIdLong());
|
|
||||||
List<ChatMessage> chatMessages = new ArrayList<>();
|
|
||||||
// List<MessageData> previousMessages = getPreviousMessages(event);
|
|
||||||
// for (MessageData previousMessage : previousMessages) {
|
|
||||||
// chatMessages.add(createSystemMessage("In a previous conversation, we discussed the topics " + previousMessage.getTags() +
|
|
||||||
// " and " + previousMessage.getResponseTags()));
|
|
||||||
// chatMessages.add(createSystemMessage("I sent you the prompt \"" + previousMessage.getPrompt() +
|
|
||||||
// "\" and you replied with \"" + previousMessage.getResponse() + "\""));
|
|
||||||
// }
|
|
||||||
chatMessages.add(chatMessage);
|
|
||||||
|
|
||||||
// Handle System Messages
|
|
||||||
chatMessages.add(createSystemMessage("You are a discord bot named Siren"));
|
|
||||||
chatMessages.add(createSystemMessage("My name is " + event.getAuthor().getName()));
|
|
||||||
|
|
||||||
return ChatCompletionRequest.builder()
|
|
||||||
.model(userSettings.getModel().getName())
|
|
||||||
.maxTokens(userSettings.getMaxTokens())
|
|
||||||
.user(event.getAuthor().getId())
|
|
||||||
.temperature(settings.getOpenAISettings().getTemperature())
|
|
||||||
// .topP(settings.getOpenAISettings().getTopP())
|
|
||||||
.frequencyPenalty(settings.getOpenAISettings().getFrequencyPenalty())
|
|
||||||
.presencePenalty(settings.getOpenAISettings().getPresencePenalty())
|
|
||||||
.messages(chatMessages)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<MessageData> getPreviousMessages(MessageReceivedEvent event) throws SQLException {
|
|
||||||
List<MessageData> previousMessages = new ArrayList<>();
|
|
||||||
if (event.isFromThread()) {
|
|
||||||
String query = new QueryBuilder("messages")
|
|
||||||
.where("guild_id = ? AND channel_id = ?")
|
|
||||||
.orderBy("timestamp DESC")
|
|
||||||
.limit(3)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Build MessageData objects from query results
|
|
||||||
List<Map<String, Object>> results = DatabaseManager.query(
|
|
||||||
query, event.getGuild().getIdLong(), event.getChannel().getIdLong());
|
|
||||||
previousMessages.addAll(DatabaseManager.parseResponse(results));
|
|
||||||
}
|
|
||||||
return previousMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CompletionRequest createCompletionRequest(String message, MessageReceivedEvent event) {
|
|
||||||
UserSettings userSettings = settings.getGuildSettings().get(event.getGuild().getIdLong())
|
|
||||||
.getUserSettings().get(event.getAuthor().getIdLong());
|
|
||||||
return CompletionRequest.builder()
|
|
||||||
.model(userSettings.getModel().getName())
|
|
||||||
.maxTokens(userSettings.getMaxTokens())
|
|
||||||
.user(event.getAuthor().getId())
|
|
||||||
.temperature(settings.getOpenAISettings().getTemperature())
|
|
||||||
.topP(settings.getOpenAISettings().getTopP())
|
|
||||||
.frequencyPenalty(settings.getOpenAISettings().getFrequencyPenalty())
|
|
||||||
.presencePenalty(settings.getOpenAISettings().getPresencePenalty())
|
|
||||||
.prompt(message)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatMessage createSystemMessage(String message) {
|
|
||||||
return new ChatMessage(Role.SYSTEM.getName(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatMessage createAssistantMessage(String message) {
|
|
||||||
return new ChatMessage(Role.ASSISTANT.getName(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatMessage createUserMessage(String message) {
|
|
||||||
return new ChatMessage(Role.USER.getName(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleResponse(ChatMessage chatMessage, MessageReceivedEvent event, String response) {
|
|
||||||
LOGGER.trace("Message Response <{}>: {}", event.getMessageId(), response);
|
|
||||||
Set<String> tags = NLP.getTags(chatMessage.getContent());
|
|
||||||
Set<String> responseTags = NLP.getTags(response);
|
|
||||||
|
|
||||||
MessageData.MessageDataBuilder builder = new MessageData.MessageDataBuilder()
|
|
||||||
.guildId(event.getGuild().getIdLong())
|
|
||||||
.channelId(event.getChannel().getIdLong())
|
|
||||||
.userId(event.getAuthor().getIdLong())
|
|
||||||
.type(chatMessage.getRole())
|
|
||||||
.prompt(chatMessage.getContent())
|
|
||||||
.response(response)
|
|
||||||
.tags(tags)
|
|
||||||
.responseTags(responseTags);
|
|
||||||
|
|
||||||
LOGGER.trace("Tags: {}", tags);
|
|
||||||
|
|
||||||
if (event.isFromThread()) {
|
|
||||||
DatabaseManager.storeMessage(builder.build());
|
|
||||||
ThreadChannel channel = event.getChannel().asThreadChannel();
|
|
||||||
channel.sendMessage(response).queue();
|
|
||||||
} else {
|
|
||||||
// The max discord title length is 100 characters
|
|
||||||
String threadTitle = chatMessage.getContent();
|
|
||||||
if (chatMessage.getContent().length() > 100) {
|
|
||||||
threadTitle = chatMessage.getContent().substring(0, 100);
|
|
||||||
}
|
|
||||||
event.getMessage().createThreadChannel(threadTitle).queue(channel -> {
|
|
||||||
DatabaseManager.storeMessage(builder.channelId(channel.getIdLong()).build());
|
|
||||||
channel.sendMessage(response).queue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseMessage(String input) {
|
|
||||||
return input.replaceAll("<@.*?>", "").replaceAll(" +", " ").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param event Message event received
|
|
||||||
* @return true if the message should be replied to by the bot, otherwise false
|
|
||||||
*/
|
|
||||||
private boolean shouldReply(MessageReceivedEvent event) {
|
|
||||||
boolean shouldReply = false;
|
|
||||||
try {
|
|
||||||
if (!event.getAuthor().isBot()) {
|
|
||||||
// Check if message mentions the bot
|
|
||||||
shouldReply = event.getMessage().getMentions().getMembers().stream().anyMatch(m -> m.getId().equals(jda.getSelfUser().getId()));
|
|
||||||
// Check if message is a reply
|
|
||||||
if (!shouldReply) {
|
|
||||||
Message message = event.getMessage().getReferencedMessage();
|
|
||||||
shouldReply = event.getMessage().getType().equals(MessageType.INLINE_REPLY) && message != null && message.getAuthor().getId().equals(jda.getSelfUser().getId());
|
|
||||||
}
|
|
||||||
// Check if message is from a bot thread
|
|
||||||
if (!shouldReply && event.isFromThread()) {
|
|
||||||
ThreadChannel channel = event.getChannel().asThreadChannel();
|
|
||||||
shouldReply = event.isFromThread() && channel.getOwner() != null && channel.getOwner().getId().equals(jda.getSelfUser().getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.error("Failed to determine bot reply status; {}", ex.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return shouldReply;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.bensherriff.siren.ai;
|
|
||||||
|
|
||||||
public enum Role {
|
|
||||||
SYSTEM("system"),
|
|
||||||
ASSISTANT("assistant"),
|
|
||||||
USER("user"),
|
|
||||||
;
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
Role(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package com.bensherriff.siren.audio;
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.event.AudioEvent;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame;
|
|
||||||
import net.dv8tion.jda.api.audio.AudioSendHandler;
|
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.nio.Buffer;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
|
|
||||||
public class AudioHandler extends AudioEventAdapter implements AudioSendHandler {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(AudioHandler.class);
|
|
||||||
private final PlayerManager manager;
|
|
||||||
private final AudioPlayer player;
|
|
||||||
private final ByteBuffer buffer;
|
|
||||||
private final MutableAudioFrame frame;
|
|
||||||
private final BlockingQueue<AudioTrack> queue;
|
|
||||||
private final long guildID;
|
|
||||||
|
|
||||||
public AudioHandler(PlayerManager manager, long guildID) {
|
|
||||||
this.manager = manager;
|
|
||||||
player = manager.createPlayer();
|
|
||||||
player.setVolume(manager.getListener().getSettings().getGuildSettings().get(guildID).getVolume());
|
|
||||||
player.addListener(this);
|
|
||||||
this.queue = new LinkedBlockingQueue<>();
|
|
||||||
this.buffer = ByteBuffer.allocate(1024);
|
|
||||||
this.frame = new MutableAudioFrame();
|
|
||||||
this.frame.setBuffer(buffer);
|
|
||||||
this.guildID = guildID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addTrack(AudioTrack track) {
|
|
||||||
if (player.getPlayingTrack() == null) {
|
|
||||||
manager.getListener().getExecutor().execute(() -> player.playTrack(track));
|
|
||||||
} else {
|
|
||||||
LOGGER.debug("Track '{}' has been queued", track.getInfo().title);
|
|
||||||
if (!queue.offer(track)) {
|
|
||||||
LOGGER.error("Failed to queue track {}", track.getInfo().title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void nextTrack() {
|
|
||||||
LOGGER.debug("Playing next track");
|
|
||||||
player.stopTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPaused(boolean paused) {
|
|
||||||
manager.getListener().getExecutor().execute(() -> player.setPaused(paused));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPaused() {
|
|
||||||
return player.isPaused();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopTrack() {
|
|
||||||
player.stopTrack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVolume(int volume) {
|
|
||||||
LOGGER.trace("Set volume to {}", volume);
|
|
||||||
player.setVolume(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerPause(AudioPlayer player) {
|
|
||||||
LOGGER.trace("isPaused: {} for {}", player.isPaused(), player.getPlayingTrack().getInfo().title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerResume(AudioPlayer player) {
|
|
||||||
LOGGER.trace("isPaused: {} for {}", player.isPaused(), player.getPlayingTrack().getInfo().title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrackStart(AudioPlayer player, AudioTrack track) {
|
|
||||||
LOGGER.trace("Starting track {}", track.getInfo().title);
|
|
||||||
manager.getListener().getJDA().getPresence().setActivity(Activity.playing(track.getInfo().title));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
|
|
||||||
LOGGER.trace("Track ended due to {}; {} ", endReason.name(), endReason.mayStartNext);
|
|
||||||
if (queue.isEmpty()) {
|
|
||||||
manager.getListener().closeAudioConnection(guildID);
|
|
||||||
manager.getListener().getJDA().getPresence().setActivity(Activity.playing("nothing"));
|
|
||||||
} else {
|
|
||||||
manager.getListener().getExecutor().execute(() -> player.playTrack(queue.poll()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEvent(AudioEvent event) {
|
|
||||||
super.onEvent(event);
|
|
||||||
// LOGGER.trace("On event {}", event.getClass().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) {
|
|
||||||
LOGGER.warn("Exception on track '{}': {}", track.getInfo().title, exception.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) {
|
|
||||||
LOGGER.warn("{} - 'track {}' is stuck", guildID, track.getInfo().title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canProvide() {
|
|
||||||
// returns true if audio was provided
|
|
||||||
return player.provide(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuffer provide20MsAudio() {
|
|
||||||
// flip to make it a read buffer
|
|
||||||
((Buffer) buffer).flip();
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOpus() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.bensherriff.siren.audio;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioConfiguration;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
|
|
||||||
|
|
||||||
public class PlayerManager extends DefaultAudioPlayerManager {
|
|
||||||
|
|
||||||
private final Listener listener;
|
|
||||||
|
|
||||||
public PlayerManager(Listener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Listener getListener() {
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initialize() {
|
|
||||||
getConfiguration().setResamplingQuality(AudioConfiguration.ResamplingQuality.MEDIUM);
|
|
||||||
registerSourceManager(new YoutubeAudioSourceManager());
|
|
||||||
registerSourceManager(SoundCloudAudioSourceManager.createDefault());
|
|
||||||
registerSourceManager(new BandcampAudioSourceManager());
|
|
||||||
registerSourceManager(new VimeoAudioSourceManager());
|
|
||||||
registerSourceManager(new TwitchStreamAudioSourceManager());
|
|
||||||
registerSourceManager(new BeamAudioSourceManager());
|
|
||||||
registerSourceManager(new HttpAudioSourceManager());
|
|
||||||
registerSourceManager(new LocalAudioSourceManager());
|
|
||||||
AudioSourceManagers.registerRemoteSources(this);
|
|
||||||
AudioSourceManagers.registerLocalSource(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public abstract class Command {
|
|
||||||
|
|
||||||
protected static final Logger LOGGER = LogManager.getLogger(Command.class);
|
|
||||||
|
|
||||||
|
|
||||||
protected final Listener listener;
|
|
||||||
protected SlashCommandData slashCommandData;
|
|
||||||
|
|
||||||
protected boolean required = false;
|
|
||||||
|
|
||||||
public Command(Listener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
public abstract void execute(SlashCommandInteractionEvent event) throws IOException;
|
|
||||||
|
|
||||||
public SlashCommandData getSlashCommandData() {
|
|
||||||
return slashCommandData;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Guild getGuild(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
Guild guild = event.getGuild();
|
|
||||||
if (guild == null) {
|
|
||||||
throw new IOException("Could not find the current guild.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return guild;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class HelpCommand extends Command {
|
|
||||||
|
|
||||||
public HelpCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("help", "Provide help information about the bot");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
event.getHook().sendMessage("TODO").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import com.bensherriff.siren.ai.ImageSize;
|
|
||||||
import com.theokanning.openai.image.ImageResult;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class ImageCommand extends Command {
|
|
||||||
|
|
||||||
public ImageCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("image", "Generate an image using DALL-E")
|
|
||||||
.addOption(OptionType.STRING, "prompt", "The prompt for image generation", true)
|
|
||||||
.addOption(OptionType.INTEGER, "count", "The number of images to be generated", false)
|
|
||||||
.addOptions(new OptionData(OptionType.STRING, "type", "The size of the picture", false)
|
|
||||||
.addChoice("Small", ImageSize.SMALL.getSize())
|
|
||||||
.addChoice("Medium", ImageSize.MEDIUM.getSize())
|
|
||||||
.addChoice("Large", ImageSize.LARGE.getSize())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO Store image in database
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
if (event.getUser().getId().equals(listener.getSettings().getOwner())) {
|
|
||||||
String prompt = Objects.requireNonNull(event.getOption("prompt")).getAsString();
|
|
||||||
|
|
||||||
int count = 1;
|
|
||||||
OptionMapping countOption = event.getOption("count");
|
|
||||||
if (countOption != null) {
|
|
||||||
count = countOption.getAsInt();
|
|
||||||
}
|
|
||||||
ImageSize size = ImageSize.SMALL;
|
|
||||||
OptionMapping sizeOption = event.getOption("size");
|
|
||||||
if (sizeOption != null) {
|
|
||||||
if (sizeOption.getAsInt() == 2) {
|
|
||||||
size = ImageSize.MEDIUM;
|
|
||||||
} else if (sizeOption.getAsInt() == 3) {
|
|
||||||
size = ImageSize.LARGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageResult result = listener.getOpenAIManager().createImage(prompt, count, size);
|
|
||||||
StringBuilder responseURLS = new StringBuilder();
|
|
||||||
result.getData().forEach(image -> responseURLS.append(image.getUrl()).append("\n"));
|
|
||||||
event.getHook().sendMessage(responseURLS.toString()).queue();
|
|
||||||
} else {
|
|
||||||
event.getHook().sendMessage("This command is currently only available to " + listener.getOwner() + ".").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class PauseCommand extends Command {
|
|
||||||
|
|
||||||
public PauseCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("pause", "Pause the current track");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
Guild guild = getGuild(event);
|
|
||||||
AudioHandler audioHandler = listener.getGuildAudioPlayer(guild);
|
|
||||||
if (audioHandler.isPaused()) {
|
|
||||||
event.getHook().sendMessage("Playback is already paused.").queue();
|
|
||||||
} else {
|
|
||||||
audioHandler.setPaused(false);
|
|
||||||
event.getHook().sendMessage("Pausing track...").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.database.DatabaseManager;
|
|
||||||
import com.bensherriff.siren.database.QueryBuilder;
|
|
||||||
import com.bensherriff.siren.exceptions.EmptyVoiceChannelException;
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import com.bensherriff.siren.settings.SettingsManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.container.MediaContainerDescriptor;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioTrack;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class PlayCommand extends Command {
|
|
||||||
|
|
||||||
public PlayCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("play", "Play a track")
|
|
||||||
.addSubcommands(new SubcommandData("url", "Play a track from a URL")
|
|
||||||
.addOption(OptionType.STRING, "url", "Track URL", true))
|
|
||||||
.addSubcommands(new SubcommandData("local", "Play a track from a local file")
|
|
||||||
.addOption(OptionType.STRING, "file", "Local file name", true))
|
|
||||||
.addSubcommands(new SubcommandData("tags", "Play a track based on tags")
|
|
||||||
.addOption(OptionType.STRING, "tags", "The list of tags separated by a semicolon", true)
|
|
||||||
.addOption(OptionType.BOOLEAN, "inclusive", "If true, tracks can match any tag"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
String audioDirectoryPath = SettingsManager.AUDIO_DIRECTORY + SettingsManager.SEPARATOR;
|
|
||||||
if ("url".equals(event.getSubcommandName())) {
|
|
||||||
String trackURL = Objects.requireNonNull(event.getOption("url")).getAsString();
|
|
||||||
listener.getPlayerManager().loadItemOrdered(event.getGuild(), trackURL, new ResultHandler(event));
|
|
||||||
} else if ("local".equals(event.getSubcommandName())) {
|
|
||||||
String fileName = Objects.requireNonNull(event.getOption("file")).getAsString();
|
|
||||||
if (!fileName.contains(".m4a")) {
|
|
||||||
fileName = fileName.concat(".m4a");
|
|
||||||
}
|
|
||||||
String trackURL = audioDirectoryPath + fileName;
|
|
||||||
listener.getPlayerManager().loadItemOrdered(event.getGuild(), trackURL, new ResultHandler(event));
|
|
||||||
} else if ("tags".equals(event.getSubcommandName())) {
|
|
||||||
String query;
|
|
||||||
String tagsString = Objects.requireNonNull(event.getOption("tags")).getAsString();
|
|
||||||
String[] tags = tagsString.split(";,");
|
|
||||||
for (int i = 0; i < tags.length; i++) {
|
|
||||||
tags[i] = tags[i].trim();
|
|
||||||
}
|
|
||||||
if (event.getOption("inclusive") != null && Objects.requireNonNull(event.getOption("inclusive")).getAsBoolean()) {
|
|
||||||
query = new QueryBuilder("audio").where("tags && ARRAY[?]").build();
|
|
||||||
} else {
|
|
||||||
query = new QueryBuilder("audio").where("tags @> ARRAY[?]").build();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
List<AudioTrack> tracks = new ArrayList<>();
|
|
||||||
List<Map<String, Object>> results = DatabaseManager.query(query, List.of(tags));
|
|
||||||
if (results.isEmpty()) {
|
|
||||||
event.getHook().sendMessage("No tracks found with the those tags.").queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Map<String, Object> result : results) {
|
|
||||||
// String title = (String) result.get("title");
|
|
||||||
// String author = (String) result.get("author");
|
|
||||||
// long length = (Long) result.get("length");
|
|
||||||
// String identifier = (String) result.get("identifier");
|
|
||||||
String fileName = audioDirectoryPath.concat((String) result.get("file_name"));
|
|
||||||
LOGGER.debug("{}", fileName);
|
|
||||||
listener.getPlayerManager().loadItemOrdered(event.getGuild(), fileName, new ResultHandler(event));
|
|
||||||
}
|
|
||||||
// AudioPlaylist playlist = new BasicAudioPlaylist("Playlist based on tags", tracks, null, false);
|
|
||||||
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to retrieve audio tags; {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ResultHandler implements AudioLoadResultHandler {
|
|
||||||
|
|
||||||
private final Guild guild;
|
|
||||||
private final String userID;
|
|
||||||
private final AudioHandler audioHandler;
|
|
||||||
private final SlashCommandInteractionEvent event;
|
|
||||||
|
|
||||||
private ResultHandler(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
this.event = event;
|
|
||||||
this.userID = event.getUser().getId();
|
|
||||||
this.guild = getGuild(event);
|
|
||||||
this.audioHandler = listener.getGuildAudioPlayer(guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void playTrack(Guild guild, String userID, AudioHandler audioHandler, AudioTrack track) throws EmptyVoiceChannelException {
|
|
||||||
listener.connectToVoiceChannel(userID, guild.getAudioManager());
|
|
||||||
audioHandler.addTrack(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void trackLoaded(AudioTrack track) {
|
|
||||||
try {
|
|
||||||
playTrack(guild, userID, audioHandler, track);
|
|
||||||
String trackTitle = "**" + track.getInfo().title + "**";
|
|
||||||
if (trackTitle.equalsIgnoreCase("Unknown title")) {
|
|
||||||
trackTitle = "track";
|
|
||||||
}
|
|
||||||
event.getHook().sendMessage("Adding " + trackTitle + " to queue...").queue();
|
|
||||||
} catch (EmptyVoiceChannelException e) {
|
|
||||||
event.getHook().sendMessage("You must be connected to a voice channel in order to play tracks!").queue();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playlistLoaded(AudioPlaylist playlist) {
|
|
||||||
AudioTrack firstTrack = playlist.getSelectedTrack();
|
|
||||||
|
|
||||||
if (firstTrack == null) {
|
|
||||||
firstTrack = playlist.getTracks().get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
playTrack(guild, userID, audioHandler, firstTrack);
|
|
||||||
event.getHook().sendMessage("Adding **" + firstTrack.getInfo().title + "** to queue (first track of playlist " + playlist.getName() + ")...").queue();
|
|
||||||
} catch (EmptyVoiceChannelException e) {
|
|
||||||
event.getHook().sendMessage("You must be connected to a voice channel in order to play tracks!").queue();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noMatches() {
|
|
||||||
event.getHook().sendMessage("No track found").queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadFailed(FriendlyException exception) {
|
|
||||||
String errorMsg = "Failed to play track";
|
|
||||||
if (exception.getMessage().contains("Unknown file format")) {
|
|
||||||
event.getHook().sendMessage(errorMsg + ". " + exception.getMessage()).queue();
|
|
||||||
} else {
|
|
||||||
event.getHook().sendMessage(errorMsg + ". Please contact " + listener.getOwner() + ".").queue();
|
|
||||||
}
|
|
||||||
LOGGER.error("{}: {}", errorMsg, exception.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class ResumeCommand extends Command {
|
|
||||||
|
|
||||||
public ResumeCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("resume", "Resume the current track");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
Guild guild = getGuild(event);
|
|
||||||
AudioHandler audioHandler = listener.getGuildAudioPlayer(guild);
|
|
||||||
if (audioHandler.isPaused()) {
|
|
||||||
audioHandler.setPaused(false);
|
|
||||||
event.getHook().sendMessage("Resuming track...").queue();
|
|
||||||
} else {
|
|
||||||
event.getHook().sendMessage("Playback is not currently paused.").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class SkipCommand extends Command {
|
|
||||||
|
|
||||||
public SkipCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("skip", "Skip the current track");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
Guild guild = getGuild(event);
|
|
||||||
AudioHandler audioHandler = listener.getGuildAudioPlayer(guild);
|
|
||||||
audioHandler.nextTrack();
|
|
||||||
event.getHook().sendMessage("Skipping track...").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class StopCommand extends Command {
|
|
||||||
|
|
||||||
public StopCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("stop", "Stop playing tracks and clear the queue");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
Guild guild = getGuild(event);
|
|
||||||
AudioHandler audioHandler = listener.getGuildAudioPlayer(guild);
|
|
||||||
audioHandler.stopTrack();
|
|
||||||
guild.getAudioManager().closeAudioConnection();
|
|
||||||
event.getHook().sendMessage("Stopping the current track and clearing the queue...").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.bensherriff.siren.commands;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.audio.AudioHandler;
|
|
||||||
import com.bensherriff.siren.Listener;
|
|
||||||
import com.bensherriff.siren.settings.SettingsManager;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class VolumeCommand extends Command {
|
|
||||||
|
|
||||||
public VolumeCommand(Listener listener) {
|
|
||||||
super(listener);
|
|
||||||
slashCommandData = Commands.slash("volume", "Set the volume")
|
|
||||||
.addOption(OptionType.INTEGER, "volume", "Updated value", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(SlashCommandInteractionEvent event) throws IOException {
|
|
||||||
Guild guild = getGuild(event);
|
|
||||||
int volume = Objects.requireNonNull(event.getOption("volume")).getAsInt();
|
|
||||||
listener.getSettings().getGuildSettings().get(guild.getIdLong()).setVolume(volume);
|
|
||||||
SettingsManager.write(listener.getSettings());
|
|
||||||
AudioHandler audioHandler = listener.getGuildAudioPlayer(guild);
|
|
||||||
audioHandler.setVolume(volume);
|
|
||||||
event.getHook().sendMessage("Setting volume to " + volume + "...").queue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.bensherriff.siren.database;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class DatabaseConnection {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DatabaseConnection.class);
|
|
||||||
private static final ExecutorService executorService = Executors.newFixedThreadPool(4);
|
|
||||||
private static final ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
|
|
||||||
|
|
||||||
public static Connection getConnection() throws SQLException {
|
|
||||||
Connection connection = connectionThreadLocal.get();
|
|
||||||
|
|
||||||
if (connection == null) {
|
|
||||||
Map<String, String> env = System.getenv();
|
|
||||||
String dbUrl = env.get("DATABASE_URL");
|
|
||||||
String dbUsername = env.get("DATABASE_USERNAME");
|
|
||||||
String dbPassword = env.get("DATABASE_PASSWORD");
|
|
||||||
|
|
||||||
connection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
|
|
||||||
connectionThreadLocal.set(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void closeConnection() {
|
|
||||||
Connection connection = connectionThreadLocal.get();
|
|
||||||
connectionThreadLocal.remove();
|
|
||||||
|
|
||||||
if (connection != null) {
|
|
||||||
executorService.submit(() -> {
|
|
||||||
try {
|
|
||||||
connection.close();
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to close connection; {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void shutdown() {
|
|
||||||
executorService.shutdown();
|
|
||||||
try {
|
|
||||||
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
|
|
||||||
LOGGER.error("ExecutorService did not terminate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
package com.bensherriff.siren.database;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.settings.LocalTrack;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.postgresql.jdbc.PgArray;
|
|
||||||
|
|
||||||
import java.sql.*;
|
|
||||||
import java.sql.Date;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static java.util.Map.entry;
|
|
||||||
|
|
||||||
public class DatabaseManager {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DatabaseManager.class);
|
|
||||||
|
|
||||||
private static final Map<String, List<String>> createTableQueries = Map.ofEntries(
|
|
||||||
entry("messages", List.of(
|
|
||||||
"id SERIAL PRIMARY KEY",
|
|
||||||
"guild_id BIGINT NOT NULL",
|
|
||||||
"channel_id BIGINT NOT NULL",
|
|
||||||
"user_id BIGINT NOT NULL",
|
|
||||||
"type VARCHAR(20) NOT NULL",
|
|
||||||
"prompt TEXT NOT NULL",
|
|
||||||
"response TEXT",
|
|
||||||
"tags TEXT[]",
|
|
||||||
"response_tags TEXT[]",
|
|
||||||
"timestamp TIMESTAMP NOT NULL DEFAULT NOW()")),
|
|
||||||
entry("embeddings", List.of(
|
|
||||||
"id SERIAL PRIMARY KEY",
|
|
||||||
"embeddings FLOAT[] NOT NULL")),
|
|
||||||
entry("message_embeddings", List.of(
|
|
||||||
"id SERIAL PRIMARY KEY",
|
|
||||||
"message_id INT NOT NULL",
|
|
||||||
"embedding_id INT NOT NULL")),
|
|
||||||
entry("audio", List.of(
|
|
||||||
"id SERIAL PRIMARY KEY",
|
|
||||||
"title TEXT NOT NULL",
|
|
||||||
"author TEXT NOT NULL",
|
|
||||||
"length BIGINT NOT NULL",
|
|
||||||
"identifier TEXT NOT NULL",
|
|
||||||
"file_name TEXT NOT NULL",
|
|
||||||
"tags TEXT[]"
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
public static void createTables() {
|
|
||||||
for (Map.Entry<String, List<String>> entry : createTableQueries.entrySet()) {
|
|
||||||
if (!createTable(entry.getKey(), entry.getValue())) {
|
|
||||||
LOGGER.warn("Failed to create one or more required database tables");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGGER.debug("Databases initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean createTable(String tableName, List<String> columns) {
|
|
||||||
if (tableExists(tableName)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
StringBuilder stringBuilder = new StringBuilder("CREATE TABLE IF NOT EXISTS ")
|
|
||||||
.append(tableName).append(" ( ");
|
|
||||||
for (int i = 0; i < columns.size(); i++) {
|
|
||||||
stringBuilder.append(columns.get(i));
|
|
||||||
if (i != columns.size() - 1) {
|
|
||||||
stringBuilder.append(", ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stringBuilder.append(")");
|
|
||||||
|
|
||||||
Connection connection = DatabaseConnection.getConnection();
|
|
||||||
LOGGER.debug("Creating '{}' database table if it does not exist", tableName);
|
|
||||||
Statement statement = connection.createStatement();
|
|
||||||
statement.execute(stringBuilder.toString());
|
|
||||||
return true;
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to create table; {}", ex.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int storeMessage(MessageData messageData) {
|
|
||||||
String INSERT_MESSAGE = "INSERT INTO messages (" +
|
|
||||||
"guild_id, " +
|
|
||||||
"channel_id, " +
|
|
||||||
"user_id, " +
|
|
||||||
"type, " +
|
|
||||||
"prompt, " +
|
|
||||||
"response, " +
|
|
||||||
"tags, " +
|
|
||||||
"response_tags) " +
|
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
|
||||||
return storeMessage("messages", INSERT_MESSAGE,
|
|
||||||
messageData.getGuildId(),
|
|
||||||
messageData.getChannelId(),
|
|
||||||
messageData.getUserId(),
|
|
||||||
messageData.getType(),
|
|
||||||
messageData.getPrompt(),
|
|
||||||
messageData.getResponse(),
|
|
||||||
messageData.getTags(),
|
|
||||||
messageData.getResponseTags()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int storeEmbedding(List<Double> data) {
|
|
||||||
String INSERT_EMBEDDING = "INSERT INTO embeddings (" +
|
|
||||||
"embeddings) " +
|
|
||||||
"VALUES (?)";
|
|
||||||
return storeMessage("embeddings", INSERT_EMBEDDING, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int storeMessageEmbeddings(int messageId, int embeddingId) {
|
|
||||||
String INSERT_MESSAGE_EMBEDDINGS = "INSERT INTO message_embeddings" +
|
|
||||||
"message_id, " +
|
|
||||||
"embeddings_id, " +
|
|
||||||
"VALUES (?, ?)";
|
|
||||||
return storeMessage("message_embeddings", INSERT_MESSAGE_EMBEDDINGS, messageId, embeddingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int storeAudio(LocalTrack track) {
|
|
||||||
String INSERT_AUDIO = "INSERT INTO audio (" +
|
|
||||||
"author, " +
|
|
||||||
"title, " +
|
|
||||||
"length, " +
|
|
||||||
"identifier, " +
|
|
||||||
"file_name, " +
|
|
||||||
"tags) " +
|
|
||||||
"VALUES (?, ?, ?, ?, ?, ?)";
|
|
||||||
return storeMessage("audio", INSERT_AUDIO, track.getAuthor(), track.getTitle(), track.getLength(),
|
|
||||||
track.getIdentifier(), track.getFileName(), track.getTags());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int storeMessage(String tableName, String insertString, Object... params) {
|
|
||||||
if (!tableExists(tableName)) {
|
|
||||||
LOGGER.warn("Table '{}' does not exist", tableName);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Connection connection = DatabaseConnection.getConnection();
|
|
||||||
PreparedStatement preparedStatement = connection.prepareStatement(insertString);
|
|
||||||
int i = 1;
|
|
||||||
for (Object param : params) {
|
|
||||||
if (param == null) {
|
|
||||||
preparedStatement.setNull(i, Types.NULL);
|
|
||||||
} else if (param instanceof String) {
|
|
||||||
preparedStatement.setString(i, (String) param);
|
|
||||||
} else if (param instanceof Integer) {
|
|
||||||
preparedStatement.setInt(i, (int) param);
|
|
||||||
} else if (param instanceof Double) {
|
|
||||||
preparedStatement.setDouble(i, (double) param);
|
|
||||||
} else if (param instanceof Float) {
|
|
||||||
preparedStatement.setFloat(i, (float) param);
|
|
||||||
} else if (param instanceof Long) {
|
|
||||||
preparedStatement.setLong(i, (long) param);
|
|
||||||
} else if (param instanceof Boolean) {
|
|
||||||
preparedStatement.setBoolean(i, (boolean) param);
|
|
||||||
} else if (param instanceof Date) {
|
|
||||||
preparedStatement.setDate(i, (Date) param);
|
|
||||||
} else if (param instanceof Time) {
|
|
||||||
preparedStatement.setTime(i, (Time) param);
|
|
||||||
} else if (param instanceof Timestamp) {
|
|
||||||
preparedStatement.setTimestamp(i, (Timestamp) param);
|
|
||||||
} else if (param instanceof byte[]) {
|
|
||||||
preparedStatement.setBytes(i, (byte[]) param);
|
|
||||||
} else if (param instanceof Blob) {
|
|
||||||
preparedStatement.setBlob(i, (Blob) param);
|
|
||||||
} else if (param instanceof Clob) {
|
|
||||||
preparedStatement.setClob(i, (Clob) param);
|
|
||||||
} else if (param instanceof List && !((List<?>) param).isEmpty() && ((List<?>) param).get(0) instanceof String) {
|
|
||||||
preparedStatement.setArray(i, connection.createArrayOf("text", ((List<String>) param).toArray()));
|
|
||||||
} else if (param instanceof List && !((List<?>) param).isEmpty() && ((List<?>) param).get(0) instanceof Double) {
|
|
||||||
preparedStatement.setArray(i, connection.createArrayOf("float8", ((List<Double>) param).toArray(new Double[0])));
|
|
||||||
} else if (param instanceof Set && !((Set<?>) param).isEmpty() && ((Set<?>) param).toArray()[0] instanceof String) {
|
|
||||||
preparedStatement.setArray(i, connection.createArrayOf("text", ((Set<String>) param).toArray()));
|
|
||||||
} else if (param instanceof String[]) {
|
|
||||||
preparedStatement.setArray(i, connection.createArrayOf("text", ((String[]) param)));
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Unsupported parameter type: " + param.getClass());
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return preparedStatement.executeUpdate();
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to store message; {}", ex.getMessage());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Map<String, Object>> query(String query, Object... params) throws SQLException, IllegalArgumentException {
|
|
||||||
LOGGER.trace("Query: <{}>", query);
|
|
||||||
Connection connection = DatabaseConnection.getConnection();
|
|
||||||
PreparedStatement stmt = connection.prepareStatement(query);
|
|
||||||
int i = 1;
|
|
||||||
for (Object param : params) {
|
|
||||||
if (param instanceof String) {
|
|
||||||
stmt.setString(i++, (String) param);
|
|
||||||
} else if (param instanceof Integer) {
|
|
||||||
stmt.setInt(i++, (Integer) param);
|
|
||||||
} else if (param instanceof Long) {
|
|
||||||
stmt.setLong(i++, (Long) param);
|
|
||||||
} else if (param instanceof Double) {
|
|
||||||
stmt.setDouble(i++, (Double) param);
|
|
||||||
} else if (param instanceof Float) {
|
|
||||||
stmt.setFloat(i++, (Float) param);
|
|
||||||
} else if (param instanceof Timestamp) {
|
|
||||||
stmt.setTimestamp(i++, (Timestamp) param);
|
|
||||||
} else if (param instanceof Boolean) {
|
|
||||||
stmt.setBoolean(i++, (Boolean) param);
|
|
||||||
} else if (param instanceof List) {
|
|
||||||
stmt.setArray(i++, connection.createArrayOf("text", ((List<?>) param).toArray()));
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Unsupported parameter type: " + param.getClass().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<Map<String, Object>> resultList = new ArrayList<>();
|
|
||||||
try (ResultSet resultSet = stmt.executeQuery()) {
|
|
||||||
ResultSetMetaData metaData = resultSet.getMetaData();
|
|
||||||
int columnCount = metaData.getColumnCount();
|
|
||||||
while (resultSet.next()) {
|
|
||||||
Map<String, Object> rowMap = new HashMap<>();
|
|
||||||
for (int j = 1; j <= columnCount; j++) {
|
|
||||||
rowMap.put(metaData.getColumnName(j), resultSet.getObject(j));
|
|
||||||
}
|
|
||||||
resultList.add(rowMap);
|
|
||||||
}
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to execute query; {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
return resultList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean tableExists(String tableName) {
|
|
||||||
try {
|
|
||||||
Connection connection = DatabaseConnection.getConnection();
|
|
||||||
Statement statement = connection.createStatement();
|
|
||||||
ResultSet resultSet = statement.executeQuery("SELECT tablename FROM pg_tables WHERE tablename = '" + tableName + "'");
|
|
||||||
return resultSet.next();
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to check if table exists; {}", ex.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearTable(String tableName) {
|
|
||||||
try {
|
|
||||||
Connection connection = DatabaseConnection.getConnection();
|
|
||||||
Statement statement = connection.createStatement();
|
|
||||||
statement.executeUpdate("TRUNCATE TABLE " + tableName);
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
LOGGER.error("Failed to clear table; {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<MessageData> parseResponse(List<Map<String, Object>> results) throws SQLException {
|
|
||||||
List<MessageData> messageData = new ArrayList<>();
|
|
||||||
for (Map<String, Object> result : results) {
|
|
||||||
Set<String> promptTags = new HashSet<>();
|
|
||||||
for (Object object : (Object[]) ((PgArray) result.get("tags")).getArray()) {
|
|
||||||
promptTags.add((String) object);
|
|
||||||
}
|
|
||||||
Set<String> responseTags = new HashSet<>();
|
|
||||||
for (Object object : (Object[]) ((PgArray) result.get("response_tags")).getArray()) {
|
|
||||||
promptTags.add((String) object);
|
|
||||||
}
|
|
||||||
messageData.add(new MessageData.MessageDataBuilder()
|
|
||||||
.guildId((long) result.get("guild_id"))
|
|
||||||
.channelId((long) result.get("channel_id"))
|
|
||||||
.userId((long) result.get("user_id"))
|
|
||||||
.type((String) result.get("type"))
|
|
||||||
.prompt((String) result.get("prompt"))
|
|
||||||
.response((String) result.get("response"))
|
|
||||||
.tags(promptTags)
|
|
||||||
.responseTags(responseTags)
|
|
||||||
.timestamp((Timestamp) result.get("timestamp"))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
return messageData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package com.bensherriff.siren.database;
|
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class MessageData {
|
|
||||||
private final Long guildId;
|
|
||||||
private final Long channelId;
|
|
||||||
private final Long userId;
|
|
||||||
private final String type;
|
|
||||||
private final String prompt;
|
|
||||||
private final String response;
|
|
||||||
private final Set<String> tags;
|
|
||||||
private final Set<String> responseTags;
|
|
||||||
private final Timestamp timestamp;
|
|
||||||
|
|
||||||
private MessageData(MessageDataBuilder builder) {
|
|
||||||
this.guildId = builder.guildId;
|
|
||||||
this.channelId = builder.channelId;
|
|
||||||
this.userId = builder.userId;
|
|
||||||
this.type = builder.type;
|
|
||||||
this.prompt = builder.prompt;
|
|
||||||
this.response = builder.response;
|
|
||||||
this.tags = builder.tags;
|
|
||||||
this.responseTags = builder.responseTags;
|
|
||||||
this.timestamp = builder.timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getGuildId() {
|
|
||||||
return guildId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getChannelId() {
|
|
||||||
return channelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPrompt() {
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getResponse() {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getTags() {
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getResponseTags() {
|
|
||||||
return responseTags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Timestamp getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MessageDataBuilder {
|
|
||||||
private Long guildId;
|
|
||||||
private Long channelId;
|
|
||||||
private Long userId;
|
|
||||||
private String type = "";
|
|
||||||
private String prompt = "";
|
|
||||||
private String response = "";
|
|
||||||
private Set<String> tags = new LinkedHashSet<>();
|
|
||||||
private Set<String> responseTags = new LinkedHashSet<>();
|
|
||||||
private Timestamp timestamp = new Timestamp(0);
|
|
||||||
|
|
||||||
public MessageDataBuilder guildId(Long guildId) {
|
|
||||||
this.guildId = guildId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder channelId(Long channelId) {
|
|
||||||
this.channelId = channelId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder userId(Long userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder type(String type) {
|
|
||||||
this.type = type;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder prompt(String prompt) {
|
|
||||||
this.prompt = prompt;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder response(String response) {
|
|
||||||
this.response = response;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder tags(Set<String> tags) {
|
|
||||||
this.tags = tags;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder responseTags(Set<String> responseTags) {
|
|
||||||
this.responseTags = responseTags;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageDataBuilder timestamp(Timestamp timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageData build() {
|
|
||||||
return new MessageData(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.bensherriff.siren.database;
|
|
||||||
|
|
||||||
public class QueryBuilder {
|
|
||||||
private boolean distinct;
|
|
||||||
private String columnList;
|
|
||||||
private final String tableName;
|
|
||||||
private String whereClause;
|
|
||||||
private String orderByClause;
|
|
||||||
private Integer limit;
|
|
||||||
|
|
||||||
public QueryBuilder(String tableName) {
|
|
||||||
this.tableName = tableName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryBuilder select(String columnList) {
|
|
||||||
this.columnList = columnList;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryBuilder where(String whereClause) {
|
|
||||||
this.whereClause = whereClause;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryBuilder orderBy(String orderByClause) {
|
|
||||||
this.orderByClause = orderByClause;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryBuilder limit(int limit) {
|
|
||||||
this.limit = limit;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryBuilder distinct(boolean distinct) {
|
|
||||||
this.distinct = distinct;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String build() {
|
|
||||||
StringBuilder queryBuilder = new StringBuilder();
|
|
||||||
queryBuilder.append("SELECT ");
|
|
||||||
if (distinct) {
|
|
||||||
queryBuilder.append("DISTINCT ");
|
|
||||||
}
|
|
||||||
if (columnList != null && !columnList.isEmpty()) {
|
|
||||||
queryBuilder.append(columnList);
|
|
||||||
} else {
|
|
||||||
queryBuilder.append("*");
|
|
||||||
}
|
|
||||||
queryBuilder.append(" FROM ");
|
|
||||||
queryBuilder.append(tableName);
|
|
||||||
if (whereClause != null) {
|
|
||||||
queryBuilder.append(" WHERE ");
|
|
||||||
queryBuilder.append(whereClause);
|
|
||||||
}
|
|
||||||
if (orderByClause != null) {
|
|
||||||
queryBuilder.append(" ORDER BY ");
|
|
||||||
queryBuilder.append(orderByClause);
|
|
||||||
}
|
|
||||||
if (limit != null) {
|
|
||||||
queryBuilder.append(" LIMIT ");
|
|
||||||
queryBuilder.append(limit);
|
|
||||||
}
|
|
||||||
return queryBuilder.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.bensherriff.siren.exceptions;
|
|
||||||
|
|
||||||
public class EmptyVoiceChannelException extends Exception {
|
|
||||||
|
|
||||||
public EmptyVoiceChannelException(String errorMessage) {
|
|
||||||
super(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EmptyVoiceChannelException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.bensherriff.siren.exceptions;
|
|
||||||
|
|
||||||
public class InvalidCommandException extends Exception {
|
|
||||||
public InvalidCommandException(String errorMessage) {
|
|
||||||
super(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvalidCommandException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.ai.Model;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class GuildSettings {
|
|
||||||
|
|
||||||
private String prefix = "!";
|
|
||||||
private int volume = 100;
|
|
||||||
|
|
||||||
private Map<Long, UserSettings> userSettings = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
public String getPrefix() {
|
|
||||||
return prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPrefix(String prefix) {
|
|
||||||
this.prefix = prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getVolume() {
|
|
||||||
return volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVolume(int volume) {
|
|
||||||
this.volume = volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Long, UserSettings> getUserSettings() {
|
|
||||||
return userSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserSettings(Map<Long, UserSettings> userSettings) {
|
|
||||||
this.userSettings = userSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LocalTrack {
|
|
||||||
private String title = "";
|
|
||||||
private String author = "";
|
|
||||||
private long length;
|
|
||||||
private String identifier = "";
|
|
||||||
private String fileName = "";
|
|
||||||
private List<String> tags = new ArrayList<>();
|
|
||||||
|
|
||||||
public String getFileName() {
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileName(String fileName) {
|
|
||||||
this.fileName = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getTags() {
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTags(List<String> tags) {
|
|
||||||
this.tags = tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthor() {
|
|
||||||
return author;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthor(String author) {
|
|
||||||
this.author = author;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLength() {
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLength(long length) {
|
|
||||||
this.length = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getIdentifier() {
|
|
||||||
return identifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIdentifier(String identifier) {
|
|
||||||
this.identifier = identifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LocalTracks {
|
|
||||||
|
|
||||||
private List<LocalTrack> tracks = new ArrayList<>();
|
|
||||||
|
|
||||||
public List<LocalTrack> getTracks() {
|
|
||||||
return tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTracks(List<LocalTrack> localTracks) {
|
|
||||||
this.tracks = localTracks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.ai.Model;
|
|
||||||
import com.bensherriff.siren.ai.Role;
|
|
||||||
|
|
||||||
public class OpenAISettings {
|
|
||||||
/**
|
|
||||||
* One of ['system', 'assistant', 'user']
|
|
||||||
* System: The system role provides full access to all OpenAI APIs and resources, including access to billing information and account management features. An API key with the system role can perform any action that is allowed by the OpenAI API.
|
|
||||||
* Assistant: The assistant role is designed for use with virtual assistants or chatbots that interact with users. An API key with the assistant role can perform actions related to natural language processing, such as generating text, answering questions, or completing tasks. The assistant role is more limited than the system role, but it still provides access to powerful language models such as GPT-3.
|
|
||||||
* User: The user role provides limited access to specific OpenAI APIs and resources. An API key with the user role can only perform actions related to specific use cases, such as accessing a particular language model or dataset. The user role is more restricted than the assistant or system roles, but it is suitable for many common use cases.
|
|
||||||
*/
|
|
||||||
public static Role defaultRole = Role.USER;
|
|
||||||
public static Model defaultModel = Model.ADA_1;
|
|
||||||
public static int defaultMaxTokens = 100;
|
|
||||||
|
|
||||||
private String token = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In milliseconds
|
|
||||||
*/
|
|
||||||
private long timeout = 10000;
|
|
||||||
private double temperature = 0.5;
|
|
||||||
private double topP = 1.0;
|
|
||||||
private double frequencyPenalty = 0.0;
|
|
||||||
private double presencePenalty = 0.0;
|
|
||||||
|
|
||||||
public String getToken() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToken(String token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimeout() {
|
|
||||||
return timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimeout(long timeout) {
|
|
||||||
this.timeout = timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getTemperature() {
|
|
||||||
return temperature;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTemperature(double temperature) {
|
|
||||||
this.temperature = temperature;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getTopP() {
|
|
||||||
return topP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTopP(double topP) {
|
|
||||||
this.topP = topP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getFrequencyPenalty() {
|
|
||||||
return frequencyPenalty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFrequencyPenalty(double frequencyPenalty) {
|
|
||||||
this.frequencyPenalty = frequencyPenalty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getPresencePenalty() {
|
|
||||||
return presencePenalty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPresencePenalty(double presencePenalty) {
|
|
||||||
this.presencePenalty = presencePenalty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class Settings {
|
|
||||||
|
|
||||||
private String token = "";
|
|
||||||
private String owner = "250842261221277697";
|
|
||||||
private int threadPool = 2;
|
|
||||||
private Map<Long, GuildSettings> guildSettings = new HashMap<>();
|
|
||||||
private OpenAISettings openAISettings = new OpenAISettings();
|
|
||||||
|
|
||||||
public String getToken() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToken(String token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOwner() {
|
|
||||||
return owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOwner(String owner) {
|
|
||||||
this.owner = owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getThreadPool() {
|
|
||||||
return threadPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThreadPool(int threadPool) {
|
|
||||||
this.threadPool = threadPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Long, GuildSettings> getGuildSettings() {
|
|
||||||
return guildSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGuildSettings(Map<Long, GuildSettings> guildSettings) {
|
|
||||||
this.guildSettings = guildSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public OpenAISettings getOpenAISettings() {
|
|
||||||
return openAISettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOpenAISettings(OpenAISettings openAISettings) {
|
|
||||||
this.openAISettings = openAISettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
|
|
||||||
public class SettingsManager {
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(SettingsManager.class);
|
|
||||||
public static final String SEPARATOR = File.separator;
|
|
||||||
public static final String USER_DIRECTORY = System.getProperty("user.dir");
|
|
||||||
public static final String PATH = String.join(SEPARATOR, USER_DIRECTORY, "settings.json");
|
|
||||||
public static final String AUDIO_DIRECTORY = String.join(SEPARATOR, USER_DIRECTORY, "audio");
|
|
||||||
public static final String TRACKS_PATH = String.join(SEPARATOR, AUDIO_DIRECTORY, "tracks.json");
|
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
private static final ObjectWriter writer = mapper.writer(new DefaultPrettyPrinter());
|
|
||||||
|
|
||||||
public static Settings load() throws IOException {
|
|
||||||
return load(PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Settings load(String path) throws IOException {
|
|
||||||
return load(path, Settings.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> T load(String path, Class<T> type) throws IOException {
|
|
||||||
File file = new File(path);
|
|
||||||
if (!file.exists()) {
|
|
||||||
LOGGER.warn("{} file does not exist, creating new file at: {}", type.getSimpleName(), file.getPath());
|
|
||||||
try {
|
|
||||||
write(path, type.getConstructor().newInstance());
|
|
||||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
|
|
||||||
throw new IOException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGGER.info("Reading {} file from {}", type.getSimpleName(), file.getPath());
|
|
||||||
try (InputStream inputStream = new FileInputStream(file)) {
|
|
||||||
return mapper.readValue(inputStream, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void write(Settings settings) throws IOException {
|
|
||||||
write(PATH, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void write(String path, Object object) throws IOException {
|
|
||||||
File file = new File(path);
|
|
||||||
writer.writeValue(file, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.bensherriff.siren.settings;
|
|
||||||
|
|
||||||
import com.bensherriff.siren.ai.Model;
|
|
||||||
|
|
||||||
public class UserSettings {
|
|
||||||
private Model model = OpenAISettings.defaultModel;
|
|
||||||
private int maxTokens = OpenAISettings.defaultMaxTokens;
|
|
||||||
|
|
||||||
public Model getModel() {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModel(Model model) {
|
|
||||||
this.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxTokens() {
|
|
||||||
return maxTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxTokens(int maxTokens) {
|
|
||||||
this.maxTokens = maxTokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Configuration status="WARN">
|
|
||||||
<Properties>
|
|
||||||
<Property name="logPath">logs</Property>
|
|
||||||
<Property name="rollingFileName">siren</Property>
|
|
||||||
<Property name="LOG_PATTERN">%d{HH:mm:ss.SSS} | %-5level | %msg%n</Property>
|
|
||||||
</Properties>
|
|
||||||
<Appenders>
|
|
||||||
<Console name="console" target="SYSTEM_OUT">
|
|
||||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
|
||||||
</Console>
|
|
||||||
<RollingFile name="rollingFile" fileName="${logPath}/${rollingFileName}.log"
|
|
||||||
filePattern="${logPath}/${rollingFileName}-%d{yyyy-MM-dd}-%i.log">
|
|
||||||
<PatternLayout pattern="%d{DEFAULT} | %-5level | %c{1}.%M() | %msg%n%throwable{short.lineNumber}"/>
|
|
||||||
<Policies>
|
|
||||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
|
||||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
|
||||||
</Policies>
|
|
||||||
<DefaultRolloverStrategy max="30"/>
|
|
||||||
</RollingFile>
|
|
||||||
</Appenders>
|
|
||||||
<Loggers>
|
|
||||||
<Root level="debug">
|
|
||||||
<AppenderRef ref="rollingFile"/>
|
|
||||||
</Root>
|
|
||||||
<Logger name="com" level="warn" additivity="false">
|
|
||||||
<AppenderRef ref="console"/>
|
|
||||||
</Logger>
|
|
||||||
<Logger name="net" level="warn" additivity="false">
|
|
||||||
<AppenderRef ref="console"/>
|
|
||||||
</Logger>
|
|
||||||
<Logger name="org" level="warn" additivity="false">
|
|
||||||
<AppenderRef ref="console"/>
|
|
||||||
</Logger>
|
|
||||||
<Logger name="edu" level="warn" additivity="false">
|
|
||||||
<AppenderRef ref="console"/>
|
|
||||||
</Logger>
|
|
||||||
<Logger name="com.bensherriff" level="trace" additivity="false">
|
|
||||||
<AppenderRef ref="console"/>
|
|
||||||
<AppenderRef ref="rollingFile"/>
|
|
||||||
</Logger>
|
|
||||||
</Loggers>
|
|
||||||
</Configuration>
|
|
||||||
14
start.sh
14
start.sh
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
#if [ -f /config.txt ]; then
|
|
||||||
# echo "configuration file found"
|
|
||||||
# mv /config.txt .
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#if [ ! -f JMusicBot-${VERSION}.jar ]; then
|
|
||||||
# wget https://github.com/jagrosh/MusicBot/releases/download/${VERSION}/JMusicBot-${VERSION}.jar
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#java -Dnogui=true -jar JMusicBot-${VERSION}.jar
|
|
||||||
|
|
||||||
java -jar /usr/local/lib/siren.jar
|
|
||||||
Reference in New Issue
Block a user