Compare commits
37
Commits
bedc8b9f02
..
3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d0edddff | ||
|
|
0b7c1bfe4f | ||
|
|
3466fb536d | ||
|
|
f01c48c61c | ||
|
|
80104d3e9a | ||
|
|
daac16ea47 | ||
|
|
e07c172a21 | ||
|
|
6429e42384 | ||
|
|
72d8bcef20 | ||
|
|
fb872f2b0b | ||
|
|
6a869afb1d | ||
|
|
88fa8aac23 | ||
|
|
29eecd42a9 | ||
|
|
b15ed9d577 | ||
|
|
04bdb75087 | ||
|
|
96e31a19ec | ||
|
|
9861fac769 | ||
|
|
8479f719ab | ||
|
|
3314ef6bea | ||
|
|
50430589e6 | ||
|
|
6b453bb7e2 | ||
|
|
bae37fc8b1 | ||
|
|
2afab8f1f5 | ||
|
|
4c27b80527 | ||
|
|
e6afe6e4c1 | ||
|
|
e0ef3f9915 | ||
|
|
6f2ba795d4 | ||
|
|
bcf78920e9 | ||
|
|
0ccc2d5e9c | ||
|
|
508c7acc67 | ||
|
|
8be0f94b3e | ||
|
|
5baa69ffe9 | ||
|
|
5a8e606a89 | ||
|
|
9ee4797aaf | ||
|
|
5294cb86ca | ||
|
|
333d638f94 | ||
|
|
4378321c65 |
No files matched your search
@@ -1,15 +0,0 @@
|
|||||||
.git
|
|
||||||
.github
|
|
||||||
.idea
|
|
||||||
.mvn
|
|
||||||
src
|
|
||||||
classes
|
|
||||||
logs
|
|
||||||
target/*
|
|
||||||
!target/netneutrality.jar
|
|
||||||
*.iml
|
|
||||||
README.md
|
|
||||||
checkstyle.xml
|
|
||||||
mvnw
|
|
||||||
mvnw.cmd
|
|
||||||
pom.xml
|
|
||||||
+6
-18
@@ -1,31 +1,19 @@
|
|||||||
NETNEUTRALITY_TAG=latest
|
|
||||||
NETNEUTRALITY_BOT_ENV=dev
|
NETNEUTRALITY_BOT_ENV=dev
|
||||||
NETNEUTRALITY_BOT_TOKEN=
|
NETNEUTRALITY_BOT_TOKEN=
|
||||||
NETNEUTRALITY_BOT_PREFIX=$
|
NETNEUTRALITY_BOT_PREFIX=$
|
||||||
NETNEUTRALITY_BOT_OWNER=
|
NETNEUTRALITY_BOT_OWNER=
|
||||||
NETNEUTRALITY_BOT_INVITE=
|
|
||||||
NETNEUTRALITY_API_PORT=9000
|
NETNEUTRALITY_API_PORT=9000
|
||||||
NETNEUTRALITY_API_KEY=
|
|
||||||
|
|
||||||
MYSQL_HOST=127.0.0.1
|
MYSQL_HOST=127.0.0.1
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_USER=java
|
|
||||||
MYSQL_PASSWORD=password
|
|
||||||
MYSQL_DATABASE=netneutrality
|
|
||||||
|
|
||||||
NETNEUTRALITY_LAVALINK_URI=ws://lavalink:2333
|
|
||||||
NETNEUTRALITY_LAVALINK_PASSWORD=youshallnotpass
|
|
||||||
NETNEUTRALITY_LAVALINK_NAME=dev-player
|
|
||||||
NETNEUTRALITY_PO_TOKEN=
|
NETNEUTRALITY_PO_TOKEN=
|
||||||
NETNEUTRALITY_VISITOR_DATA=
|
NETNEUTRALITY_VISITOR_DATA=
|
||||||
NETNEUTRALITY_OAUTH_TOKEN=
|
|
||||||
NETNEUTRALITY_CIPHER_URL=http://ejs-api:8001
|
|
||||||
NETNEUTRALITY_CIPHER_PASSWORD=
|
|
||||||
NETNEUTRALITY_WEBPO_URL=http://webpo:8080
|
|
||||||
NETNEUTRALITY_WEBPO_PASSWORD=
|
|
||||||
|
|
||||||
NETNEUTRALITY_SENTRY_DSN=
|
NETNEUTRALITY_SENTRY_DSN=
|
||||||
GITEA_HOST=
|
GITLAB_HOST=
|
||||||
GITEA_TOKEN=
|
GITLAB_TOKEN=
|
||||||
GITEA_OWNER=
|
GITLAB_PROJECT_ID=
|
||||||
GITEA_REPO=
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "main"
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- "main"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
package:
|
|
||||||
name: Package
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
timeout-minutes: 15
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '25'
|
|
||||||
cache: 'maven'
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: |
|
|
||||||
chmod +x ./mvnw
|
|
||||||
./mvnw --batch-mode --update-snapshots verify
|
|
||||||
|
|
||||||
- name: Upload test reports
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: junit-reports
|
|
||||||
path: |
|
|
||||||
**/target/*-reports/TEST-*.xml
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Upload Maven artifacts
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: maven-artifacts
|
|
||||||
path: |
|
|
||||||
target/netneutrality.jar
|
|
||||||
startup.sh
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
maven-publish:
|
|
||||||
name: Publish Maven Package
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
timeout-minutes: 15
|
|
||||||
needs: package
|
|
||||||
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '25'
|
|
||||||
cache: 'maven'
|
|
||||||
server-id: gitea
|
|
||||||
server-username: REGISTRY_USER
|
|
||||||
server-password: REGISTRY_PASSWORD
|
|
||||||
|
|
||||||
- name: Publish Maven package
|
|
||||||
run: |
|
|
||||||
chmod +x ./mvnw
|
|
||||||
./mvnw --batch-mode deploy -DskipTests
|
|
||||||
env:
|
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
docker:
|
|
||||||
name: Docker Build and Push
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
timeout-minutes: 15
|
|
||||||
needs: package
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: maven-artifacts
|
|
||||||
path: .
|
|
||||||
|
|
||||||
- name: Compute Docker tags
|
|
||||||
id: meta
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
REGISTRY="${{ secrets.REGISTRY_URL }}/${{ secrets.REGISTRY_USER }}/netneutrality"
|
|
||||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
|
||||||
echo "tags=${REGISTRY}:${ver},${REGISTRY}:latest" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "tags=${REGISTRY}:main" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Log in to registry
|
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
|
|
||||||
with:
|
|
||||||
registry: ${{ secrets.REGISTRY_URL }}
|
|
||||||
username: ${{ secrets.REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
+1
-2
@@ -33,16 +33,15 @@ hs_err_pid*
|
|||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
*.iws
|
*.iws
|
||||||
.junie/plans
|
|
||||||
|
|
||||||
# Mac
|
# Mac
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Maven
|
# Maven
|
||||||
|
log/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
#log4j
|
#log4j
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
docker-compose.override.yml
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
image: eclipse-temurin:17-jdk-alpine
|
||||||
|
|
||||||
|
variables:
|
||||||
|
MAVEN_OPTS: >-
|
||||||
|
-Dhttps.protocols=TLSv1.2
|
||||||
|
-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository
|
||||||
|
-Dorg.slf4j.simpleLogger.showDateTime=true
|
||||||
|
-Djava.awt.headless=true
|
||||||
|
|
||||||
|
MAVEN_CLI_OPTS: >-
|
||||||
|
--batch-mode
|
||||||
|
--errors
|
||||||
|
--fail-at-end
|
||||||
|
--show-version
|
||||||
|
--no-transfer-progress
|
||||||
|
-DinstallAtEnd=true
|
||||||
|
-DdeployAtEnd=true
|
||||||
|
|
||||||
|
cache:
|
||||||
|
key: "maven-$CI_COMMIT_REF_SLUG"
|
||||||
|
paths:
|
||||||
|
- .m2/repository
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- compile
|
||||||
|
- test
|
||||||
|
- package
|
||||||
|
|
||||||
|
compile:
|
||||||
|
stage: compile
|
||||||
|
script:
|
||||||
|
- "chmod +x ./mvnw"
|
||||||
|
- "./mvnw $MAVEN_CLI_OPTS compile"
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- "chmod +x ./mvnw"
|
||||||
|
- "./mvnw $MAVEN_CLI_OPTS test"
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
junit:
|
||||||
|
- "**/target/*-reports/TEST-*.xml"
|
||||||
|
|
||||||
|
package:
|
||||||
|
stage: package
|
||||||
|
script:
|
||||||
|
- "chmod +x ./mvnw"
|
||||||
|
- "./mvnw $MAVEN_CLI_OPTS package -DskipTests=true"
|
||||||
|
artifacts:
|
||||||
|
name: "Maven artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
|
||||||
|
paths:
|
||||||
|
- "**/target"
|
||||||
+18
-3
@@ -1,3 +1,18 @@
|
|||||||
wrapperVersion=3.3.4
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
distributionType=only-script
|
# or more contributor license agreements. See the NOTICE file
|
||||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
|
||||||
|
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||||
+14
-3
@@ -1,6 +1,17 @@
|
|||||||
FROM eclipse-temurin:25-jre@sha256:ff82c74a630c08b417502c4c18189e62074edbb28eb1f2d31b799ac138c01058
|
FROM eclipse-temurin:17-jdk-alpine AS build
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends netcat-openbsd curl && rm -rf /var/lib/apt/lists/*
|
COPY .mvn/ .mvn
|
||||||
|
COPY mvnw pom.xml ./
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN ["chmod", "+x", "./mvnw"]
|
||||||
|
RUN ./mvnw install
|
||||||
|
|
||||||
|
|
||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
|
||||||
|
RUN apk add netcat-openbsd
|
||||||
|
RUN apk add curl
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -8,7 +19,7 @@ ARG API_PORT=9000
|
|||||||
EXPOSE ${API_PORT}
|
EXPOSE ${API_PORT}
|
||||||
|
|
||||||
COPY startup.sh ./
|
COPY startup.sh ./
|
||||||
COPY target/netneutrality.jar ./
|
COPY --from=build /target/netneutrality-jar-with-dependencies.jar ./
|
||||||
|
|
||||||
RUN ["chmod", "+x", "./startup.sh"]
|
RUN ["chmod", "+x", "./startup.sh"]
|
||||||
CMD ["./startup.sh"]
|
CMD ["./startup.sh"]
|
||||||
+7
-19
@@ -1,31 +1,19 @@
|
|||||||
NETNEUTRALITY_TAG=latest
|
|
||||||
|
|
||||||
NETNEUTRALITY_BOT_ENV=dev
|
NETNEUTRALITY_BOT_ENV=dev
|
||||||
NETNEUTRALITY_BOT_TOKEN=
|
NETNEUTRALITY_BOT_TOKEN=
|
||||||
NETNEUTRALITY_BOT_PREFIX=$
|
NETNEUTRALITY_BOT_PREFIX=
|
||||||
NETNEUTRALITY_BOT_OWNER=
|
NETNEUTRALITY_BOT_OWNER=
|
||||||
NETNEUTRALITY_BOT_INVITE=
|
|
||||||
NETNEUTRALITY_API_PORT=9000
|
NETNEUTRALITY_API_PORT=9000
|
||||||
|
|
||||||
MYSQL_HOST=127.0.0.1
|
MYSQL_HOST=127.0.0.1
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_USER=java
|
|
||||||
MYSQL_PASSWORD=password
|
|
||||||
MYSQL_DATABASE=netneutrality
|
|
||||||
MYSQL_ROOT_PASSWORD=password
|
|
||||||
MARIADB_AUTO_UPGRADE=1
|
|
||||||
|
|
||||||
NETNEUTRALITY_PO_TOKEN=
|
NETNEUTRALITY_PO_TOKEN=
|
||||||
NETNEUTRALITY_VISITOR_DATA=
|
NETNEUTRALITY_VISITOR_DATA=
|
||||||
NETNEUTRALITY_CIPHER_URL=http://localhost:8001
|
|
||||||
NETNEUTRALITY_CIPHER_PASSWORD=
|
|
||||||
|
|
||||||
NETNEUTRALITY_LAVALINK_URI=ws://lavalink:2333
|
|
||||||
NETNEUTRALITY_LAVALINK_PASSWORD=youshallnotpass
|
|
||||||
NETNEUTRALITY_LAVALINK_NAME=main
|
|
||||||
|
|
||||||
NETNEUTRALITY_SENTRY_DSN=
|
NETNEUTRALITY_SENTRY_DSN=
|
||||||
GITEA_HOST=
|
GITLAB_HOST=
|
||||||
GITEA_TOKEN=
|
GITLAB_TOKEN=
|
||||||
GITEA_OWNER=
|
GITLAB_PROJECT_ID=
|
||||||
GITEA_REPO=
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!DOCTYPE module PUBLIC
|
|
||||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
|
||||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
|
||||||
|
|
||||||
<module name="Checker">
|
|
||||||
<property name="severity" value="warning"/>
|
|
||||||
|
|
||||||
<!-- Allows tab characters -->
|
|
||||||
<module name="FileTabCharacter">
|
|
||||||
<property name="eachLine" value="false"/>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
<module name="TreeWalker">
|
|
||||||
<!-- Indentation with tabs -->
|
|
||||||
<module name="Indentation">
|
|
||||||
<property name="basicOffset" value="4"/>
|
|
||||||
<property name="tabWidth" value="4"/>
|
|
||||||
<property name="forceStrictCondition" value="false"/>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
<!-- Common useful checks -->
|
|
||||||
<module name="EmptyBlock">
|
|
||||||
<property name="option" value="text"/>
|
|
||||||
</module>
|
|
||||||
<module name="NeedBraces"/>
|
|
||||||
<module name="AvoidStarImport"/>
|
|
||||||
<module name="UnusedImports"/>
|
|
||||||
<module name="EqualsHashCode"/>
|
|
||||||
|
|
||||||
<!-- Relaxed Javadoc rules -->
|
|
||||||
<module name="JavadocMethod">
|
|
||||||
<property name="allowMissingParamTags" value="true"/>
|
|
||||||
<property name="allowMissingReturnTag" value="true"/>
|
|
||||||
</module>
|
|
||||||
</module>
|
|
||||||
</module>
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:lts
|
||||||
|
container_name: db
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- netneutrality-db:/var/lib/mysql
|
||||||
|
- ./sql:/docker-entrypoint-initdb.d
|
||||||
|
environment:
|
||||||
|
MYSQL_USER: 'java'
|
||||||
|
MYSQL_PASSWORD: 'password'
|
||||||
|
MYSQL_DATABASE: 'netneutrality'
|
||||||
|
MYSQL_ROOT_PASSWORD: 'password'
|
||||||
|
MARIADB_AUTO_UPGRADE: '1'
|
||||||
|
ports:
|
||||||
|
- "3306"
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
bot:
|
||||||
|
container_name: bot
|
||||||
|
build: ./
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
environment:
|
||||||
|
MYSQL_HOST: 'db'
|
||||||
|
MYSQL_USER: 'java'
|
||||||
|
MYSQL_PASSWORD: 'password'
|
||||||
|
MYSQL_DATABASE: 'netneutrality'
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
webapp:
|
||||||
|
image: bot-webapp:dev
|
||||||
|
build: ../netneutrality-webapp
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- bot
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
environment:
|
||||||
|
DB_CONNECTION: 'mysql'
|
||||||
|
DB_HOST: 'db'
|
||||||
|
DB_PORT: '3306'
|
||||||
|
DB_DATABASE: 'netneutrality'
|
||||||
|
DB_USERNAME: 'java'
|
||||||
|
DB_PASSWORD: 'password'
|
||||||
|
BOT_URL: 'http://bot:9000'
|
||||||
|
ports:
|
||||||
|
- '8080:80'
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
netneutrality-db:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
+29
-73
@@ -1,88 +1,44 @@
|
|||||||
services:
|
services:
|
||||||
netneutrality-db:
|
db:
|
||||||
image: mariadb:11@sha256:1cac8492bd78b1ec693238dc600be173397efd7b55eabc725abc281dc855b482
|
image: mariadb:lts
|
||||||
container_name: db
|
container_name: db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- netneutrality
|
|
||||||
volumes:
|
volumes:
|
||||||
- netneutrality-db:/var/lib/mysql
|
- netneutrality-db:/var/lib/mysql
|
||||||
|
- ./sql:/docker-entrypoint-initdb.d
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_USER=${MYSQL_USER:-java}
|
MYSQL_USER: 'java'
|
||||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-password}
|
MYSQL_PASSWORD: 'password'
|
||||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-netneutrality}
|
MYSQL_DATABASE: 'netneutrality'
|
||||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-password}
|
MYSQL_ROOT_PASSWORD: 'password'
|
||||||
- MARIADB_AUTO_UPGRADE=1
|
MARIADB_AUTO_UPGRADE: '1'
|
||||||
|
ports:
|
||||||
netneutrality-bot:
|
- "3306:3306"
|
||||||
image: gitea.bobert.me/anthorob/netneutrality:${NETNEUTRALITY_TAG:-latest}
|
|
||||||
container_name: bot
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file: .env
|
|
||||||
networks:
|
networks:
|
||||||
- netneutrality
|
- internal
|
||||||
|
|
||||||
|
bot:
|
||||||
|
container_name: bot
|
||||||
|
build: ./
|
||||||
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- netneutrality-db
|
- db
|
||||||
- lavalink
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_HOST=${MYSQL_HOST:-netneutrality-db}
|
MYSQL_HOST: 'db'
|
||||||
- MYSQL_PORT=${MYSQL_PORT:-3306}
|
MYSQL_PORT: '3306'
|
||||||
- MYSQL_USER=${MYSQL_USER:-java}
|
MYSQL_USER: 'java'
|
||||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-password}
|
MYSQL_PASSWORD: 'password'
|
||||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-netneutrality}
|
MYSQL_DATABASE: 'netneutrality'
|
||||||
- API_PORT=${NETNEUTRALITY_API_PORT:-9000}
|
env_file: ".env"
|
||||||
|
ports:
|
||||||
# Lavalink >= 4.2.0 is required: the node handles DAVE end-to-end encryption,
|
- "9000:9000"
|
||||||
# so the bot no longer ships native encoders.
|
|
||||||
lavalink:
|
|
||||||
image: ghcr.io/lavalink-devs/lavalink:4.2
|
|
||||||
container_name: lavalink
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
networks:
|
||||||
- netneutrality
|
- internal
|
||||||
depends_on:
|
|
||||||
- ejs-api
|
|
||||||
- webpo
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./lavalink/application.yml:/opt/Lavalink/application.yml:ro
|
netneutrality-db:
|
||||||
environment:
|
|
||||||
- SERVER_PASSWORD=${NETNEUTRALITY_LAVALINK_PASSWORD:-youshallnotpass}
|
|
||||||
- NETNEUTRALITY_CIPHER_URL=${NETNEUTRALITY_CIPHER_URL:-http://ejs-api:8001}
|
|
||||||
- NETNEUTRALITY_CIPHER_PASSWORD=${NETNEUTRALITY_CIPHER_PASSWORD:-}
|
|
||||||
- NETNEUTRALITY_OAUTH_TOKEN=${NETNEUTRALITY_OAUTH_TOKEN:-}
|
|
||||||
- NETNEUTRALITY_POT_URL=${NETNEUTRALITY_POT_URL:-http://webpo:8080}
|
|
||||||
- NETNEUTRALITY_PO_TOKEN=${NETNEUTRALITY_PO_TOKEN:-}
|
|
||||||
- NETNEUTRALITY_VISITOR_DATA=${NETNEUTRALITY_VISITOR_DATA:-}
|
|
||||||
|
|
||||||
ejs-api:
|
|
||||||
image: ghcr.io/kikkia/yt-cipher:master@sha256:4c5ec381ff57336cfc24822d2b526a5ad7cd0171f61065a1bbc87bb736842449
|
|
||||||
container_name: ejs_signature_api
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- netneutrality
|
|
||||||
# environment: # UNCOMMENT THIS LINE AND API_TOKEN TO ADD A PASSWORD
|
|
||||||
# - API_TOKEN=test
|
|
||||||
|
|
||||||
webpo:
|
|
||||||
image: ghcr.io/ashton045/webpo-generator
|
|
||||||
container_name: webpo-generator
|
|
||||||
environment:
|
|
||||||
- HOST=${HOST:-::}
|
|
||||||
- PORT=${PORT:-8080}
|
|
||||||
- WORKERS=${WORKERS:-1}
|
|
||||||
- QUEUE_SIZE=${QUEUE_SIZE:-32}
|
|
||||||
- MAX_PENDING_REQUESTS=${MAX_PENDING_REQUESTS:-256}
|
|
||||||
- REQUEST_TIMEOUT=${REQUEST_TIMEOUT:-30000}
|
|
||||||
- CACHE_SIZE=${CACHE_SIZE:-100}
|
|
||||||
- CACHE_TTL=${CACHE_TTL:-300000}
|
|
||||||
- VISITOR_TTL=${VISITOR_TTL:-600000}
|
|
||||||
- API_TOKEN=
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
netneutrality-db: null
|
|
||||||
netneutrality-lavalink-plugins: null
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
netneutrality: null
|
internal:
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Net Neutrality — Technical Documentation
|
|
||||||
|
|
||||||
This directory contains detailed technical documentation for the **Net Neutrality** Discord bot
|
|
||||||
(`me.bobert:netneutrality`, version `3.0.0`).
|
|
||||||
|
|
||||||
It is intended for developers working on the codebase. It documents every major system and
|
|
||||||
subsystem, how they fit together, and the end-to-end flows that tie them.
|
|
||||||
|
|
||||||
## Table of contents
|
|
||||||
|
|
||||||
| Document | Subsystem |
|
|
||||||
|---|---|
|
|
||||||
| [architecture.md](architecture.md) | High-level architecture, startup/shutdown lifecycle, tech stack |
|
|
||||||
| [events.md](events.md) | Event handling & routing (`EventsHandler`) |
|
|
||||||
| [contexts.md](contexts.md) | Contexts — the abstraction over Discord events |
|
|
||||||
| [commands.md](commands.md) | Commands — message & slash commands, dispatch, options |
|
|
||||||
| [tasks.md](tasks.md) | Tasks — button/emoji/modal/step/built-in/persistable/scheduled tasks |
|
|
||||||
| [persistence.md](persistence.md) | Database — Hibernate/JPA, Flyway, entities, repositories, services |
|
|
||||||
| [music.md](music.md) | Music — Lavalink node & client, queue, event dispatch |
|
|
||||||
| [web-api.md](web-api.md) | HTTP Web API — internal control & introspection server |
|
|
||||||
| [threading.md](threading.md) | Threading & scheduling infrastructure |
|
|
||||||
| [utilities-and-bootstrap.md](utilities-and-bootstrap.md) | Utilities, configuration, logging, exceptions |
|
|
||||||
|
|
||||||
## How to read this documentation
|
|
||||||
|
|
||||||
Start with [architecture.md](architecture.md) for the big picture and the startup sequence.
|
|
||||||
Then read [events.md](events.md) and [contexts.md](contexts.md), because almost everything else
|
|
||||||
is triggered through a **Context** produced by the **EventsHandler**. From there, the two primary
|
|
||||||
execution paths are [commands.md](commands.md) and [tasks.md](tasks.md).
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- Class names are given fully qualified on first mention, then by simple name.
|
|
||||||
- Database entity classes are prefixed with `O` (e.g. `OGuild`), and are documented in
|
|
||||||
[persistence.md](persistence.md).
|
|
||||||
- Environment variables / configuration keys use the `NETNEUTRALITY_*` and `MYSQL_*` prefixes and
|
|
||||||
are documented in [utilities-and-bootstrap.md](utilities-and-bootstrap.md).
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# Architecture Overview
|
|
||||||
|
|
||||||
Net Neutrality is a Java 25 Discord bot built on top of [JDA](https://github.com/discord-jda/JDA).
|
|
||||||
It is packaged as a single fat JAR (via the Maven Shade plugin) whose entry point is
|
|
||||||
`me.bobert.netneutrality.App`.
|
|
||||||
|
|
||||||
## Big picture
|
|
||||||
|
|
||||||
Discord sends **events** over the gateway. A single JDA `EventListener`
|
|
||||||
(`me.bobert.netneutrality.hooks.EventsHandler`) receives every event and turns it into a
|
|
||||||
**Context** — a high-level abstraction over that event. Each context is then routed to one of three
|
|
||||||
execution paths:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────┐
|
|
||||||
Discord Gateway ───────▶│ EventsHandler │
|
|
||||||
(JDA events) │ event class → Context factory│
|
|
||||||
└───────────────┬──────────────┘
|
|
||||||
│ builds a Context
|
|
||||||
┌─────────────────────────┼───────────────────────────┐
|
|
||||||
▼ ▼ ▼
|
|
||||||
CommandContext TaskContext (no Context)
|
|
||||||
│ │ lambda /
|
|
||||||
▼ ▼ consumer runs
|
|
||||||
CommandProcessor TaskManager.handle() directly
|
|
||||||
.dispatch() (lookup task by id) (e.g. GuildJoin)
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
Command.execute() Task.execute()
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **Command flow** — the context is a `CommandContext`; it is handed to `CommandProcessor` which
|
|
||||||
resolves and runs a `Command`. See [commands.md](commands.md).
|
|
||||||
2. **Task flow** — the context is a `TaskContext`; it is handed to `TaskManager` which looks up a
|
|
||||||
`Task` by its **id** and runs it. See [tasks.md](tasks.md).
|
|
||||||
3. **Lambda flow** — for simple events (e.g. `GuildJoinEvent`, `GuildLeaveEvent`) a consumer runs
|
|
||||||
directly and no context is routed onward. See [events.md](events.md).
|
|
||||||
|
|
||||||
Around this core are the supporting subsystems: [persistence](persistence.md) (Hibernate + Flyway),
|
|
||||||
[music](music.md) (a remote Lavalink node), the [web API](web-api.md), and the [threading/scheduling](threading.md)
|
|
||||||
infrastructure.
|
|
||||||
|
|
||||||
## Technology stack
|
|
||||||
|
|
||||||
| Concern | Library | Version |
|
|
||||||
|---|---|---|
|
|
||||||
| Discord API | `net.dv8tion:JDA` | 6.4.1 |
|
|
||||||
| Audio engine | Lavalink v4 node (`ghcr.io/lavalink-devs/lavalink`) | 4.2 |
|
|
||||||
| Audio client | `dev.arbjerg:lavalink-client` | 3.4.0 |
|
|
||||||
| ORM | `org.hibernate.orm:hibernate-core` | 7.1.9.Final |
|
|
||||||
| Connection pool | `hibernate-hikaricp` (HikariCP) | 7.1.9.Final |
|
|
||||||
| JDBC driver | `com.mysql:mysql-connector-j` | 9.7.0 |
|
|
||||||
| DB migrations | `org.flywaydb:flyway-core` / `flyway-mysql` | 11.20.3 |
|
|
||||||
| Error tracking | `io.sentry:sentry` | 8.41.0 |
|
|
||||||
| HTTP client | `com.squareup.okhttp3:okhttp-jvm` | 5.3.2 |
|
|
||||||
| JSON | `org.json:json` + `com.google.code.gson:gson` | — |
|
|
||||||
| Classpath scanning | `org.reflections:reflections` | 0.10.2 |
|
|
||||||
| Emoji parsing | `net.fellbaum:jemoji` | 1.7.6 |
|
|
||||||
| Logging | Log4j2 + `log4j-slf4j2-impl` | 2.26.0 |
|
|
||||||
| Testing | JUnit Jupiter 6 + Mockito 5 | — |
|
|
||||||
|
|
||||||
The HTTP **server** for the internal API uses the JDK built-in
|
|
||||||
`com.sun.net.httpserver.HttpServer` (no external framework); see [web-api.md](web-api.md).
|
|
||||||
|
|
||||||
## Startup lifecycle
|
|
||||||
|
|
||||||
The entry point is `App.main()` followed by the `App()` constructor which calls
|
|
||||||
`startup()`. Bootstrap is driven by a hand-rolled
|
|
||||||
[`ApplicationContext`](utilities-and-bootstrap.md#applicationcontext--lifecycle) that owns wired
|
|
||||||
instances and starts every [`Lifecycle`](utilities-and-bootstrap.md#applicationcontext--lifecycle)
|
|
||||||
component in ascending priority order.
|
|
||||||
|
|
||||||
1. **Print banner / read env** — reads `NETNEUTRALITY_BOT_ENV` (default `dev`) via the early-bootstrap
|
|
||||||
`Settings.getInstance()` singleton (the only remaining process-wide settings access point; it is
|
|
||||||
also registered into the context as `SettingsProvider` for constructor injection).
|
|
||||||
2. **Sentry** — if `NETNEUTRALITY_SENTRY_DSN` is set, initialize the Sentry SDK
|
|
||||||
(environment, release = `VERSION`, traces sample rate 1.0).
|
|
||||||
3. **Log levels** — in `dev`, raise JDA and Lavalink client logging to `DEBUG`.
|
|
||||||
4. `startup()`:
|
|
||||||
1. Validate `NETNEUTRALITY_BOT_TOKEN` is present (throws if missing).
|
|
||||||
2. `buildContext()` — register domain services and lifecycle adapters (see priorities below).
|
|
||||||
3. `context.start()` — start every `Lifecycle` component in ascending priority order.
|
|
||||||
4. Register a JVM shutdown hook calling `shutdown()` → `context.stop()`.
|
|
||||||
|
|
||||||
### Lifecycle priorities (startup order)
|
|
||||||
|
|
||||||
| Priority | Component | What `start()` does |
|
|
||||||
|---|---|---|
|
|
||||||
| 10 | `SessionManager` (`SessionProvider`) | Build Hibernate `SessionFactory` + run Flyway migrations. Binds itself into `JpaUtils`. |
|
|
||||||
| 20 | `ThreadManager` (`ExecutorProvider`) | No-op start (executors are lazy). Bound into `ChannelMessagePurger`. |
|
|
||||||
| 30 | `CommandManager` (`CommandRegistry`) | Reflectively discover/register `Command` implementations with context-aware constructor injection. |
|
|
||||||
| 30 | `TaskManagerImpl` (`TaskManager`) | Reflectively load built-in & scheduled tasks (DI) and reload persistable tasks from the DB. |
|
|
||||||
| 40 | `MusicServiceImpl` (`MusicService`) | Create the shared `LavalinkClient`, register the node and subscribe to its events. |
|
|
||||||
| 55 | `ScheduledTaskManager` | No-op start (tasks are scheduled by `TaskManager` during its own start). |
|
|
||||||
| 60 | JDA adapter | Build `JDA` with gateway intents and await ready. |
|
|
||||||
| 65 | Post-JDA adapter | Register slash commands, attach `EventsHandler` (injected providers), set presence. |
|
|
||||||
| 70 | `WebApi` | Start the internal HTTP server (`NETNEUTRALITY_API_PORT`, default 9000). |
|
|
||||||
|
|
||||||
Commands and tasks that need shared services declare them as constructor parameters
|
|
||||||
(`MusicService`, `TaskManager`, `GuildServiceProvider`, `SettingsProvider`, `CommandRegistry`, …).
|
|
||||||
`ReflectionHelper` picks the richest public constructor whose parameter types all resolve from the
|
|
||||||
context (falling back to a no-arg constructor when none match).
|
|
||||||
|
|
||||||
### Gateway intents
|
|
||||||
|
|
||||||
`initJDA()` enables: `GUILD_MEMBERS`, `GUILD_MESSAGE_REACTIONS`, `GUILD_VOICE_STATES`,
|
|
||||||
`MESSAGE_CONTENT`. It also installs `new JDAVoiceUpdateListener(music.getClient())` as the voice
|
|
||||||
dispatch interceptor, so voice server/state updates are forwarded to Lavalink instead of JDA's own
|
|
||||||
audio stack. DAVE (end-to-end encrypted voice) is handled by the node, which is why the bot no
|
|
||||||
longer ships native audio encoders.
|
|
||||||
|
|
||||||
## Shutdown lifecycle
|
|
||||||
|
|
||||||
`App.shutdown()` (invoked by the JVM shutdown hook, `ShutdownCommand`, or the HTTP
|
|
||||||
`/actions/shutdown` endpoint) calls `context.stop()`, which stops every registered `Lifecycle`
|
|
||||||
component in the **exact reverse** of start order. Failures in one component are logged and do not
|
|
||||||
block the others. Effectively this means:
|
|
||||||
|
|
||||||
1. `WebApi.stop()` — stop the HTTP server and its executor.
|
|
||||||
2. Post-JDA adapter — no-op.
|
|
||||||
3. JDA adapter — `jda.shutdown()`.
|
|
||||||
4. `ScheduledTaskManager.stop()` — cancel all scheduled tasks.
|
|
||||||
5. `MusicServiceImpl.stop()` — dispose the event subscriptions, destroy the players and close the Lavalink client.
|
|
||||||
6. `CommandManager` / `TaskManagerImpl` — no-op stop today.
|
|
||||||
7. `ThreadManager.stop()` — shut down the shared executors.
|
|
||||||
8. `SessionManager.stop()` — release the Hibernate registry / connection pool.
|
|
||||||
|
|
||||||
## Configuration & build
|
|
||||||
|
|
||||||
- **Config sources** — `Settings` loads `bot.properties` and overlays environment variables (env
|
|
||||||
vars win). See [utilities-and-bootstrap.md](utilities-and-bootstrap.md#settings). An example file
|
|
||||||
lives at `bot.properties.example`.
|
|
||||||
- **Build** — `./mvnw package` produces `target/netneutrality.jar` (Shade fat JAR, main class
|
|
||||||
`me.bobert.netneutrality.App`). Checkstyle (`checkstyle.xml`) runs in the `test` phase
|
|
||||||
and fails the build on violations.
|
|
||||||
- **Run** — the preferred method is Docker Compose (`docker compose build && docker compose up -d`),
|
|
||||||
which also provisions the MariaDB database. See `README.md` and `docker-compose.yml`.
|
|
||||||
|
|
||||||
## Package map
|
|
||||||
|
|
||||||
| Package | Contents |
|
|
||||||
|---------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|
|
||||||
| `me.bobert.netneutrality` | Entry point (`App`) |
|
|
||||||
| `…bootstrap` | `ApplicationContext`, `Lifecycle`, `BotState`, `ReflectionHelper` |
|
|
||||||
| `…core.command` | Command engine (registry, processor, manager, options, impl bases) |
|
|
||||||
| `…core.context` | Context interfaces, abstract bases (`CommandContext`, `TaskContext`, etc.) |
|
|
||||||
| `…core.tasks` | Task engine (manager, subtypes, traits, persistence, generic buttons) |
|
|
||||||
| `…core.thread` | Executors & scheduling (`ThreadManager`, `ScheduledTaskManager`, `BaseThreadFactory`) |
|
|
||||||
| `…core` | `LogHandler` |
|
|
||||||
| `…command.*` | Concrete command implementations (`admin`, `basic`, `bot`, `crypto`, `custom`, `git`, `music`, `react`) |
|
|
||||||
| `…context.command` / `…context.tasks` | Concrete context implementations (slash, message, button, modal, emoji, etc.) |
|
|
||||||
| `…context` | `MusicChannelContext` |
|
|
||||||
| `…tasks.*` | Concrete task implementations (`git`, `help`, `music`, `reaction`) |
|
|
||||||
| `…db` | Session/migration, `Repository`, `JpaUtils`, `GSettings` |
|
|
||||||
| `…db.entities` / `…db.repository` / `…db.service` | Persistence model, repositories, services |
|
|
||||||
| `…hooks` | `EventsHandler` |
|
|
||||||
| `…http` | Internal web API and HTTP client module |
|
|
||||||
| `…music` | Music domain (service, manager, queue, scheduler, track loading) |
|
|
||||||
| `…utils` | `Settings` / `SettingsProvider`, `ChannelMessagePurger`, `MusicUtils` |
|
|
||||||
| `…shared.util` | Cross-cutting helpers |
|
|
||||||
| `…exceptions` | Custom exception hierarchy |
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
# Commands
|
|
||||||
|
|
||||||
**Packages:** `me.bobert.netneutrality.framework.command` (framework),
|
|
||||||
`me.bobert.netneutrality.feature.*.command` (concrete commands)
|
|
||||||
|
|
||||||
A **Command** is what a user executes on Discord. It receives a
|
|
||||||
[`CommandContext`](contexts.md#commandcontext) and runs logic based on it. There are two primary
|
|
||||||
kinds:
|
|
||||||
|
|
||||||
- **Slash commands** — first-class Discord application commands. Their structure (options,
|
|
||||||
subcommands, groups) has rules defined by Discord and must be configured **procedurally** via
|
|
||||||
`CommandOptionsBuilder`.
|
|
||||||
- **Message (prefix) commands** — a plain message beginning with the configured prefix
|
|
||||||
(`NETNEUTRALITY_BOT_PREFIX`, default `$`).
|
|
||||||
|
|
||||||
A core goal of the framework is that **both kinds are authored the same way**: a command declares its
|
|
||||||
metadata and options once, and the framework adapts it to either interaction style. Most of that
|
|
||||||
adaptation logic lives in `CommandProcessor`.
|
|
||||||
|
|
||||||
## The `Command` contract
|
|
||||||
|
|
||||||
`Command` (interface) defines:
|
|
||||||
|
|
||||||
- Metadata — `getCommandName()`, `getDescription()`, `getCategory()`
|
|
||||||
([`CommandCategory`](#commandcategory)).
|
|
||||||
- Type — `getType()` (`SLASH` by default).
|
|
||||||
- Permissions — `needAdmin()`, `canBeExecutedBy(...)`, guild-only / slash-only constraints.
|
|
||||||
- Option declaration — `registerCommandOptions(CommandOptionsBuilder)` (procedural option setup).
|
|
||||||
- Execution — `execute(CommandContext) : CommandExecutionResult`.
|
|
||||||
|
|
||||||
### Base implementations (`core.command.impl`)
|
|
||||||
|
|
||||||
| Class | Adds |
|
|
||||||
|---|---|
|
|
||||||
| `AbstractCommand` | Default `Command` implementation; type defaults to `SLASH`; basic permission logic. |
|
|
||||||
| `AdminCommand` | Sets `needAdmin() = true` and category `ADMINISTRATION`. |
|
|
||||||
| `MusicCommand` | Guild-only; verifies the guild's music role and music channel before execution. |
|
|
||||||
| `MessageMenuCommand` | Discord **message** context-menu command; skips option registration; wraps the target `Message`. |
|
|
||||||
| `UserMenuCommand` | Discord **user** context-menu command; skips option registration; wraps the target `User`. |
|
|
||||||
|
|
||||||
## Unified options
|
|
||||||
|
|
||||||
To let one command definition serve both slash and message input, values are read through a wrapper
|
|
||||||
interface:
|
|
||||||
|
|
||||||
- `CommandOption` (interface) — uniform accessors: `getAsString()`, `getAsInt()`, `getAsMember()`,
|
|
||||||
`getAsBoolean()`, … .
|
|
||||||
- `SlashCommandOption` — wraps JDA's `OptionMapping` (Discord already parsed the value).
|
|
||||||
- `MessageCommandOption` — wraps a value parsed manually from the message text.
|
|
||||||
|
|
||||||
### Procedural option configuration — `CommandOptionsBuilder`
|
|
||||||
|
|
||||||
`CommandOptionsBuilder` implements JDA's `SlashCommandData` and acts as a proxy over JDA's internal
|
|
||||||
`CommandDataImpl`. A command builds its argument surface in `registerCommandOptions(builder)` by
|
|
||||||
adding options, subcommands, and subcommand groups. The same declaration is used both to:
|
|
||||||
|
|
||||||
1. register the slash command with Discord (see [registration](#registration-lifecycle)), and
|
|
||||||
2. drive the message-command parser (which tries to match tokens in the message against the declared
|
|
||||||
`OptionData`).
|
|
||||||
|
|
||||||
## Command management
|
|
||||||
|
|
||||||
### `CommandManager`
|
|
||||||
|
|
||||||
Responsible for **discovery** and **registration**.
|
|
||||||
|
|
||||||
- **Discovery** — at `init()`, uses [`ReflectionHelper`](utilities-and-bootstrap.md#reflectionhelper)
|
|
||||||
to scan for `Command` implementations and instantiate them.
|
|
||||||
- **Storage** — commands are kept in a `ConcurrentHashMap<String, CommandContainer>` keyed by command
|
|
||||||
name.
|
|
||||||
- **Slash registration** — `registerSlashCommands(JDA)` converts each command's declaration into JDA
|
|
||||||
`SlashCommandData` and pushes it to Discord's API.
|
|
||||||
|
|
||||||
### `CommandContainer`
|
|
||||||
|
|
||||||
Wraps a `Command` instance together with its JDA metadata (`CommandDataImpl`). It also builds the
|
|
||||||
"usage" string shown by the help command.
|
|
||||||
|
|
||||||
### `CommandCategory`
|
|
||||||
|
|
||||||
Enum grouping commands for help/display purposes (e.g. `ADMINISTRATION`, `UTILITY`, `MUSIC`,
|
|
||||||
`CRYPTO`, and an unlisted/hidden group for owner/debug commands).
|
|
||||||
|
|
||||||
### `CommandExecutionResult`
|
|
||||||
|
|
||||||
The value returned by `execute(...)`. It encodes the outcome and any response payload:
|
|
||||||
|
|
||||||
- `success(...)` — normal success (optionally with a message/embed to send).
|
|
||||||
- `error(...)` / `warning(...)` — failure/advisory outcomes with a message.
|
|
||||||
- `none()` — nothing to send synchronously (the command handles its own async response, e.g. music).
|
|
||||||
- May also carry a follow-up **`Task`** so a command can install an interactive
|
|
||||||
[task](tasks.md) (e.g. buttons) as part of its result.
|
|
||||||
|
|
||||||
## The dispatch flow — `CommandProcessor`
|
|
||||||
|
|
||||||
`CommandProcessor` is constructed with the `CommandContext` and driven by `dispatch()`:
|
|
||||||
|
|
||||||
1. **Resolution** — match the invoked name against a `CommandContainer` in `CommandManager`.
|
|
||||||
2. **Validation**
|
|
||||||
- command enabled?
|
|
||||||
- guild-only / slash-only constraints satisfied?
|
|
||||||
- **permissions** — `canBeExecutedBy(...)` and, for admin commands, `needAdmin()` verified against
|
|
||||||
the bot owner (`NETNEUTRALITY_BOT_OWNER`) and the member's guild-admin status.
|
|
||||||
3. **Usage parsing (`checkUsage`)** — detect whether a subcommand or subcommand group is being
|
|
||||||
invoked and record the active subcommand on the context.
|
|
||||||
4. **Option parsing**
|
|
||||||
- **Slash** — Discord already parsed the arguments; map each `OptionMapping` to a
|
|
||||||
`SlashCommandOption`.
|
|
||||||
- **Message** — `MessageCommandContext` tokenizes the message and matches tokens against the
|
|
||||||
declared `OptionData` (resolving mentions to members/roles, parsing integers/booleans, etc.).
|
|
||||||
5. **Execution** — call `command.execute(context)`.
|
|
||||||
6. **Result handling** — act on the returned `CommandExecutionResult`: send success/error/warning
|
|
||||||
responses, or, if a `Task` was returned, register it with the [`TaskManager`](tasks.md).
|
|
||||||
|
|
||||||
Errors are surfaced through the exception hierarchy
|
|
||||||
([`CommandExecutionException`](utilities-and-bootstrap.md#exceptions), and
|
|
||||||
`GracefulContextError` for user-facing, non-logged failures).
|
|
||||||
|
|
||||||
## Registration lifecycle
|
|
||||||
|
|
||||||
```
|
|
||||||
App.startup()
|
|
||||||
├─ CommandManager.init() // reflective discovery → ConcurrentHashMap
|
|
||||||
└─ CommandManager.registerSlashCommands(jda) // push SlashCommandData to Discord
|
|
||||||
```
|
|
||||||
|
|
||||||
At runtime, an incoming command event becomes a `CommandContext` in the
|
|
||||||
[`EventsHandler`](events.md), which calls `new CommandProcessor(context).dispatch()`.
|
|
||||||
|
|
||||||
## Command catalog
|
|
||||||
|
|
||||||
Concrete commands live under `me.bobert.netneutrality.feature.*.command`, grouped by feature package:
|
|
||||||
|
|
||||||
| Command | Package | Type | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `BlacklistCommand` | `admin` | Slash | Manage the server's word blacklist. |
|
|
||||||
| `ReactionCommand` | `admin` | Slash | Manage emoji-reaction roles on the server. |
|
|
||||||
| `HelpCommand` | `basic` | Slash | List every command with its usage. |
|
|
||||||
| `PingCommand` | `basic` | Slash | Reply with the gateway ping. |
|
|
||||||
| `ConfigureCommand` | `bot` | Slash | Configure the bot for the server (subcommands). |
|
|
||||||
| `CryptoPriceCommand` | `crypto` | Slash | Fetch cryptocurrency prices. |
|
|
||||||
| `ReportIssueCommand` | `git` | Slash | Report a bug (opens the Gitea report flow). |
|
|
||||||
| `PlayCommand` | `music` | Slash | Search/queue and start playback. |
|
|
||||||
| `QueueCommand` / `ViewQueueCommand` | `music` | Slash | View/manage the queue. |
|
|
||||||
| `SkipCommand` / `VoteSkipCommand` | `music` | Slash | Skip (or vote-skip) the current track. |
|
|
||||||
| `ClearQueueCommand` / `ShuffleCommand` | `music` | Slash | Clear / shuffle the queue. |
|
|
||||||
| `JoinCommand` / `LeaveCommand` | `music` | Slash | Join / leave the voice channel. |
|
|
||||||
| `CurrentlyPlayingCommand` | `music` | Slash | Show the currently playing track. |
|
|
||||||
| `ResumeCommand` / `StopCommand` / `VolumeCommand` | `music` | Slash | Playback controls. |
|
|
||||||
| `BitconnectCommand` | `music` | Slash | Novelty music command. |
|
|
||||||
| `CreateEmojiReaction` / `ModifyEmojiReaction` | `react` | Message menu | Create/modify emoji-role mappings from a message. |
|
|
||||||
| `DatabaseCommand`, `PurgeCommand`, `ShutdownCommand`, `JoinClanCommand`, `TestCommand`, `SlashCommandTest`, `TrackStuckTest` | `custom` | Mixed | Owner/debug and personal utilities (the `custom` package is gitignored per-deployment). |
|
|
||||||
|
|
||||||
### Representative commands
|
|
||||||
|
|
||||||
**`PingCommand` (simplest slash command)** — extends `AbstractCommand`, overrides `execute` to return
|
|
||||||
`CommandExecutionResult.success(...)` containing the JDA gateway ping. It illustrates the minimal
|
|
||||||
execution path (no options, synchronous response).
|
|
||||||
|
|
||||||
**`ConfigureCommand` (complex slash command)** — uses `registerCommandOptions` to declare several
|
|
||||||
`SubcommandData` (e.g. music-volume, error-logging). In `execute`, it switches on
|
|
||||||
`context.getSubcommand()` and reads typed values via `context.getOption(name, resolver)`, then
|
|
||||||
persists them through the [guild settings service](persistence.md#service-layer).
|
|
||||||
|
|
||||||
**`PlayCommand` (music)** — extends `MusicCommand` (so it enforces guild-only + music role/channel).
|
|
||||||
It declares a single required `STRING` query option, delegates loading to
|
|
||||||
[`MusicManager`](music.md), and returns `CommandExecutionResult.none()` because the audio subsystem
|
|
||||||
produces the user-facing response asynchronously (often installing a
|
|
||||||
[`LoadPlaylistTask`](tasks.md#concrete-task-catalog) with choice buttons).
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# Contexts
|
|
||||||
|
|
||||||
**Packages:** `me.bobert.netneutrality.framework.context` (framework),
|
|
||||||
`me.bobert.netneutrality.framework.context` (concrete implementations)
|
|
||||||
|
|
||||||
A **Context** is a high-level abstraction over a Discord event. It hides the differences between the
|
|
||||||
many JDA event types (slash command, message, button, modal, reaction, …) behind a single, uniform
|
|
||||||
API for reading who/where the event came from and for replying to it.
|
|
||||||
|
|
||||||
Contexts are created either by the [`EventsHandler`](events.md) when Discord triggers an event, or by
|
|
||||||
another context further down a flow (for example, a reply produces an [`AsyncContext`](#asynccontext)
|
|
||||||
that can be edited later). A context can be handled by one of three flows depending on its type and
|
|
||||||
the event it wraps: the [command processor](commands.md), the [task manager](tasks.md), or a simple
|
|
||||||
lambda. **The id of a context only matters for tasks** — the task manager uses `getId()` to locate
|
|
||||||
the `Task` that should run (see [tasks.md](tasks.md)).
|
|
||||||
|
|
||||||
## Type hierarchy
|
|
||||||
|
|
||||||
```
|
|
||||||
Context (interface)
|
|
||||||
├── InteractionContext (interface) ── reply/defer/ack via IReplyCallback + InteractionHook
|
|
||||||
├── NoMessageContext (interface) ── defaults for events without a triggering message
|
|
||||||
│ └── MessageContext (interface) ── adds a triggering Message (prefix commands, replies)
|
|
||||||
│
|
|
||||||
└── AbstractContext (abstract, implements Context) ── stores JDA/Guild/Channel/Member/User
|
|
||||||
├── CommandContext (abstract) ── command flow; holds CommandOptions + subcommand metadata
|
|
||||||
│ ├── SlashCommandContext
|
|
||||||
│ ├── MessageCommandContext
|
|
||||||
│ ├── MessageMenuContext
|
|
||||||
│ └── UserMenuContext
|
|
||||||
├── TaskContext (abstract) ── task flow; abstract getId(); contextData state map
|
|
||||||
│ ├── ButtonTaskContext
|
|
||||||
│ ├── EmojiTaskContext
|
|
||||||
│ ├── MessageTaskContext
|
|
||||||
│ └── ModalTaskContext
|
|
||||||
├── AsyncContext ── wraps the result of a previous reply (edit/follow-up)
|
|
||||||
└── MusicChannelContext ── synthetic context targeting a guild's music channel
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core framework types
|
|
||||||
|
|
||||||
### `Context` (interface)
|
|
||||||
|
|
||||||
The root contract for every context. Notable methods:
|
|
||||||
|
|
||||||
- `getJDA()`, `getGuild()`, `getChannel()`, `getGuildUser()` (`Member`), `getDiscordUser()` (`User`)
|
|
||||||
- `reply(...)` — unified send; overloads support plain text, embeds, message components, an
|
|
||||||
`ephemeral` flag, and an async callback that receives an [`AsyncContext`](#asynccontext) for
|
|
||||||
follow-up edits.
|
|
||||||
- `defer(ephemeral, consumer)` — signals "thinking…" and later completes the response.
|
|
||||||
- `ack(ephemeral)` — minimal acknowledgement of the event.
|
|
||||||
- `getInteractionHook()` — the JDA `InteractionHook` when the underlying event is an interaction.
|
|
||||||
|
|
||||||
### `AbstractContext` (abstract class)
|
|
||||||
|
|
||||||
Base implementation that stores the common JDA entities (`JDA`, `Guild`, `MessageChannel`, `Member`,
|
|
||||||
`User`) and exposes them via the `Context` getters. Its constructor also sets the **Sentry user**
|
|
||||||
(id + effective name) so any error captured while handling this context is attributed to the right
|
|
||||||
Discord user:
|
|
||||||
|
|
||||||
```java
|
|
||||||
if (user != null) {
|
|
||||||
io.sentry.protocol.User sentryUser = new io.sentry.protocol.User();
|
|
||||||
sentryUser.setId(user.getId());
|
|
||||||
sentryUser.setUsername(user.getEffectiveName());
|
|
||||||
Sentry.setUser(sentryUser);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `InteractionContext` (interface)
|
|
||||||
|
|
||||||
Behavioral mix-in for events that are JDA `Interaction`s (slash commands, buttons, modals, context
|
|
||||||
menus). It implements `reply`/`defer`/`ack` in terms of `IReplyCallback` and `InteractionHook`, so
|
|
||||||
these contexts can reply ephemerally, defer, and edit the original response.
|
|
||||||
|
|
||||||
### `NoMessageContext` / `MessageContext` (interfaces)
|
|
||||||
|
|
||||||
- `NoMessageContext` — provides sensible defaults for events that have **no** triggering message
|
|
||||||
(e.g. reactions, interactions).
|
|
||||||
- `MessageContext extends NoMessageContext` — adds access to a triggering `Message`, used by prefix
|
|
||||||
commands and by replies that drive step tasks.
|
|
||||||
|
|
||||||
## Command contexts
|
|
||||||
|
|
||||||
### `CommandContext`
|
|
||||||
|
|
||||||
Abstract base for the [command flow](commands.md). It carries the parsed
|
|
||||||
[`CommandOption`s](commands.md#unified-options) and the active subcommand / subcommand-group metadata,
|
|
||||||
and exposes type-safe option access, e.g. `getOption(name, resolver)`. Concrete subclasses adapt a
|
|
||||||
specific JDA event to this API.
|
|
||||||
|
|
||||||
| Class | Wraps | Notes |
|
|
||||||
|-------------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `SlashCommandContext` | `SlashCommandInteractionEvent` | Options already parsed by Discord; maps `OptionMapping` → `SlashCommandOption`. Interaction-based replies. |
|
|
||||||
| `MessageCommandContext` | `MessageReceivedEvent` (prefix) | Contains the **manual parser** that turns the raw message string into `CommandOption`s (mentions, integers, booleans, …). Message-based replies. |
|
|
||||||
| `MessageMenuContext` | `MessageContextInteractionEvent` | "Apps → …" action on a message; exposes the target `Message`. |
|
|
||||||
| `UserMenuContext` | `UserContextInteractionEvent` | "Apps → …" action on a user; exposes the target `User`. |
|
|
||||||
|
|
||||||
## Task contexts
|
|
||||||
|
|
||||||
### `TaskContext`
|
|
||||||
|
|
||||||
Abstract base for the [task flow](tasks.md). Two things distinguish it:
|
|
||||||
|
|
||||||
- **`getId()` (abstract)** — the id used by `TaskManager` to find the matching `Task`. How the id is
|
|
||||||
derived depends on the concrete subtype (see the table below and [tasks.md](tasks.md#the-task-id)).
|
|
||||||
- **`contextData` map** — a small state bag used to carry values between steps of a multi-step flow.
|
|
||||||
|
|
||||||
| Class | Wraps | `getId()` derived from |
|
|
||||||
|----------------------|------------------------------------|------------------------------------------------------------|
|
|
||||||
| `ButtonTaskContext` | `ButtonInteractionEvent` | `getComponentId()` of the clicked button |
|
|
||||||
| `ModalTaskContext` | `ModalInteractionEvent` | modal `customId` |
|
|
||||||
| `EmojiTaskContext` | `MessageReactionAddEvent` | composite `guildId:messageId` |
|
|
||||||
| `MessageTaskContext` | `MessageReceivedEvent` (no prefix) | the id of the message being replied to (drives step tasks) |
|
|
||||||
|
|
||||||
## Reply mechanism & `AsyncContext`
|
|
||||||
|
|
||||||
### `AsyncContext`
|
|
||||||
|
|
||||||
`me.bobert.netneutrality.framework.context.AsyncContext` wraps the **result** of a previous reply — either an
|
|
||||||
`InteractionHook` (for interactions) or a `Message` (for message replies) — so follow-up actions are
|
|
||||||
expressed the same way regardless of the original trigger:
|
|
||||||
|
|
||||||
```java
|
|
||||||
context.reply("Initial response", async -> {
|
|
||||||
// 'async' is an AsyncContext for the message just sent
|
|
||||||
async.edit("Updated response");
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Under the hood:
|
|
||||||
|
|
||||||
- **Interactions** — edits/follow-ups go through `hook.editOriginal()` / `hook.sendMessage()`.
|
|
||||||
- **Messages** — edits/follow-ups go through `message.editMessage()` / `message.reply()`.
|
|
||||||
|
|
||||||
This is what makes the command and task code path-agnostic: authors write against `Context` /
|
|
||||||
`AsyncContext` and never branch on "was this a slash command or a text message?".
|
|
||||||
|
|
||||||
### `MusicChannelContext`
|
|
||||||
|
|
||||||
A synthetic `AbstractContext` that targets a guild's configured **music channel** rather than the
|
|
||||||
channel an event came from. The music subsystem uses it to post now-playing / queue messages into the
|
|
||||||
correct channel even when playback state changes without a fresh user interaction. See
|
|
||||||
[music.md](music.md).
|
|
||||||
|
|
||||||
## End-to-end summary
|
|
||||||
|
|
||||||
| Event | Context | Flow | Async result wraps |
|
|
||||||
|---------------------------------|-------------------------|---------|--------------------|
|
|
||||||
| `SlashCommandInteractionEvent` | `SlashCommandContext` | Command | `InteractionHook` |
|
|
||||||
| `MessageReceivedEvent` (prefix) | `MessageCommandContext` | Command | `Message` |
|
|
||||||
| `ButtonInteractionEvent` | `ButtonTaskContext` | Task | `InteractionHook` |
|
|
||||||
| `MessageReactionAddEvent` | `EmojiTaskContext` | Task | `Message` |
|
|
||||||
| `ModalInteractionEvent` | `ModalTaskContext` | Task | `InteractionHook` |
|
|
||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
# Event Handling & Routing
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality.platform.event`
|
|
||||||
**Primary class:** `EventsHandler`
|
|
||||||
|
|
||||||
`EventsHandler` is the single JDA `EventListener` registered on the `JDA` instance
|
|
||||||
(`jda.addEventListener(new EventsHandler())` in `App.startup()`). It is the funnel through
|
|
||||||
which **every** gateway event passes. Its job is to translate a raw JDA event into a
|
|
||||||
[Context](contexts.md) and route that context to the correct subsystem.
|
|
||||||
|
|
||||||
## Listener registry
|
|
||||||
|
|
||||||
Listeners are held in a static, thread-safe map keyed by the JDA event class:
|
|
||||||
|
|
||||||
```java
|
|
||||||
private static final Map<Class<? extends Event>, Function<? extends Event, Context>> listeners
|
|
||||||
= new ConcurrentHashMap<>();
|
|
||||||
```
|
|
||||||
|
|
||||||
Each entry is a **factory function**: given an event of that type, it returns a `Context` (or `null`).
|
|
||||||
The map is populated once in a `static {}` block.
|
|
||||||
|
|
||||||
### `addListener` vs `addGenericListener`
|
|
||||||
|
|
||||||
- `addListener(Class<T>, Function<? super T, Context>)` — registers a factory that produces a
|
|
||||||
`Context` to be routed onward (command or task flow).
|
|
||||||
- `addGenericListener(Class<T>, Consumer<T>)` — registers a factory that runs a side-effecting
|
|
||||||
consumer and returns `null`, so nothing is routed onward. This is the **lambda flow** for simple
|
|
||||||
events.
|
|
||||||
|
|
||||||
## Registered events
|
|
||||||
|
|
||||||
| JDA event | Produces | Flow |
|
|
||||||
|---|---|---|
|
|
||||||
| `MessageReceivedEvent` (prefix match) | `MessageCommandContext` | Command |
|
|
||||||
| `MessageReceivedEvent` (no prefix) | `MessageTaskContext` | Task |
|
|
||||||
| `SlashCommandInteractionEvent` | `SlashCommandContext` | Command |
|
|
||||||
| `UserContextInteractionEvent` | `UserMenuContext` | Command |
|
|
||||||
| `MessageContextInteractionEvent` | `MessageMenuContext` | Command |
|
|
||||||
| `ButtonInteractionEvent` | `ButtonTaskContext` | Task |
|
|
||||||
| `ModalInteractionEvent` | `ModalTaskContext` | Task |
|
|
||||||
| `MessageReactionAddEvent` | `EmojiTaskContext` | Task |
|
|
||||||
| `GuildJoinEvent` | — (consumer) | Lambda |
|
|
||||||
| `GuildLeaveEvent` | — (consumer) | Lambda |
|
|
||||||
|
|
||||||
### Message routing detail
|
|
||||||
|
|
||||||
For `MessageReceivedEvent` the factory:
|
|
||||||
|
|
||||||
1. Ignores messages authored by bots and webhook messages (returns `null`).
|
|
||||||
2. Reads the configured prefix (`NETNEUTRALITY_BOT_PREFIX`, default `$`).
|
|
||||||
3. If the trimmed message starts with the prefix → **`MessageCommandContext`** (command flow).
|
|
||||||
4. Otherwise → **`MessageTaskContext`** (task flow). This lets ordinary messages participate in the
|
|
||||||
task system, e.g. as a reply that advances a [step task](tasks.md#33-step-tasks).
|
|
||||||
|
|
||||||
### Guild lifecycle (lambda flow)
|
|
||||||
|
|
||||||
- `GuildJoinEvent` — resolves `GuildService.get(guild)`; if debug logging is enabled for that guild,
|
|
||||||
sends a notice to the debug channel.
|
|
||||||
- `GuildLeaveEvent` — `GuildService.get(guild).deleteGuild()` removes the guild's stored data.
|
|
||||||
|
|
||||||
## Dispatch algorithm
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
public void onEvent(@Nonnull GenericEvent event) {
|
|
||||||
handleEvent((Event) event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleEvent(Event event) {
|
|
||||||
Class<?> eventClass = event.getClass();
|
|
||||||
while (eventClass != null) {
|
|
||||||
if (listeners.containsKey(eventClass)) {
|
|
||||||
Context context = listeners.get(eventClass).apply(event);
|
|
||||||
if (context instanceof CommandContext c) handleCommand(c);
|
|
||||||
if (context instanceof TaskContext t) handleTask(t);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
eventClass = eventClass.getSuperclass(); // walk up the class hierarchy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Key points:
|
|
||||||
|
|
||||||
- **Superclass walk** — if the exact event class is not registered, the handler walks up the
|
|
||||||
superclass chain until it finds a match. This means a listener registered for a base event type
|
|
||||||
also catches subclasses.
|
|
||||||
- **Routing by context type** — the resulting object is routed purely by its runtime type:
|
|
||||||
a `CommandContext` goes to `CommandProcessor.dispatch()`, a `TaskContext` goes to
|
|
||||||
`TaskManager.getInstance().handle(context)`.
|
|
||||||
- **`null` contexts** (bot messages, consumer-only events) are simply not routed anywhere.
|
|
||||||
|
|
||||||
## Extending the event system
|
|
||||||
|
|
||||||
To handle a new event type:
|
|
||||||
|
|
||||||
1. Add an `addListener(SomeEvent.class, SomeContext::new)` (or `addGenericListener(...)` for a
|
|
||||||
consumer-only side effect) to the `static {}` block.
|
|
||||||
2. If it should run the command or task flow, make the produced context extend
|
|
||||||
[`CommandContext`](contexts.md#commandcontext) or [`TaskContext`](contexts.md#taskcontext)
|
|
||||||
respectively.
|
|
||||||
-170
@@ -1,170 +0,0 @@
|
|||||||
# Music
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality.music`
|
|
||||||
**Engine:** a remote [Lavalink](https://lavalink.dev) v4 node driven by
|
|
||||||
[`dev.arbjerg:lavalink-client`](https://github.com/lavalink-devs/Lavalink-Client) 3.4.0.
|
|
||||||
|
|
||||||
The music subsystem provides per-guild audio playback: searching/loading tracks from multiple
|
|
||||||
sources, queueing, scheduling, and streaming audio into a Discord voice channel. Since the
|
|
||||||
migration to Lavalink, **decoding, source resolution and voice sending happen on the node**, not in
|
|
||||||
the bot process. The bot only sends REST/WebSocket commands and reacts to the node's events, which
|
|
||||||
means new sources (YouTube, LavaSrc, …) are added by configuring server-side plugins rather than by
|
|
||||||
changing Java code.
|
|
||||||
|
|
||||||
## Key classes
|
|
||||||
|
|
||||||
| Class | Responsibility |
|
|
||||||
|---|---|
|
|
||||||
| `MusicServiceImpl` | Owns the shared `LavalinkClient`, registers the node, and dispatches its event flux. |
|
|
||||||
| `MusicManager` | Central per-guild coordinator; resolves the guild's `Link` and exposes a synchronous facade. |
|
|
||||||
| `QueueManager` | Holds the guild's track queue (`LinkedBlockingDeque<TrackInfo>`); enqueue/skip/shuffle. |
|
|
||||||
| `TrackScheduler` | Event sink for `onTrackStart` / `onTrackEnd` / `onTrackException` / `onTrackStuck`; caches the current track. |
|
|
||||||
| `TrackLoadingHandler` | `AbstractAudioLoadResultHandler`; handles track/playlist/search/no-match/failure results. |
|
|
||||||
| `TrackInfo` | Value object pairing a Lavalink `Track` with its requester (`Member`). |
|
|
||||||
| `SourceFinder` | Identifies the source/type of a query (YouTube, SoundCloud, HTTP, …). |
|
|
||||||
| `StuckTrackRecoveryHandler` | Retry-then-skip logic for tracks the node reports as stuck. |
|
|
||||||
|
|
||||||
`MusicServiceImpl` keeps a `Map<String, MusicManager>` of managers keyed by guild id, so each guild
|
|
||||||
has its own link, scheduler, and queue.
|
|
||||||
|
|
||||||
## Startup
|
|
||||||
|
|
||||||
`MusicServiceImpl` is a priority-40 [`Lifecycle`](architecture.md#startup-lifecycle) component. Its
|
|
||||||
constructor creates the `LavalinkClient` (the bot user id is derived from the token) and `start()`
|
|
||||||
registers the configured node. **A node that is unreachable never prevents the bot from booting**:
|
|
||||||
the client retries in the background and music commands reply with an error in the meantime.
|
|
||||||
|
|
||||||
Because the client must exist before JDA is built, `App.initJDA()` (priority 60) installs
|
|
||||||
`new JDAVoiceUpdateListener(music.getClient())` as the gateway's voice dispatch interceptor. JDA's
|
|
||||||
`AudioManager` is therefore **not** used for playback; voice connections are opened and closed
|
|
||||||
through `JDA#getDirectAudioController()`.
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
| Setting | Default | Meaning |
|
|
||||||
|---|---|---|
|
|
||||||
| `NETNEUTRALITY_LAVALINK_URI` | `ws://lavalink:2333` | WebSocket URI of the node |
|
|
||||||
| `NETNEUTRALITY_LAVALINK_PASSWORD` | — | Node password; music is disabled when missing |
|
|
||||||
| `NETNEUTRALITY_LAVALINK_NAME` | `main` | Node name used in logs |
|
|
||||||
|
|
||||||
## Audio pipeline (end-to-end)
|
|
||||||
|
|
||||||
```
|
|
||||||
/play query
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
MusicManager.play(query, ctx)
|
|
||||||
│ SourceFinder identifies the source and builds the identifier
|
|
||||||
▼
|
|
||||||
link.loadItem(identifier).subscribe(new TrackLoadingHandler(...))
|
|
||||||
│
|
|
||||||
├─ ontrackLoaded ──▶ connect to voice (MusicUtils) ──▶ QueueManager.queue(track, member)
|
|
||||||
├─ onPlaylistLoaded / onSearchResultLoaded ──▶ install a LoadPlaylistTask (choice buttons)
|
|
||||||
├─ noMatches ──▶ reply "nothing found"
|
|
||||||
└─ loadFailed ──▶ reply / log error
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
QueueManager: if nothing is playing, send createOrUpdatePlayer().setTrack(...); else append
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Lavalink node decodes and streams the audio to the Discord voice gateway
|
|
||||||
│
|
|
||||||
▼ (track ends)
|
|
||||||
node event ──▶ MusicServiceImpl dispatcher ──▶ TrackScheduler.onTrackEnd ──▶ QueueManager.startNextTrack()
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Event dispatch** — `MusicServiceImpl` holds a single subscription per event type
|
|
||||||
(`TrackStartEvent`, `TrackEndEvent`, `TrackExceptionEvent`, `TrackStuckEvent`,
|
|
||||||
`WebSocketClosedEvent`) and routes each event to the owning guild's `TrackScheduler`. All
|
|
||||||
subscriptions are disposed in `stop()`.
|
|
||||||
- **Synchronous facade** — every Lavalink operation returns a reactive `Mono`. `MusicManager`
|
|
||||||
subscribes internally so the 13 music commands keep their existing synchronous signatures;
|
|
||||||
failures are reported through `LogHandler` and the attached `MusicChannelContext`.
|
|
||||||
- **Current track** — the node's player state is only readable via REST, so the last
|
|
||||||
`TrackStartEvent` is cached on the scheduler and used by `getCurrentlyPlaying()` and by the queue
|
|
||||||
to decide whether playback must be started.
|
|
||||||
- **Requester tracking** — each queued item is a `TrackInfo` (track + requesting `Member`), used for
|
|
||||||
now-playing/queue displays and vote-skip. Duplicate rejection compares the encoded track payload.
|
|
||||||
- **Playlists / search** — playlist and search loads produce a
|
|
||||||
[`LoadPlaylistTask`](tasks.md#concrete-task-catalog) with per-result buttons
|
|
||||||
(`LoadTrackFromPlaylistTask`), tying the music and [task](tasks.md) subsystems together. Playlist
|
|
||||||
size is bounded by the [`MUSIC_MAXPLAYLISTSIZE`](persistence.md#gsettings) guild setting.
|
|
||||||
- **Stuck tracks** — the first `TrackStuckEvent` re-queues the track and seeks back to the last
|
|
||||||
known position; a second consecutive event skips to the next track.
|
|
||||||
- **Output** — playback messages are posted through a
|
|
||||||
[`MusicChannelContext`](contexts.md#musicchannelcontext) so they land in the guild's configured
|
|
||||||
music channel.
|
|
||||||
|
|
||||||
## Running the node
|
|
||||||
|
|
||||||
`docker-compose.yml` defines a `lavalink` service (image `ghcr.io/lavalink-devs/lavalink:4.2`, the
|
|
||||||
minimum version that handles DAVE end-to-end encrypted voice) on the `netneutrality` network, with
|
|
||||||
`lavalink/application.yml` mounted read-only at `/opt/Lavalink/application.yml`. The bot depends on
|
|
||||||
it and the node depends on the existing `ejs-api` (yt-cipher) container.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d lavalink
|
|
||||||
docker compose logs -f lavalink # wait for "Lavalink is ready to accept connections"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding a server-side plugin
|
|
||||||
|
|
||||||
Plugins are declared in `lavalink/application.yml` and downloaded by the node on boot into the
|
|
||||||
`netneutrality-lavalink-plugins` volume — no bot rebuild is required:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
lavalink:
|
|
||||||
plugins:
|
|
||||||
- dependency: "dev.lavalink.youtube:youtube-plugin:1.18.2"
|
|
||||||
snapshot: false
|
|
||||||
```
|
|
||||||
|
|
||||||
A plugin that is not on the default Lavalink repository can be pulled from any Maven repository by
|
|
||||||
adding `repository:` to the entry. The node currently runs a JitPack build of the SABR-enabled
|
|
||||||
YouTube fork ([`ashton045/youtube-source`](https://jitpack.io/#ashton045/youtube-source), branch
|
|
||||||
`feat/sabr-support`) — no local build of the fork is required, JitPack publishes a ready Lavalink
|
|
||||||
plugin jar:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
lavalink:
|
|
||||||
plugins:
|
|
||||||
- dependency: "com.github.ashton045.youtube-source:youtube-plugin:17619654b1"
|
|
||||||
repository: "https://jitpack.io"
|
|
||||||
snapshot: false
|
|
||||||
```
|
|
||||||
|
|
||||||
The node logs `Failed to check for updates for youtube-plugin: Not Found` on boot; this is harmless,
|
|
||||||
JitPack publishes no `maven-metadata.xml` for commit-pinned versions.
|
|
||||||
|
|
||||||
Plugin-specific configuration goes under the top-level `plugins:` key. The YouTube plugin is
|
|
||||||
configured there with the remote cipher URL, poToken and visitorData, which used to live in
|
|
||||||
`MusicServiceImpl.start()`.
|
|
||||||
|
|
||||||
## Udio support (deferred)
|
|
||||||
|
|
||||||
Udio playback is **temporarily unavailable**. The previous implementation was a client-side
|
|
||||||
Lavaplayer `AudioSourceManager`, and Lavalink can only resolve sources that live on the server.
|
|
||||||
`SourceFinder` still recognises Udio URLs but returns no identifier, and `MusicManager.play` replies
|
|
||||||
that Udio is temporarily unsupported.
|
|
||||||
|
|
||||||
The original `UdioAudioSourceManager`, `UdioAudioTrack` and their test have been preserved verbatim
|
|
||||||
under [`docs/reference/udio-lavalink-plugin/`](reference/udio-lavalink-plugin) — they are **not
|
|
||||||
compiled** — as the reference for a future Lavalink **server plugin** port. That plugin is out of
|
|
||||||
scope of this migration and is tracked as a follow-up.
|
|
||||||
|
|
||||||
## Guild lifecycle & cleanup
|
|
||||||
|
|
||||||
- `MusicPlayerAutoDisconnect` (scheduled task) leaves voice when the channel is empty or the queue
|
|
||||||
has run out; the connected channel is read from the bot's gateway voice state.
|
|
||||||
- `MusicManagerCleanup` (scheduled task) evicts unused per-guild `MusicManager` instances.
|
|
||||||
- On shutdown, `MusicServiceImpl.stop()` disposes the event subscriptions, clears the queues,
|
|
||||||
destroys every player and closes the client
|
|
||||||
(called from [`App.shutdown()`](architecture.md#shutdown-lifecycle)).
|
|
||||||
|
|
||||||
## Related commands
|
|
||||||
|
|
||||||
All music commands extend [`MusicCommand`](commands.md#base-implementations-corecommandimpl) (guild
|
|
||||||
only + music role/channel enforced): `PlayCommand`, `SkipCommand`/`VoteSkipCommand`,
|
|
||||||
`QueueCommand`/`ViewQueueCommand`, `ClearQueueCommand`, `ShuffleCommand`, `JoinCommand`/`LeaveCommand`,
|
|
||||||
`CurrentlyPlayingCommand`, `ResumeCommand`/`StopCommand`, `VolumeCommand`, `BitconnectCommand`. See
|
|
||||||
[commands.md](commands.md#command-catalog).
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# Persistence & Database
|
|
||||||
|
|
||||||
**Packages:** `me.bobert.netneutrality.platform.persistence` (session/migration),
|
|
||||||
`me.bobert.netneutrality.db` (entities, repositories, services).
|
|
||||||
**Resources:** `hibernate.cfg.xml`, `db/migration/V1__initial_schema.sql`,
|
|
||||||
`db/migration/V2__task_type_id.sql`.
|
|
||||||
|
|
||||||
## Stack
|
|
||||||
|
|
||||||
| Concern | Technology |
|
|
||||||
|------------|-------------------------------------------------------------------------------|
|
|
||||||
| ORM | Hibernate 7.1.9 (Jakarta JPA) |
|
|
||||||
| Engine | MariaDB 11 (per `docker-compose.yml`), MySQL Connector/J 9.7.0 driver, InnoDB |
|
|
||||||
| Pooling | HikariCP (via `hibernate-hikaricp`) |
|
|
||||||
| Migrations | Flyway 11.20.3 (`flyway-core` + `flyway-mysql`) |
|
|
||||||
|
|
||||||
Configuration is externalized via `Settings` (env vars / `bot.properties`):
|
|
||||||
`MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`.
|
|
||||||
|
|
||||||
## Session & migration lifecycle
|
|
||||||
|
|
||||||
Both are wired in at [startup](architecture.md#startup-lifecycle):
|
|
||||||
|
|
||||||
1. **`SessionManager.getSessionFactory()`** — lazily builds the Hibernate `SessionFactory`. It reads
|
|
||||||
the static config from `hibernate.cfg.xml` (dialect `MySQLDialect`, driver
|
|
||||||
`com.mysql.cj.jdbc.Driver`, the Hikari connection provider, and the mapped entity list) and
|
|
||||||
overrides the JDBC url/user/password from environment variables:
|
|
||||||
|
|
||||||
```java
|
|
||||||
settings.put(Environment.JAKARTA_JDBC_URL,
|
|
||||||
String.format("jdbc:mysql://%s:%s/%s", dbHost, dbPort, dbName));
|
|
||||||
settings.put(Environment.JAKARTA_JDBC_USER, dbUser);
|
|
||||||
settings.put(Environment.JAKARTA_JDBC_PASSWORD, dbPass);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **`DatabaseMigration.migrate()`** — runs `flyway.migrate()` against the same database, applying the
|
|
||||||
versioned scripts under `src/main/resources/db/migration/` (starting with
|
|
||||||
`V1__initial_schema.sql`). The Flyway Maven plugin (`flyway-dev.sh`/`.ps1`) is available for local
|
|
||||||
development, using `FLYWAY_URL` / `FLYWAY_USER` / `FLYWAY_PASSWORD`.
|
|
||||||
|
|
||||||
3. **`SessionManager.shutdown()`** — destroys the service registry / releases the pool on shutdown.
|
|
||||||
|
|
||||||
Repositories obtain sessions through the factory; transactional access uses helpers in
|
|
||||||
[`JpaUtils`](utilities-and-bootstrap.md) (open session, run in a transaction, close).
|
|
||||||
|
|
||||||
## Entity catalog
|
|
||||||
|
|
||||||
Entities live in `me.bobert.netneutrality.data.entity` and are prefixed with `O`. `OGuild` is the hub
|
|
||||||
of a star-shaped model; most other tables reference `guild_id` with `ON DELETE CASCADE`.
|
|
||||||
|
|
||||||
| Entity | Table | Key | Relationships / notes |
|
|
||||||
|-----------------|------------------|--------------------------|--------------------------------------------------------------------------------------------------------|
|
|
||||||
| `OGuild` | `guilds` | `guild_id` (String) | Central entity; one-to-many with settings, blacklist, log messages, logging. |
|
|
||||||
| `OGuildSetting` | `guild_settings` | `id` (auto int) | Many-to-one `OGuild`; key/value guild settings. |
|
|
||||||
| `OBlacklist` | `blacklist` | `id` (auto int) | Many-to-one `OGuild`; blacklisted words. |
|
|
||||||
| `OLogging` | `logging` | `OLoggingId` (composite) | Composite key `guild_id` + `channel_id`; `@MapsId` to `OGuild`; debug/error log channel config. |
|
|
||||||
| `OLoggingId` | — (embeddable) | — | `@Embeddable`/`@IdClass` for the composite key. |
|
|
||||||
| `OLogMessage` | `log_message` | `id` (auto int) | Many-to-one `OGuild`; message logging history. |
|
|
||||||
| `OTask` | `tasks` | `id` (auto int) | [Persistable task](tasks.md#31-persistable-tasks) state; unique `task_id`, `task_class` holds the logical `typeId`, JSON `payload`. |
|
|
||||||
| `OWebUser` | `web_users` | `id` (String) | Web dashboard user (tokens/expiry). *Present in schema & code but not listed in `hibernate.cfg.xml`.* |
|
|
||||||
|
|
||||||
### ER overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌───────────────┐
|
|
||||||
│ OGuild │ (guilds.guild_id)
|
|
||||||
└───────┬───────┘
|
|
||||||
┌───────────┬───────┼────────────┬─────────────┐
|
|
||||||
▼ ▼ ▼ ▼ ▼
|
|
||||||
OGuildSetting OBlacklist OLogging OLogMessage (…)
|
|
||||||
guild_settings blacklist logging log_message
|
|
||||||
(FK guild) (FK guild) (PK guild+ (FK guild)
|
|
||||||
channel)
|
|
||||||
|
|
||||||
OTask (tasks) OWebUser (web_users) ── standalone
|
|
||||||
```
|
|
||||||
|
|
||||||
`OTask` and `OWebUser` are not tied to the guild graph.
|
|
||||||
|
|
||||||
## Repository layer
|
|
||||||
|
|
||||||
A lightweight generic repository pattern is used.
|
|
||||||
|
|
||||||
- **`Repository<O>`** (interface) — generic CRUD: `findAll()`, `findById(String)`, `save(O)` (via
|
|
||||||
`session.merge()`), `deleteBy(String)`.
|
|
||||||
- **`BlacklistRepository`** — find-by-guild and case-insensitive word lookups.
|
|
||||||
- **`GuildRepository`** — CRUD for guilds.
|
|
||||||
- **`GuildSettingRepository`** — set/get setting values; removes rows that equal the default value.
|
|
||||||
- **`MessageRepository`** — message logging / edit history.
|
|
||||||
- **`TaskRepository`** — CRUD for `OTask` rows (used by the task persistence flow).
|
|
||||||
|
|
||||||
## Service layer
|
|
||||||
|
|
||||||
Services wrap repositories with business logic and **caching**.
|
|
||||||
|
|
||||||
- **`GuildService`** — gateway for guild operations; caches one `GuildService` per guild id
|
|
||||||
(`ConcurrentHashMap`); `findOrCreate` guarantees a guild row exists; `deleteGuild()` on leave;
|
|
||||||
`isDebugEnabled()` / debug-channel helpers used by [`EventsHandler`](events.md) and
|
|
||||||
[`LogHandler`](utilities-and-bootstrap.md#loghandler).
|
|
||||||
- **`BlacklistService`** — maintains a `blacklistCache` (guild id → set of words) for fast message
|
|
||||||
filtering.
|
|
||||||
- **`GuildSettingService`** — reads/writes guild settings backed by [`GSettings`](#gsettings); caches
|
|
||||||
values in `settingsCache` and reloads on change; used by the
|
|
||||||
[web API settings endpoints](web-api.md).
|
|
||||||
- **`TaskService`** — serializes [`PersistableTask`](tasks.md#31-persistable-tasks) instances to JSON
|
|
||||||
for the `tasks` table, keyed by the task's stable `typeId()` in `task_class`, and reconstructs them
|
|
||||||
at startup via [`PersistableTaskRegistry`](tasks.md#persistabletaskregistry--discovery-and-restore).
|
|
||||||
Rows with an unknown `task_class` are logged and skipped.
|
|
||||||
|
|
||||||
## GSettings
|
|
||||||
|
|
||||||
`GSettings` is an **enum** of the available per-guild configuration keys and their defaults:
|
|
||||||
|
|
||||||
| Setting | Default |
|
|
||||||
|-------------------------|---------|
|
|
||||||
| `MUSIC_ROLE` | (none) |
|
|
||||||
| `MUSIC_CHANNEL` | (none) |
|
|
||||||
| `MUSIC_VOLUME` | `30` |
|
|
||||||
| `MUSIC_MAXPLAYLISTSIZE` | `20` |
|
|
||||||
|
|
||||||
`GuildSettingService` looks up an `OGuildSetting` row by the enum `name()`; if no row exists for a
|
|
||||||
guild/setting, the enum's `defaultValue` is used. This is how `MusicCommand` obtains the guild's music
|
|
||||||
role/channel and how `ConfigureCommand` persists changes.
|
|
||||||
|
|
||||||
## Schema (`V1__initial_schema.sql`)
|
|
||||||
|
|
||||||
The initial migration creates the tables backing the entities above (`guilds`, `guild_settings`,
|
|
||||||
`blacklist`, `logging`, `log_message`, `tasks`, `web_users`), with `guild_id` foreign keys and
|
|
||||||
`ON DELETE CASCADE` from the guild-scoped tables to `guilds`. Subsequent schema changes should be
|
|
||||||
added as new `V3__…`, `V4__…` Flyway scripts in the same directory.
|
|
||||||
|
|
||||||
`V2__task_type_id.sql` rewrites `tasks.task_class` from the old fully-qualified class names to the
|
|
||||||
stable logical `typeId` (e.g. `emoji-reaction-role`), so previously persisted rows remain loadable.
|
|
||||||
-290
@@ -1,290 +0,0 @@
|
|||||||
# Tasks
|
|
||||||
|
|
||||||
**Packages:** `me.bobert.netneutrality.framework.task` (framework),
|
|
||||||
`me.bobert.netneutrality.feature.*.task` (concrete tasks)
|
|
||||||
|
|
||||||
Tasks are one of the more advanced concepts in the bot. A **Task** is a unit of logic that is looked
|
|
||||||
up and executed by **id** when a matching Discord event arrives, as opposed to a
|
|
||||||
[command](commands.md) which is looked up by name.
|
|
||||||
|
|
||||||
The original motivation was **Discord interactions** (buttons). When a user clicks a button, Discord
|
|
||||||
sends a `ButtonInteractionEvent` whose id is `getComponentId()`. A `Task` with that same id was
|
|
||||||
inserted earlier (typically by the command that created the button), so the `TaskManager` can find
|
|
||||||
the task and execute it. The system has since grown to cover several task subtypes:
|
|
||||||
|
|
||||||
| Subtype | Trigger | Id source |
|
|
||||||
|----------------------------------------------|------------------------------------|----------------------------------------|
|
|
||||||
| [Button task](#button-emoji-and-modal-tasks) | `ButtonInteractionEvent` | button `componentId` |
|
|
||||||
| [Modal task](#button-emoji-and-modal-tasks) | `ModalInteractionEvent` | modal `customId` |
|
|
||||||
| [Emoji task](#button-emoji-and-modal-tasks) | `MessageReactionAddEvent` | composite `guildId:messageId` |
|
|
||||||
| [Step task](#33-step-tasks) | replied-to message | last-message id (assigned dynamically) |
|
|
||||||
| [Built-in task](#32-built-in-tasks) | button with a hardcoded id | hardcoded id |
|
|
||||||
| [Persistable task](#31-persistable-tasks) | any of the above, restored from DB | as stored |
|
|
||||||
| [Scheduled task](#34-scheduled-tasks) | timer | n/a (time-based, not id-based) |
|
|
||||||
|
|
||||||
## The `Task` id
|
|
||||||
|
|
||||||
The id is the **primary lookup key** used by `TaskManager`. How it is derived depends on the subtype:
|
|
||||||
|
|
||||||
- **Buttons** → the button's `componentId`.
|
|
||||||
- **Modals** → the modal's `customId`.
|
|
||||||
- **Emoji reactions** → a composite `guildId:messageId` (a reaction is tied to a specific message).
|
|
||||||
- **Step tasks** → start life with a random UUID, then re-key themselves to the DM/last-message id
|
|
||||||
once that message exists (see [step tasks](#33-step-tasks)).
|
|
||||||
|
|
||||||
## `Task` contract & lifecycle
|
|
||||||
|
|
||||||
`Task` (interface) defines `execute(TaskContext)` and returns a **`Lifecycle`** value that tells the
|
|
||||||
`TaskManager` what to do with the task afterwards:
|
|
||||||
|
|
||||||
| Lifecycle | Meaning |
|
|
||||||
|----------------------------|-------------------------------------------------------------------|
|
|
||||||
| `BUILTIN` | Task is static and stays registered indefinitely. |
|
|
||||||
| `KEEP` | Keep the task in memory after execution (can run again). |
|
|
||||||
| `REMOVE_SELF` | Remove this task after execution (one-shot). |
|
|
||||||
| `REMOVE_SELF_AND_CHILDREN` | Remove this task and all of its [child tasks](#trait-interfaces). |
|
|
||||||
|
|
||||||
### Task base classes (`core.tasks.impl`)
|
|
||||||
|
|
||||||
```
|
|
||||||
AbstractTask (implements Task)
|
|
||||||
├── ButtonTask ── executed from a ButtonTaskContext
|
|
||||||
├── EmojiTask ── executed from an EmojiTaskContext
|
|
||||||
├── ModalTask ── executed from a ModalTaskContext
|
|
||||||
├── GenericCustomIdTask ── task keyed by an explicit custom id
|
|
||||||
├── GenericRandomIdTask ── task keyed by a generated random id
|
|
||||||
├── AbstractBuiltInTask ── base for hardcoded, reflectively-loaded tasks
|
|
||||||
├── AbstractPersistableTask ── base for DB-backed tasks (implements PersistableTask)
|
|
||||||
└── AbstractStepTask ── base for multi-step Q&A flows (implements StepTask)
|
|
||||||
```
|
|
||||||
|
|
||||||
## `TaskManager`
|
|
||||||
|
|
||||||
`me.bobert.netneutrality.framework.task.TaskManager` is the registry, lookup, and execution
|
|
||||||
coordinator.
|
|
||||||
|
|
||||||
### Registration — `insert`
|
|
||||||
|
|
||||||
- Adds the task to the active map keyed by its id.
|
|
||||||
- If the task is a [`PersistableTask`](#31-persistable-tasks), it is also **saved to the database**
|
|
||||||
via [`TaskService`](persistence.md#service-layer).
|
|
||||||
- If the task is a [`StepTask`](#33-step-tasks) without a message id yet, it is held in a separate
|
|
||||||
`pendingTasks` collection until its id is assigned.
|
|
||||||
|
|
||||||
### Execution — `handle(TaskContext)`
|
|
||||||
|
|
||||||
1. Look up the task by `context.getId()`.
|
|
||||||
2. For ordinary (non-built-in, non-persistable) tasks, acquire a **`ReentrantLock`** so the same task
|
|
||||||
cannot be executed concurrently (prevents double-clicks / races). Tasks that share a
|
|
||||||
[parent](#trait-interfaces) share the same lock id.
|
|
||||||
3. Call `task.execute(context)`.
|
|
||||||
4. Apply the returned `Lifecycle` (keep / remove / remove-with-children); persistable tasks removed
|
|
||||||
here are also deleted from the DB.
|
|
||||||
5. If the task is [`Deletable`](#trait-interfaces), delete the original message/interaction.
|
|
||||||
6. Unless the task is [`NotAcknowledgeable`](#trait-interfaces), acknowledge the interaction.
|
|
||||||
|
|
||||||
### Startup — `init`
|
|
||||||
|
|
||||||
Called from `App.startup()`:
|
|
||||||
|
|
||||||
1. Reflectively load all [`BuiltInTask`](#32-built-in-tasks) implementations.
|
|
||||||
2. Reflectively load all [`ScheduledTask`](#34-scheduled-tasks) implementations and register them
|
|
||||||
with [`ScheduledTaskManager`](threading.md).
|
|
||||||
3. Load persistable tasks from the database (via `TaskService`) and reconstruct them (see
|
|
||||||
[persistence rules](#31-persistable-tasks)).
|
|
||||||
|
|
||||||
## Button, emoji and modal tasks
|
|
||||||
|
|
||||||
These are the "interaction" tasks and the original purpose of the subsystem:
|
|
||||||
|
|
||||||
- **`ButtonTask`** — executed when a button whose `componentId` matches the task id is clicked. The
|
|
||||||
matching context is a [`ButtonTaskContext`](contexts.md#task-contexts).
|
|
||||||
- **`ModalTask`** — executed when a modal is submitted; matched by the modal `customId`. A modal is
|
|
||||||
typically opened by another interaction (e.g. `ReportIssueTask` opens a title/description modal).
|
|
||||||
- **`EmojiTask`** — executed when a reaction is added to a specific message; matched by the composite
|
|
||||||
`guildId:messageId` id.
|
|
||||||
|
|
||||||
The generic helpers `GenericCustomIdTask` / `GenericRandomIdTask` let a command install an ad-hoc,
|
|
||||||
function-backed task under a chosen or generated id (e.g. the generic `AcceptButton` / `CancelButton`
|
|
||||||
under `tasks/generic`).
|
|
||||||
|
|
||||||
## 3.1 Persistable tasks
|
|
||||||
|
|
||||||
**Classes:** `PersistableTask` (interface), `AbstractPersistableTask`,
|
|
||||||
`core.tasks.persistence.PersistableTaskRegistry`; entity
|
|
||||||
[`OTask`](persistence.md#entity-catalog).
|
|
||||||
|
|
||||||
Persistable tasks survive restarts: they are serialized to the database and rebuilt on startup.
|
|
||||||
There is **no mapping file** — task types are discovered on the classpath and each task knows how to
|
|
||||||
rebuild itself.
|
|
||||||
|
|
||||||
### The contract
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface PersistableTask extends Task {
|
|
||||||
default String typeId() { return getName(); } // stable logical id stored in the DB
|
|
||||||
void restore(JSONObject payload); // rebuild state from a previous toPayload()
|
|
||||||
void insert(CommandContext context);
|
|
||||||
void save();
|
|
||||||
void delete();
|
|
||||||
String toPayload();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`typeId()`** is the value written to `tasks.task_class`. It defaults to `getName()`, so a package
|
|
||||||
move or class rename never breaks restoring existing rows.
|
|
||||||
- **`restore(JSONObject)`** is the counterpart of `toPayload()`. `AbstractPersistableTask` provides a
|
|
||||||
no-op default, so only tasks that carry extra state override it.
|
|
||||||
- **`toPayload()`** serializes the bean getters (`new JSONObject(this)`) and always includes `id` and
|
|
||||||
`lockId`, which `TaskService.validateTask` verifies against the stored row.
|
|
||||||
|
|
||||||
### `PersistableTaskRegistry` — discovery and restore
|
|
||||||
|
|
||||||
`PersistableTaskRegistry.scan()` runs once during `TaskManagerImpl.init()` and uses the same
|
|
||||||
`ReflectionHelper.loadTasks(..., ApplicationContext, ...)` discovery used for
|
|
||||||
[built-in](#32-built-in-tasks) and [scheduled](#34-scheduled-tasks) tasks: every concrete
|
|
||||||
`PersistableTask` under `me.bobert.netneutrality` is instantiated once and indexed by its `typeId()`.
|
|
||||||
Two types declaring the same id fail fast at startup with both class names in the message.
|
|
||||||
|
|
||||||
`create(typeId, payload)` then produces a **fresh** instance per row:
|
|
||||||
|
|
||||||
```java
|
|
||||||
PersistableTask task = ReflectionHelper.instantiate(type, context);
|
|
||||||
task.restore(payload);
|
|
||||||
```
|
|
||||||
|
|
||||||
Because instantiation goes through the DI context, a restored task needs a constructor whose
|
|
||||||
parameters are all resolvable from the `ApplicationContext` — in practice `(TaskManager)`. Fields
|
|
||||||
that only exist in the payload (ids, mappings) are assigned by `restore()` before the instance is
|
|
||||||
handed to the `TaskManager`.
|
|
||||||
|
|
||||||
> **Adding a persistable task:** write **one class** extending `AbstractPersistableTask` (or
|
|
||||||
> `EmojiTask`), give it a `(TaskManager)` constructor for the registry plus whatever constructors the
|
|
||||||
> feature code needs, and override `restore()` if it holds state beyond the bean getters. Nothing
|
|
||||||
> else has to be registered — the scan picks it up and `getName()` becomes its `typeId()`.
|
|
||||||
|
|
||||||
### Persistence round-trip
|
|
||||||
|
|
||||||
```
|
|
||||||
insert(task) ──▶ TaskService.persistTask ──▶ OTask row (task_id, task_class = typeId, payload JSON)
|
|
||||||
startup ──▶ PersistableTaskRegistry.scan() (typeId ──▶ class)
|
|
||||||
──▶ TaskService reads OTask rows ──▶ registry.create(task_class, payload)
|
|
||||||
──▶ DI instantiate ──▶ task.restore(payload)
|
|
||||||
```
|
|
||||||
|
|
||||||
Rows whose `task_class` is not a registered `typeId` are logged and skipped; the remaining rows still
|
|
||||||
load.
|
|
||||||
|
|
||||||
## 3.2 Built-in tasks
|
|
||||||
|
|
||||||
**Classes:** `BuiltInTask`, `AbstractBuiltInTask`.
|
|
||||||
|
|
||||||
Built-in tasks are hardcoded tasks with hardcoded ids, conceptually similar to commands. They are
|
|
||||||
loaded reflectively at startup (in `TaskManager.init()`) and stay registered for the lifetime of the
|
|
||||||
bot (`Lifecycle.BUILTIN`).
|
|
||||||
|
|
||||||
A button that triggers a built-in task is created from its class:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Button button = BuiltInTask.createButton(SomeBuiltInTask.class);
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces a JDA `Button` whose component id equals the built-in task's hardcoded id, so clicking
|
|
||||||
it routes straight to that task. Examples: the help buttons `BotInfo`, `CommandHelp`, `ReportBug`
|
|
||||||
(`tasks/help`).
|
|
||||||
|
|
||||||
## 3.3 Step tasks
|
|
||||||
|
|
||||||
**Classes:** `StepTask` (interface), `AbstractStepTask`.
|
|
||||||
|
|
||||||
Step tasks ask the user a series of questions and run logic based on each answer. Because Discord has
|
|
||||||
no native "conversation" primitive, the abstraction tracks the **last message id** and only runs the
|
|
||||||
next step when *that* message is replied to.
|
|
||||||
|
|
||||||
Flow:
|
|
||||||
|
|
||||||
1. A step task is created (e.g. by a setup command) and sends a prompt message; initially it is keyed
|
|
||||||
by a random UUID and held in `pendingTasks`.
|
|
||||||
2. Once the prompt message exists, the task **re-keys itself to that message's id** and moves into the
|
|
||||||
active `tasks` map.
|
|
||||||
3. When the user replies to that message, the incoming `MessageReceivedEvent` becomes a
|
|
||||||
[`MessageTaskContext`](contexts.md#task-contexts) whose id is the replied-to message id — so
|
|
||||||
`TaskManager` finds the step task.
|
|
||||||
4. `executeStep` validates/processes the answer and returns a `StepResult`:
|
|
||||||
success, failure, or **next step** (which installs the following step task).
|
|
||||||
|
|
||||||
`AbstractStepTask` provides answer validation keyed to a step "type" (e.g. a role id, an emoji, …).
|
|
||||||
|
|
||||||
## 3.4 Scheduled tasks
|
|
||||||
|
|
||||||
**Classes:** `ScheduledTask`, coordinated by
|
|
||||||
[`ScheduledTaskManager`](threading.md#scheduledtaskmanager).
|
|
||||||
|
|
||||||
Scheduled tasks are time-based background jobs (as opposed to id-based interaction tasks). They are
|
|
||||||
loaded reflectively at startup and registered with `ScheduledTaskManager`, which schedules them at a
|
|
||||||
fixed interval derived from the task's `TimeFrame` (e.g. minutely / hourly / daily / custom) and
|
|
||||||
cancels them all on shutdown.
|
|
||||||
|
|
||||||
> **Status:** the scheduled-task abstraction is intended to eventually replace the ad-hoc
|
|
||||||
> [`ThreadManager`](threading.md) threads and is still evolving. The concrete scheduled tasks that
|
|
||||||
> exist today are listed in the [catalog](#concrete-task-catalog).
|
|
||||||
|
|
||||||
## Trait interfaces
|
|
||||||
|
|
||||||
Tasks can opt into extra behavior by implementing marker/behavior interfaces in
|
|
||||||
`core.tasks.trait`:
|
|
||||||
|
|
||||||
| Trait | Effect |
|
|
||||||
|----------------------|------------------------------------------------------------------------------------------|
|
|
||||||
| `Deletable` | After successful execution, `TaskManager` deletes the original message/interaction. |
|
|
||||||
| `NotAcknowledgeable` | Suppresses the default acknowledgement (the task sends its own response). |
|
|
||||||
| `WithButton` | The task can render itself as a JDA `Button` (`createButton()`). |
|
|
||||||
| `WithChildren` | The task tracks child tasks and can clean them up together (`REMOVE_SELF_AND_CHILDREN`). |
|
|
||||||
| `WithParent` | Links a child task to a parent; children share the parent's execution lock. |
|
|
||||||
|
|
||||||
## Concrete task catalog
|
|
||||||
|
|
||||||
| Task | Package | Base type | Description |
|
|
||||||
|-----------------------------|-------------|------------------------------|--------------------------------------------------------------|
|
|
||||||
| `EmojiReactionRoleTask` | `emoji` | `EmojiTask` (persistable) | Assign/remove a role when a user reacts with a mapped emoji. |
|
|
||||||
| `AcceptButton` | `generic` | `ButtonTask` | Generic "accept" button running a supplied function. |
|
|
||||||
| `CancelButton` | `generic` | `ButtonTask` | Generic "cancel" button running a supplied function. |
|
|
||||||
| `ReportIssueTask` | `git` | `ModalTask` | Modal to report a bug; creates a Gitea issue. |
|
|
||||||
| `BotInfo` | `help` | `AbstractBuiltInTask` | Show bot info + invite link. |
|
|
||||||
| `CommandHelp` | `help` | `AbstractBuiltInTask` | Show command help + the bot prefix. |
|
|
||||||
| `ReportBug` | `help` | `AbstractBuiltInTask` | Start a bug-report flow / provide the report link. |
|
|
||||||
| `LoadPlaylistTask` | `music` | `ButtonTask` (with children) | Present playlist/search results as choice buttons. |
|
|
||||||
| `LoadTrackFromPlaylistTask` | `music` | `ButtonTask` | Load one specific track from the results. |
|
|
||||||
| `EmojiRoleFirstStep` | `reaction` | `AbstractStepTask` | Ask for the target role id. |
|
|
||||||
| `EmojiRoleSecondStep` | `reaction` | `AbstractStepTask` | Ask for the emoji to map. |
|
|
||||||
| `EmojiRoleModify` | `reaction` | `AbstractStepTask` | Modify an existing emoji-role mapping. |
|
|
||||||
| `EmojiRoleRemoveStep` | `reaction` | `AbstractStepTask` | Remove an emoji-role mapping. |
|
|
||||||
| `MusicManagerCleanup` | `scheduled` | `ScheduledTask` | Evict unused per-guild music managers. |
|
|
||||||
| `MusicPlayerAutoDisconnect` | `scheduled` | `ScheduledTask` | Disconnect from voice when idle. |
|
|
||||||
| `TaskCleanup` | `scheduled` | `ScheduledTask` | Periodically remove expired non-persistable tasks. |
|
|
||||||
|
|
||||||
### Worked example — emoji-role setup (step → persistable)
|
|
||||||
|
|
||||||
1. `EmojiRoleFirstStep` sends a prompt: "reply with the role id"; `AbstractStepTask` validates it is a
|
|
||||||
real role in the guild.
|
|
||||||
2. `executeStep` returns `StepResult.next(new EmojiRoleSecondStep(...))`, carrying the chosen role.
|
|
||||||
3. `EmojiRoleSecondStep` asks for the emoji; on a valid answer it creates an
|
|
||||||
`EmojiReactionRoleTask`.
|
|
||||||
4. That `EmojiReactionRoleTask` is `insert`ed into `TaskManager` — because it is a
|
|
||||||
[persistable task](#31-persistable-tasks) it is written to the `tasks` table and will be restored
|
|
||||||
on the next startup via the
|
|
||||||
[`PersistableTaskRegistry`](#persistabletaskregistry--discovery-and-restore).
|
|
||||||
|
|
||||||
### Worked example — Gitea bug report (modal)
|
|
||||||
|
|
||||||
1. `ReportIssueTask.replyToInteraction` opens a modal with **title** and **description** fields.
|
|
||||||
2. On submit, `ModalTaskContext` routes back to the task; `execute` reads the field values.
|
|
||||||
3. The task calls the Gitea API via `GiteaClient` (using `GITEA_HOST` / `GITEA_TOKEN` /`r`n `GITEA_OWNER` / `GITEA_REPO` from configuration) and creates an issue, then returns `Lifecycle.REMOVE_SELF`.
|
|
||||||
|
|
||||||
### Worked example — music results (parent/children)
|
|
||||||
|
|
||||||
1. `LoadPlaylistTask` posts a message with a button per result (children:
|
|
||||||
`LoadTrackFromPlaylistTask` / `AcceptButton`).
|
|
||||||
2. All children share the parent's lock id ([`WithParent`](#trait-interfaces)), so simultaneous
|
|
||||||
clicks are serialized.
|
|
||||||
3. Selecting a result can return `REMOVE_SELF_AND_CHILDREN` to tear down the whole interaction.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Threading & Scheduling
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality.platform.concurrency`
|
|
||||||
|
|
||||||
This subsystem centralizes asynchronous execution and recurring jobs.
|
|
||||||
|
|
||||||
## Classes
|
|
||||||
|
|
||||||
| Class | Responsibility |
|
|
||||||
|---|---|
|
|
||||||
| `ThreadManager` | Static registry of `ExecutorService` instances shared across the bot. |
|
|
||||||
| `BaseThreadFactory` | Custom `ThreadFactory` giving threads meaningful names and an uncaught-exception handler. |
|
|
||||||
| `ScheduledTaskManager` | Schedules and cancels recurring [scheduled tasks](tasks.md#34-scheduled-tasks). |
|
|
||||||
|
|
||||||
## `ThreadManager`
|
|
||||||
|
|
||||||
- `getScheduledExecutor()` — a `ScheduledThreadPool` sized to the number of CPU cores.
|
|
||||||
- `getExecutor(name)` — returns (and caches) a named thread pool for ad-hoc async work.
|
|
||||||
- `shutdownExecutor()` — shuts the executors down cleanly; called from
|
|
||||||
[`App.shutdown()`](architecture.md#shutdown-lifecycle).
|
|
||||||
|
|
||||||
## `BaseThreadFactory`
|
|
||||||
|
|
||||||
Names created threads (e.g. `web-api-1`) so they are identifiable in logs and stack dumps, and installs
|
|
||||||
an [`ExecutorException`](utilities-and-bootstrap.md#exceptions) as the thread's
|
|
||||||
`UncaughtExceptionHandler` so crashes on worker threads are still routed through
|
|
||||||
[`LogHandler`](utilities-and-bootstrap.md#loghandler). It is used both by `ThreadManager` and by the
|
|
||||||
[Web API](web-api.md) server pool.
|
|
||||||
|
|
||||||
## `ScheduledTaskManager`
|
|
||||||
|
|
||||||
Coordinates [`ScheduledTask`](tasks.md#34-scheduled-tasks) instances (discovered reflectively at
|
|
||||||
startup by [`TaskManager.init()`](tasks.md#taskmanager)):
|
|
||||||
|
|
||||||
- `schedule(ScheduledTask task)` — schedules the task at a fixed delay/interval derived from its
|
|
||||||
`TimeFrame` (minutely / hourly / daily / custom), using the shared scheduled executor.
|
|
||||||
- `cancelAll()` — cancels every scheduled task; called first in
|
|
||||||
[`App.shutdown()`](architecture.md#shutdown-lifecycle).
|
|
||||||
|
|
||||||
The existing scheduled tasks are `MusicManagerCleanup`, `MusicPlayerAutoDisconnect` (which reads the
|
|
||||||
bot's gateway voice state rather than JDA's audio manager, see [music.md](music.md)), and
|
|
||||||
`TaskCleanup` (see the [task catalog](tasks.md#concrete-task-catalog)).
|
|
||||||
|
|
||||||
> **Direction of travel:** the scheduled-task abstraction is intended to eventually replace ad-hoc
|
|
||||||
> threads created via `ThreadManager`, giving a single, uniform way to express recurring background
|
|
||||||
> work. It is still evolving (see [tasks.md §3.4](tasks.md#34-scheduled-tasks)).
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
# Utilities, Bootstrap, Logging & Exceptions
|
|
||||||
|
|
||||||
This document covers the cross-cutting glue of the bot: the entry points, configuration, logging /
|
|
||||||
error tracking, the shared utility helpers, and the custom exception hierarchy.
|
|
||||||
|
|
||||||
## Bootstrap
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality` / `me.bobert.netneutrality.bootstrap`
|
|
||||||
|
|
||||||
| Class | Role |
|
|
||||||
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `App` | The application: holds the process instance and the `JDA` handle, builds the `ApplicationContext`, runs `context.start()` / `context.stop()`, and is the JAR main class (`main()`). See [architecture.md](architecture.md). |
|
|
||||||
| `bootstrap.ApplicationContext` | Hand-rolled container: `register(T)`, `get(Class<T>)`, ordered `start()` / `stop()` for `Lifecycle` components. |
|
|
||||||
| `bootstrap.Lifecycle` | Optional component contract: `start() throws Exception`, default `stop()`, default `priority()`. |
|
|
||||||
| `core.BotState` | Work-in-progress global state helper: exposes a `SAFE` flag and `dump()`, which serializes per-guild/music state to `state.json`. Bound to `GuildServiceProvider` / `MusicService` during bootstrap. |
|
|
||||||
|
|
||||||
The full startup and shutdown sequences are documented in
|
|
||||||
[architecture.md](architecture.md#startup-lifecycle).
|
|
||||||
|
|
||||||
### `ApplicationContext` + `Lifecycle`
|
|
||||||
|
|
||||||
Domain services are constructed once in `App.buildContext()`, registered with the context,
|
|
||||||
and resolved later by type:
|
|
||||||
|
|
||||||
- `register(component)` — indexes the concrete class; if the component implements `Lifecycle` it
|
|
||||||
participates in ordered start/stop.
|
|
||||||
- `get(Class<T>)` — returns the first registered instance assignable to `T` (interfaces work).
|
|
||||||
- `start()` — sorts `Lifecycle` components by ascending `priority()` and starts them.
|
|
||||||
- `stop()` — stops them in the exact reverse order; individual failures are logged and swallowed so
|
|
||||||
remaining components still tear down.
|
|
||||||
|
|
||||||
Constructor injection for reflectively loaded commands/tasks is handled by
|
|
||||||
[`ReflectionHelper`](#reflectionhelper): the richest public constructor whose parameter types all
|
|
||||||
resolve via `context.get(type)` is preferred; otherwise the no-arg constructor is used.
|
|
||||||
|
|
||||||
**Interfaces used for injection** (implemented by the concrete managers registered above):
|
|
||||||
|
|
||||||
| Interface | Typical implementor |
|
|
||||||
|-----------------------------------------|---------------------------------------|
|
|
||||||
| `platform.config.SettingsProvider` | `utils.Settings` |
|
|
||||||
| `platform.persistence.SessionProvider` | `core.db.SessionManager` |
|
|
||||||
| `platform.concurrency.ExecutorProvider` | `core.thread.ThreadManager` |
|
|
||||||
| `framework.command.CommandRegistry` | `core.command.CommandManager` |
|
|
||||||
| `framework.task.TaskManager` | `core.tasks.TaskManagerImpl` |
|
|
||||||
| `feature.music.MusicService` | `music.MusicServiceImpl` |
|
|
||||||
| `data.service.GuildServiceProvider` | `db.service.GuildServiceProviderImpl` |
|
|
||||||
|
|
||||||
There are **no** `getInstance()` / `instance()` shims on the managers above. The only intentional
|
|
||||||
static access points left are:
|
|
||||||
|
|
||||||
- `Settings.getInstance()` — early bootstrap only (`main()`, Sentry, token validation) before the
|
|
||||||
context exists; the same instance is registered as `SettingsProvider` for everything else.
|
|
||||||
- `App.getInstance()` — application root used by a couple of shutdown entry points
|
|
||||||
(`ShutdownCommand`). Prefer constructor injection of the needed collaborators where possible.
|
|
||||||
- `JpaUtils.bind(SessionProvider)` / `LogHandler.bind(GuildServiceProvider)` /
|
|
||||||
`ChannelMessagePurger.bind(ExecutorProvider)` — single well-defined static bind points for deep
|
|
||||||
utility graphs that are not constructor-injected (repositories, static log helpers).
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### `Settings` / `SettingsProvider`
|
|
||||||
|
|
||||||
**Class:** `me.bobert.netneutrality.platform.config.Settings` implements
|
|
||||||
`me.bobert.netneutrality.platform.config.SettingsProvider`.
|
|
||||||
|
|
||||||
`Settings` is the single source of truth for configuration. It loads values from the `bot.properties`
|
|
||||||
file and overlays **environment variables** on top (environment variables win).
|
|
||||||
|
|
||||||
- **Early bootstrap:** `Settings.getInstance()` (process-wide, created once before the context).
|
|
||||||
- **Runtime injection:** depend on `SettingsProvider` and let `ApplicationContext` supply the same
|
|
||||||
instance. Prefer this path in commands, tasks, handlers, and services.
|
|
||||||
- `Settings.getVersion()` / `Settings.getDiscordInvite()` — thin static helpers used by the banner /
|
|
||||||
invite strings; they delegate to the bootstrap instance.
|
|
||||||
|
|
||||||
An example configuration file is `bot.properties.example`. Key configuration values:
|
|
||||||
|
|
||||||
| Key | Purpose |
|
|
||||||
|------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
|
|
||||||
| `NETNEUTRALITY_BOT_ENV` | `dev` / `prod`; controls debug logging and the Sentry environment. |
|
|
||||||
| `NETNEUTRALITY_BOT_TOKEN` | Discord bot token (**required**). |
|
|
||||||
| `NETNEUTRALITY_BOT_PREFIX` | Message-command prefix (default `$`). |
|
|
||||||
| `NETNEUTRALITY_BOT_OWNER` | Bot owner user id (used for owner-only commands / permissions). |
|
|
||||||
| `NETNEUTRALITY_BOT_INVITE` | Invite link surfaced by help/info. |
|
|
||||||
| `NETNEUTRALITY_API_PORT` | Port for the [Web API](web-api.md) (default `9000`). |
|
|
||||||
| `MYSQL_*` | Database connection settings (see [persistence.md](persistence.md)). |
|
|
||||||
| `NETNEUTRALITY_SENTRY_DSN` | Enables Sentry when present. |
|
|
||||||
| `GITEA_HOST` / `GITEA_TOKEN` / `GITEA_OWNER` / `GITEA_REPO` | Gitea issue reporting ([`ReportIssueTask`](tasks.md#worked-example--gitea-bug-report-modal)). Token needs `write:issue`. |
|
|
||||||
| `NETNEUTRALITY_LAVALINK_URI` / `_PASSWORD` / `_NAME` | Connection settings for the [Lavalink node](music.md#settings). |
|
|
||||||
| `NETNEUTRALITY_PO_TOKEN` / `NETNEUTRALITY_VISITOR_DATA` / `NETNEUTRALITY_CIPHER_*` | YouTube auth/cipher settings, consumed by the node via `lavalink/application.yml`. |
|
|
||||||
|
|
||||||
## Utility helpers
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality.shared.util` (helpers) / `me.bobert.netneutrality.bootstrap` / `me.bobert.netneutrality.platform.*`
|
|
||||||
|
|
||||||
| Class | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `Settings` | Configuration (see above). |
|
|
||||||
| `ReflectionHelper` | Classpath scanning / reflective instantiation (see below). |
|
|
||||||
| `ContextUtils` | Boilerplate helpers for building/handling [contexts](contexts.md). |
|
|
||||||
| `GuildUtils` | Guild-related helpers (lookups, permission checks). |
|
|
||||||
| `JpaUtils` | Session/transaction helpers for the [persistence](persistence.md) layer. Bound once to a `SessionProvider` during bootstrap (`JpaUtils.bind`). |
|
|
||||||
| `MusicUtils` | Voice-channel connectivity and DJ/permission checks for [music](music.md). Callers pass the guild `MusicManager` (no static music lookup). |
|
|
||||||
| `MiscUtils` | Assorted small helpers. |
|
|
||||||
| `ChannelMessagePurger` | Bulk message deletion (used by the purge command). Bound once to an `ExecutorProvider` during bootstrap. |
|
|
||||||
| `MessageFunction` | Standardized message/embed formatting. Help text builders take a `CommandRegistry` rather than a static command manager. |
|
|
||||||
| `UserFunction` | Standardized user/string formatting. |
|
|
||||||
|
|
||||||
### `ReflectionHelper`
|
|
||||||
|
|
||||||
Built on the `org.reflections` library, `ReflectionHelper` discovers and instantiates implementation
|
|
||||||
classes in specific packages. It is the mechanism behind the bot's "drop a class in the right package
|
|
||||||
and it's picked up" model, used to auto-load:
|
|
||||||
|
|
||||||
- [`Command`](commands.md) implementations (`CommandManager` / `CommandRegistry.start()`),
|
|
||||||
- [`BuiltInTask`](tasks.md#32-built-in-tasks) and [`ScheduledTask`](tasks.md#34-scheduled-tasks)
|
|
||||||
implementations (`TaskManager.start()`).
|
|
||||||
|
|
||||||
When given an `ApplicationContext`, `newInstance(Class, ApplicationContext)` selects the public
|
|
||||||
constructor with the most parameters that can all be resolved via `context.get(parameterType)`. If
|
|
||||||
no such constructor exists it falls back to the no-arg constructor. That is how music/admin/react
|
|
||||||
commands receive `MusicService`, `GuildServiceProvider`, `TaskManager`, etc. without static
|
|
||||||
singletons.
|
|
||||||
|
|
||||||
## Logging & error tracking
|
|
||||||
|
|
||||||
### `LogHandler`
|
|
||||||
|
|
||||||
**Class:** `me.bobert.netneutrality.platform.logging.LogHandler`.
|
|
||||||
|
|
||||||
`LogHandler` is the unified logging/error entry point. It routes messages and exceptions to:
|
|
||||||
|
|
||||||
1. **Log4j2** (configured by `log4j2.properties`; SLF4J is bridged via `log4j-slf4j2-impl`),
|
|
||||||
2. **Sentry** (when `NETNEUTRALITY_SENTRY_DSN` is configured), and
|
|
||||||
3. optionally a per-guild **Discord debug channel** (e.g. `sendMessageToDebugChannel(guild, msg)`),
|
|
||||||
gated by [`GuildService.isDebugEnabled()`](persistence.md#service-layer) /
|
|
||||||
[`OLogging`](persistence.md#entity-catalog) configuration.
|
|
||||||
|
|
||||||
The Sentry **user** context is attached automatically when a
|
|
||||||
[`AbstractContext`](contexts.md#abstractcontext-abstract-class) is created, so captured errors are
|
|
||||||
attributed to the Discord user that triggered them.
|
|
||||||
|
|
||||||
## Exception hierarchy
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality.exceptions`
|
|
||||||
|
|
||||||
```
|
|
||||||
RuntimeException
|
|
||||||
└── ContextException ── carries a Context (guild/channel/user)
|
|
||||||
├── CommandExecutionException ── thrown during command processing
|
|
||||||
└── TaskExecutionException ── thrown during task execution
|
|
||||||
|
|
||||||
GracefulContextError ── user-facing, non-logged failure → ephemeral reply
|
|
||||||
ExecutorException ── Thread.UncaughtExceptionHandler → routes crashes to LogHandler
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`ContextException`** — base runtime exception that carries the [`Context`](contexts.md) in which
|
|
||||||
it occurred, so downstream handling can respond in the right channel and attribute the error.
|
|
||||||
- **`CommandExecutionException`** / **`TaskExecutionException`** — specializations thrown by the
|
|
||||||
[command](commands.md) and [task](tasks.md) flows respectively.
|
|
||||||
- **`GracefulContextError`** — signals an expected, user-facing problem (e.g. bad input); when caught
|
|
||||||
it produces an ephemeral message to the user instead of a full error log/Sentry event.
|
|
||||||
- **`ExecutorException`** — an `UncaughtExceptionHandler` installed by
|
|
||||||
[`BaseThreadFactory`](threading.md#basethreadfactory) so exceptions escaping worker threads are still
|
|
||||||
captured by [`LogHandler`](#loghandler).
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# HTTP Web API
|
|
||||||
|
|
||||||
**Package:** `me.bobert.netneutrality.platform.web` (client module) and
|
|
||||||
`me.bobert.netneutrality.platform.web.server` (server).
|
|
||||||
**Server:** JDK built-in `com.sun.net.httpserver.HttpServer` (no external web framework).
|
|
||||||
|
|
||||||
The bot exposes a small internal HTTP API for health monitoring, control, and guild introspection —
|
|
||||||
intended for an external dashboard/companion, not for end users in Discord.
|
|
||||||
|
|
||||||
## Server startup
|
|
||||||
|
|
||||||
`WebApi` is a [`Lifecycle`](utilities-and-bootstrap.md#applicationcontext--lifecycle) component
|
|
||||||
(priority 70) registered in [`App.buildContext()`](architecture.md#startup-lifecycle).
|
|
||||||
It receives `SettingsProvider`, `GuildServiceProvider`, and suppliers for the live `JDA` /
|
|
||||||
shutdown callback via its constructor. On `start()` it:
|
|
||||||
|
|
||||||
- reads the port from `NETNEUTRALITY_API_PORT` (default `9000`),
|
|
||||||
- creates an `HttpServer` bound to that port,
|
|
||||||
- installs an `Executor` backed by a fixed thread pool of 10 threads named via
|
|
||||||
[`BaseThreadFactory`](threading.md),
|
|
||||||
- registers the handler contexts (below), and
|
|
||||||
- starts the server.
|
|
||||||
|
|
||||||
On `stop()` the server and its executor are shut down gracefully.
|
|
||||||
|
|
||||||
## Handler model
|
|
||||||
|
|
||||||
`AbstractHandler` (implements `HttpHandler`) is the base for all handlers and provides helpers such as
|
|
||||||
`sendJsonResponse(...)` and `sendStringResponse(...)`. Concrete handlers extend it and are mounted at
|
|
||||||
a path prefix.
|
|
||||||
|
|
||||||
| Handler | Base path | Endpoints |
|
|
||||||
|---|---|---|
|
|
||||||
| `RootHandler` | `/` | Simple HTML "OK" landing response. |
|
|
||||||
| `GuildHandler` | `/guilds` | `/guilds/owner` — guild ids + owner ids (JSON); `/guilds/channels/{guildId}` — text channels (JSON); `/guilds/roles/{guildId}` — roles (JSON). |
|
|
||||||
| `ActionHandler` | `/actions` | `/actions/status` — JDA health + gateway ping; `/actions/shutdown` — graceful shutdown. |
|
|
||||||
| `SettingsHandler` | `/settings` | `/settings/catalog` — every known setting with its default value and description; `/settings/current/{guildId}` — current guild settings; `/settings/reload/{guildId}` — invalidate & reload the settings cache. |
|
|
||||||
|
|
||||||
The action/guild handlers read live state from the [`JDA`](architecture.md) instance and from the
|
|
||||||
[persistence services](persistence.md#service-layer) (e.g. `GuildSettingService` for the settings
|
|
||||||
endpoints).
|
|
||||||
|
|
||||||
## HTTP client — `WebModule`
|
|
||||||
|
|
||||||
`WebModule` wraps an `OkHttpClient` for the bot's own outbound HTTP calls (e.g. checking for updates
|
|
||||||
via GitLab). It keeps a small `LinkedHashMap`-based response cache limited to 50 entries.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- There is no authentication layer documented on these endpoints; the API is expected to be bound to a
|
|
||||||
trusted/internal network (e.g. within the Docker network). `/actions/shutdown` is destructive, so the
|
|
||||||
port should not be exposed publicly.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
param (
|
|
||||||
[ValidateSet("migrate", "info", "validate", "clean", "baseline", "repair")]
|
|
||||||
[string]$Command = "info"
|
|
||||||
)
|
|
||||||
|
|
||||||
$envFile = ".env"
|
|
||||||
if (-not (Test-Path $envFile)) {
|
|
||||||
Write-Error ".env file not found in the current directory."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Get-Content $envFile | ForEach-Object {
|
|
||||||
if ($_ -match "^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$") {
|
|
||||||
$name = $matches[1]
|
|
||||||
$value = $matches[2]
|
|
||||||
[System.Environment]::SetEnvironmentVariable($name, $value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$MYSQL_HOST = $env:MYSQL_HOST
|
|
||||||
$MYSQL_PORT = $env:MYSQL_PORT
|
|
||||||
$MYSQL_DATABASE = $env:MYSQL_DATABASE
|
|
||||||
|
|
||||||
if (-not $MYSQL_HOST -or -not $MYSQL_PORT -or -not $MYSQL_DATABASE) {
|
|
||||||
Write-Error "MYSQL_HOST, MYSQL_PORT, or MYSQL_DATABASE missing from .env"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$env:FLYWAY_URL = "jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}"
|
|
||||||
$env:FLYWAY_USER = $env:MYSQL_USER
|
|
||||||
$env:FLYWAY_PASSWORD = $env:MYSQL_PASSWORD
|
|
||||||
|
|
||||||
Write-Host "Ensuring database '$MYSQL_DATABASE' exists..."
|
|
||||||
$mysqlCommand = "CREATE DATABASE IF NOT EXISTS $MYSQL_DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
|
||||||
& mysql --host=$MYSQL_HOST --port=$MYSQL_PORT --user=$env:MYSQL_USER --password=$env:MYSQL_PASSWORD -e $mysqlCommand
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Error "Failed to create database or connect to MySQL. Check your credentials or server status."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
.\mvnw flyway:$Command
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
COMMAND=${1:-info}
|
|
||||||
ENV_FILE=".env"
|
|
||||||
|
|
||||||
if [[ ! -f "$ENV_FILE" ]]; then
|
|
||||||
echo ".env file not found in the current directory."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export variables from .env
|
|
||||||
export $(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_FILE" | xargs)
|
|
||||||
|
|
||||||
if [[ -z "$MYSQL_HOST" || -z "$MYSQL_PORT" || -z "$MYSQL_DATABASE" ]]; then
|
|
||||||
echo "MYSQL_HOST, MYSQL_PORT, or MYSQL_DATABASE missing from .env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export FLYWAY_URL="jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}"
|
|
||||||
export FLYWAY_USER="$MYSQL_USER"
|
|
||||||
export FLYWAY_PASSWORD="$MYSQL_PASSWORD"
|
|
||||||
|
|
||||||
echo "Ensuring database '$MYSQL_DATABASE' exists..."
|
|
||||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" \
|
|
||||||
-e "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo "Failed to create database or connect to MySQL. Check your credentials or server status."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
./mvnw flyway:"$COMMAND"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
server:
|
|
||||||
port: 2333
|
|
||||||
address: 0.0.0.0
|
|
||||||
|
|
||||||
lavalink:
|
|
||||||
plugins:
|
|
||||||
# Replacement for the in-process dev.lavalink.youtube:v2 source manager.
|
|
||||||
# Custom SABR-enabled fork (ashton045/youtube-source, feat/sabr-support) built by JitPack.
|
|
||||||
- dependency: "com.github.ashton045.youtube-source:youtube-plugin:33d50abd5f"
|
|
||||||
repository: "https://jitpack.io"
|
|
||||||
snapshot: false
|
|
||||||
server:
|
|
||||||
password: "${SERVER_PASSWORD:youshallnotpass}"
|
|
||||||
sources:
|
|
||||||
# Handled by the youtube-plugin below, must stay disabled here.
|
|
||||||
youtube: false
|
|
||||||
soundcloud: true
|
|
||||||
bandcamp: false
|
|
||||||
twitch: false
|
|
||||||
vimeo: false
|
|
||||||
nico: false
|
|
||||||
http: true
|
|
||||||
local: false
|
|
||||||
filters:
|
|
||||||
volume: true
|
|
||||||
bufferDurationMs: 400
|
|
||||||
frameBufferDurationMs: 5000
|
|
||||||
opusEncodingQuality: 10
|
|
||||||
resamplingQuality: LOW
|
|
||||||
trackStuckThresholdMs: 10000
|
|
||||||
useSeekGhosting: true
|
|
||||||
youtubePlaylistLoadLimit: 6
|
|
||||||
playerUpdateInterval: 5
|
|
||||||
youtubeSearchEnabled: true
|
|
||||||
soundcloudSearchEnabled: true
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
youtube:
|
|
||||||
enabled: true
|
|
||||||
allowSearch: true
|
|
||||||
allowDirectVideoIds: true
|
|
||||||
allowDirectPlaylistIds: true
|
|
||||||
clients:
|
|
||||||
- TV
|
|
||||||
- MWEB
|
|
||||||
- WEB
|
|
||||||
- MUSIC
|
|
||||||
- WEBEMBEDDED
|
|
||||||
- ANDROID_VR
|
|
||||||
- TVHTML5_SIMPLY
|
|
||||||
oauth:
|
|
||||||
enabled: true
|
|
||||||
refreshToken: "${NETNEUTRALITY_OAUTH_TOKEN}"
|
|
||||||
remoteCipher:
|
|
||||||
url: "${NETNEUTRALITY_CIPHER_URL:http://ejs-api:8001}"
|
|
||||||
password: "${NETNEUTRALITY_CIPHER_PASSWORD:}"
|
|
||||||
remotePot:
|
|
||||||
url: "${NETNEUTRALITY_POT_URL:http://webpo:8080}"
|
|
||||||
|
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
root: INFO
|
|
||||||
lavalink: DEBUG
|
|
||||||
# Set to INFO to see the OAuth flow instructions (device code / verification URL) in the terminal.
|
|
||||||
dev.lavalink.youtube.http.YoutubeOauth2Handler: INFO
|
|
||||||
@@ -19,277 +19,290 @@
|
|||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||||
|
#
|
||||||
|
# Required ENV vars:
|
||||||
|
# ------------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir
|
||||||
#
|
#
|
||||||
# Optional ENV vars
|
# Optional ENV vars
|
||||||
# -----------------
|
# -----------------
|
||||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
# e.g. to debug Maven itself, use
|
||||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
set -euf
|
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
|
||||||
|
|
||||||
# OS specific support.
|
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||||
native_path() { printf %s\\n "$1"; }
|
. /usr/local/etc/mavenrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /etc/mavenrc ] ; then
|
||||||
|
. /etc/mavenrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$HOME/.mavenrc" ] ; then
|
||||||
|
. "$HOME/.mavenrc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OS specific support. $var _must_ be set to either true or false.
|
||||||
|
cygwin=false;
|
||||||
|
darwin=false;
|
||||||
|
mingw=false
|
||||||
case "$(uname)" in
|
case "$(uname)" in
|
||||||
CYGWIN* | MINGW*)
|
CYGWIN*) cygwin=true ;;
|
||||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
MINGW*) mingw=true;;
|
||||||
native_path() { cygpath --path --windows "$1"; }
|
Darwin*) darwin=true
|
||||||
|
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||||
|
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||||
|
if [ -z "$JAVA_HOME" ]; then
|
||||||
|
if [ -x "/usr/libexec/java_home" ]; then
|
||||||
|
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||||
|
else
|
||||||
|
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||||
|
fi
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# set JAVACMD and JAVACCMD
|
if [ -z "$JAVA_HOME" ] ; then
|
||||||
set_java_home() {
|
if [ -r /etc/gentoo-release ] ; then
|
||||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
JAVA_HOME=$(java-config --jre-home)
|
||||||
if [ -n "${JAVA_HOME-}" ]; then
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||||
|
if $cygwin ; then
|
||||||
|
[ -n "$JAVA_HOME" ] &&
|
||||||
|
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||||
|
[ -n "$CLASSPATH" ] &&
|
||||||
|
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||||
|
if $mingw ; then
|
||||||
|
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||||
|
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$JAVA_HOME" ]; then
|
||||||
|
javaExecutable="$(which javac)"
|
||||||
|
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||||
|
# readlink(1) is not available as standard on Solaris 10.
|
||||||
|
readLink=$(which readlink)
|
||||||
|
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||||
|
if $darwin ; then
|
||||||
|
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||||
|
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||||
|
else
|
||||||
|
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||||
|
fi
|
||||||
|
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||||
|
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||||
|
JAVA_HOME="$javaHome"
|
||||||
|
export JAVA_HOME
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$JAVACMD" ] ; then
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
echo " We cannot execute $JAVACMD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$JAVA_HOME" ] ; then
|
||||||
|
echo "Warning: JAVA_HOME environment variable is not set."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# traverses directory structure from process work directory to filesystem root
|
||||||
|
# first directory with .mvn subdirectory is considered project base directory
|
||||||
|
find_maven_basedir() {
|
||||||
|
if [ -z "$1" ]
|
||||||
|
then
|
||||||
|
echo "Path not specified to find_maven_basedir"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="$(
|
|
||||||
'set' +e
|
|
||||||
'unset' -f command 2>/dev/null
|
|
||||||
'command' -v java
|
|
||||||
)" || :
|
|
||||||
JAVACCMD="$(
|
|
||||||
'set' +e
|
|
||||||
'unset' -f command 2>/dev/null
|
|
||||||
'command' -v javac
|
|
||||||
)" || :
|
|
||||||
|
|
||||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
basedir="$1"
|
||||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
wdir="$1"
|
||||||
return 1
|
while [ "$wdir" != '/' ] ; do
|
||||||
fi
|
if [ -d "$wdir"/.mvn ] ; then
|
||||||
fi
|
basedir=$wdir
|
||||||
}
|
|
||||||
|
|
||||||
# hash string like Java String::hashCode
|
|
||||||
hash_string() {
|
|
||||||
str="${1:-}" h=0
|
|
||||||
while [ -n "$str" ]; do
|
|
||||||
char="${str%"${str#?}"}"
|
|
||||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
|
||||||
str="${str#?}"
|
|
||||||
done
|
|
||||||
printf %x\\n $h
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose() { :; }
|
|
||||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf %s\\n "$1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
trim() {
|
|
||||||
# MWRAPPER-139:
|
|
||||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
|
||||||
# Needed for removing poorly interpreted newline sequences when running in more
|
|
||||||
# exotic environments such as mingw bash on Windows.
|
|
||||||
printf "%s" "${1}" | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptDir="$(dirname "$0")"
|
|
||||||
scriptName="$(basename "$0")"
|
|
||||||
|
|
||||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
|
||||||
while IFS="=" read -r key value; do
|
|
||||||
case "${key-}" in
|
|
||||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
|
||||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
|
||||||
esac
|
|
||||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
|
||||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
|
||||||
|
|
||||||
case "${distributionUrl##*/}" in
|
|
||||||
maven-mvnd-*bin.*)
|
|
||||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
|
||||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
|
||||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
|
||||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
|
||||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
|
||||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
|
||||||
*)
|
|
||||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
|
||||||
distributionPlatform=linux-amd64
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
|
||||||
;;
|
|
||||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
|
||||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
|
||||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
|
||||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
|
||||||
distributionUrlName="${distributionUrl##*/}"
|
|
||||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
|
||||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
|
||||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
|
||||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
|
||||||
|
|
||||||
exec_maven() {
|
|
||||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
|
||||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -d "$MAVEN_HOME" ]; then
|
|
||||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
|
||||||
exec_maven "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "${distributionUrl-}" in
|
|
||||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
|
||||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# prepare tmp dir
|
|
||||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
|
||||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
|
||||||
trap clean HUP INT TERM EXIT
|
|
||||||
else
|
|
||||||
die "cannot create temp dir"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
|
||||||
|
|
||||||
# Download and Install Apache Maven
|
|
||||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
|
||||||
verbose "Downloading from: $distributionUrl"
|
|
||||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
|
||||||
|
|
||||||
# select .zip or .tar.gz
|
|
||||||
if ! command -v unzip >/dev/null; then
|
|
||||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
|
||||||
distributionUrlName="${distributionUrl##*/}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# verbose opt
|
|
||||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
|
||||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
|
||||||
|
|
||||||
# normalize http auth
|
|
||||||
case "${MVNW_PASSWORD:+has-password}" in
|
|
||||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
|
||||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
|
||||||
verbose "Found wget ... using wget"
|
|
||||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
|
||||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
|
||||||
verbose "Found curl ... using curl"
|
|
||||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
|
||||||
elif set_java_home; then
|
|
||||||
verbose "Falling back to use Java to download"
|
|
||||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
|
||||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
|
||||||
cat >"$javaSource" <<-END
|
|
||||||
public class Downloader extends java.net.Authenticator
|
|
||||||
{
|
|
||||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
|
||||||
{
|
|
||||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
|
||||||
}
|
|
||||||
public static void main( String[] args ) throws Exception
|
|
||||||
{
|
|
||||||
setDefault( new Downloader() );
|
|
||||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END
|
|
||||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
|
||||||
verbose " - Compiling Downloader.java ..."
|
|
||||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
|
||||||
verbose " - Running Downloader.java ..."
|
|
||||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
|
||||||
if [ -n "${distributionSha256Sum-}" ]; then
|
|
||||||
distributionSha256Result=false
|
|
||||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
|
||||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
|
||||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
|
||||||
exit 1
|
|
||||||
elif command -v sha256sum >/dev/null; then
|
|
||||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
|
||||||
distributionSha256Result=true
|
|
||||||
fi
|
|
||||||
elif command -v shasum >/dev/null; then
|
|
||||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
|
||||||
distributionSha256Result=true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
|
||||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ $distributionSha256Result = false ]; then
|
|
||||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
|
||||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# unzip and move
|
|
||||||
if command -v unzip >/dev/null; then
|
|
||||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
|
||||||
else
|
|
||||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
|
||||||
actualDistributionDir=""
|
|
||||||
|
|
||||||
# First try the expected directory name (for regular distributions)
|
|
||||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
|
||||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
|
||||||
actualDistributionDir="$distributionUrlNameMain"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
|
||||||
if [ -z "$actualDistributionDir" ]; then
|
|
||||||
# enable globbing to iterate over items
|
|
||||||
set +f
|
|
||||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
|
||||||
actualDistributionDir="$(basename "$dir")"
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||||
|
if [ -d "${wdir}" ]; then
|
||||||
|
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||||
fi
|
fi
|
||||||
|
# end of workaround
|
||||||
done
|
done
|
||||||
set -f
|
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# concatenates all lines of a file
|
||||||
|
concat_lines() {
|
||||||
|
if [ -f "$1" ]; then
|
||||||
|
# Remove \r in case we run on Windows within Git Bash
|
||||||
|
# and check out the repository with auto CRLF management
|
||||||
|
# enabled. Otherwise, we may read lines that are delimited with
|
||||||
|
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||||
|
# splitting rules.
|
||||||
|
tr -s '\r\n' ' ' < "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
if [ "$MVNW_VERBOSE" = true ]; then
|
||||||
|
printf '%s\n' "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||||
|
if [ -z "$BASE_DIR" ]; then
|
||||||
|
exit 1;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$actualDistributionDir" ]; then
|
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
log "$MAVEN_PROJECTBASEDIR"
|
||||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
|
||||||
die "Could not find Maven distribution directory in extracted archive"
|
##########################################################################################
|
||||||
|
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||||
|
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||||
|
##########################################################################################
|
||||||
|
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||||
|
if [ -r "$wrapperJarPath" ]; then
|
||||||
|
log "Found $wrapperJarPath"
|
||||||
|
else
|
||||||
|
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||||
|
|
||||||
|
if [ -n "$MVNW_REPOURL" ]; then
|
||||||
|
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||||
|
else
|
||||||
|
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||||
|
fi
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||||
|
safeValue=$(echo "$value" | tr -d '\r')
|
||||||
|
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||||
|
esac
|
||||||
|
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
log "Downloading from: $wrapperUrl"
|
||||||
|
|
||||||
|
if $cygwin; then
|
||||||
|
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
if command -v wget > /dev/null; then
|
||||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
log "Found wget ... using wget"
|
||||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||||
|
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||||
|
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||||
|
else
|
||||||
|
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||||
|
fi
|
||||||
|
elif command -v curl > /dev/null; then
|
||||||
|
log "Found curl ... using curl"
|
||||||
|
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||||
|
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||||
|
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||||
|
else
|
||||||
|
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Falling back to using Java to download"
|
||||||
|
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||||
|
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||||
|
# For Cygwin, switch paths to Windows format before running javac
|
||||||
|
if $cygwin; then
|
||||||
|
javaSource=$(cygpath --path --windows "$javaSource")
|
||||||
|
javaClass=$(cygpath --path --windows "$javaClass")
|
||||||
|
fi
|
||||||
|
if [ -e "$javaSource" ]; then
|
||||||
|
if [ ! -e "$javaClass" ]; then
|
||||||
|
log " - Compiling MavenWrapperDownloader.java ..."
|
||||||
|
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||||
|
fi
|
||||||
|
if [ -e "$javaClass" ]; then
|
||||||
|
log " - Running MavenWrapperDownloader.java ..."
|
||||||
|
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
##########################################################################################
|
||||||
|
# End of extension
|
||||||
|
##########################################################################################
|
||||||
|
|
||||||
clean || :
|
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||||
exec_maven "$@"
|
wrapperSha256Sum=""
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||||
|
esac
|
||||||
|
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
if [ -n "$wrapperSha256Sum" ]; then
|
||||||
|
wrapperSha256Result=false
|
||||||
|
if command -v sha256sum > /dev/null; then
|
||||||
|
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||||
|
wrapperSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum > /dev/null; then
|
||||||
|
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||||
|
wrapperSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||||
|
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $wrapperSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||||
|
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin; then
|
||||||
|
[ -n "$JAVA_HOME" ] &&
|
||||||
|
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||||
|
[ -n "$CLASSPATH" ] &&
|
||||||
|
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||||
|
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||||
|
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Provide a "standardized" way to retrieve the CLI args that will
|
||||||
|
# work with both Windows and non-Windows executions.
|
||||||
|
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||||
|
export MAVEN_CMD_LINE_ARGS
|
||||||
|
|
||||||
|
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086 # safe args
|
||||||
|
exec "$JAVACMD" \
|
||||||
|
$MAVEN_OPTS \
|
||||||
|
$MAVEN_DEBUG_OPTS \
|
||||||
|
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||||
|
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||||
|
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
<# : batch portion
|
|
||||||
@REM ----------------------------------------------------------------------------
|
@REM ----------------------------------------------------------------------------
|
||||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
@REM or more contributor license agreements. See the NOTICE file
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
@@ -19,171 +18,188 @@
|
|||||||
@REM ----------------------------------------------------------------------------
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
@REM ----------------------------------------------------------------------------
|
@REM ----------------------------------------------------------------------------
|
||||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||||
|
@REM
|
||||||
|
@REM Required ENV vars:
|
||||||
|
@REM JAVA_HOME - location of a JDK home dir
|
||||||
@REM
|
@REM
|
||||||
@REM Optional ENV vars
|
@REM Optional ENV vars
|
||||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||||
|
@REM e.g. to debug Maven itself, use
|
||||||
|
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||||
|
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||||
@REM ----------------------------------------------------------------------------
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||||
@SET __MVNW_CMD__=
|
@echo off
|
||||||
@SET __MVNW_ERROR__=
|
@REM set title of command window
|
||||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
title %0
|
||||||
@SET PSModulePath=
|
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
|
||||||
|
@REM set %HOME% to equivalent of $HOME
|
||||||
|
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||||
|
|
||||||
|
@REM Execute a user defined script before this one
|
||||||
|
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||||
|
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||||
|
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||||
|
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||||
|
:skipRcPre
|
||||||
|
|
||||||
|
@setlocal
|
||||||
|
|
||||||
|
set ERROR_CODE=0
|
||||||
|
|
||||||
|
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||||
|
@setlocal
|
||||||
|
|
||||||
|
@REM ==== START VALIDATION ====
|
||||||
|
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Error: JAVA_HOME not found in your environment. >&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||||
|
echo location of your Java installation. >&2
|
||||||
|
echo.
|
||||||
|
goto error
|
||||||
|
|
||||||
|
:OkJHome
|
||||||
|
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||||
|
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||||
|
echo location of your Java installation. >&2
|
||||||
|
echo.
|
||||||
|
goto error
|
||||||
|
|
||||||
|
@REM ==== END VALIDATION ====
|
||||||
|
|
||||||
|
:init
|
||||||
|
|
||||||
|
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||||
|
@REM Fallback to current working directory if not found.
|
||||||
|
|
||||||
|
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||||
|
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||||
|
|
||||||
|
set EXEC_DIR=%CD%
|
||||||
|
set WDIR=%EXEC_DIR%
|
||||||
|
:findBaseDir
|
||||||
|
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||||
|
cd ..
|
||||||
|
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||||
|
set WDIR=%CD%
|
||||||
|
goto findBaseDir
|
||||||
|
|
||||||
|
:baseDirFound
|
||||||
|
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||||
|
cd "%EXEC_DIR%"
|
||||||
|
goto endDetectBaseDir
|
||||||
|
|
||||||
|
:baseDirNotFound
|
||||||
|
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||||
|
cd "%EXEC_DIR%"
|
||||||
|
|
||||||
|
:endDetectBaseDir
|
||||||
|
|
||||||
|
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||||
|
|
||||||
|
@setlocal EnableExtensions EnableDelayedExpansion
|
||||||
|
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||||
|
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||||
|
|
||||||
|
:endReadAdditionalConfig
|
||||||
|
|
||||||
|
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||||
|
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||||
|
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||||
|
|
||||||
|
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||||
|
|
||||||
|
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||||
|
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||||
)
|
)
|
||||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
|
||||||
@SET __MVNW_PSMODULEP_SAVE=
|
|
||||||
@SET __MVNW_ARG0_NAME__=
|
|
||||||
@SET MVNW_USERNAME=
|
|
||||||
@SET MVNW_PASSWORD=
|
|
||||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
|
||||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
|
||||||
@GOTO :EOF
|
|
||||||
: end batch / begin powershell #>
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||||
if ($env:MVNW_VERBOSE -eq "true") {
|
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||||
$VerbosePreference = "Continue"
|
if exist %WRAPPER_JAR% (
|
||||||
}
|
if "%MVNW_VERBOSE%" == "true" (
|
||||||
|
echo Found %WRAPPER_JAR%
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
if not "%MVNW_REPOURL%" == "" (
|
||||||
|
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||||
|
)
|
||||||
|
if "%MVNW_VERBOSE%" == "true" (
|
||||||
|
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||||
|
echo Downloading from: %WRAPPER_URL%
|
||||||
|
)
|
||||||
|
|
||||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
powershell -Command "&{"^
|
||||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
"$webclient = new-object System.Net.WebClient;"^
|
||||||
if (!$distributionUrl) {
|
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||||
}
|
"}"^
|
||||||
|
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||||
|
"}"
|
||||||
|
if "%MVNW_VERBOSE%" == "true" (
|
||||||
|
echo Finished downloading %WRAPPER_JAR%
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@REM End of extension
|
||||||
|
|
||||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||||
"maven-mvnd-*" {
|
SET WRAPPER_SHA_256_SUM=""
|
||||||
$USE_MVND = $true
|
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||||
$MVN_CMD = "mvnd.cmd"
|
)
|
||||||
break
|
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||||
}
|
powershell -Command "&{"^
|
||||||
default {
|
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||||
$USE_MVND = $false
|
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||||
break
|
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||||
}
|
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||||
}
|
" exit 1;"^
|
||||||
|
"}"^
|
||||||
|
"}"
|
||||||
|
if ERRORLEVEL 1 goto error
|
||||||
|
)
|
||||||
|
|
||||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
@REM work with both Windows and non-Windows executions.
|
||||||
if ($env:MVNW_REPOURL) {
|
set MAVEN_CMD_LINE_ARGS=%*
|
||||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
|
||||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
|
||||||
}
|
|
||||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
|
||||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
|
||||||
|
|
||||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
%MAVEN_JAVA_EXE% ^
|
||||||
if ($env:MAVEN_USER_HOME) {
|
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
%MAVEN_OPTS% ^
|
||||||
}
|
%MAVEN_DEBUG_OPTS% ^
|
||||||
|
-classpath %WRAPPER_JAR% ^
|
||||||
|
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||||
|
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||||
|
if ERRORLEVEL 1 goto error
|
||||||
|
goto end
|
||||||
|
|
||||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
:error
|
||||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
set ERROR_CODE=1
|
||||||
}
|
|
||||||
|
|
||||||
$MAVEN_WRAPPER_DISTS = $null
|
:end
|
||||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
|
||||||
} else {
|
|
||||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
|
||||||
}
|
|
||||||
|
|
||||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||||
|
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||||
|
:skipRcPost
|
||||||
|
|
||||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
|
||||||
exit $?
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
# prepare tmp dir
|
cmd /C exit /B %ERROR_CODE%
|
||||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
|
||||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
|
||||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
|
||||||
trap {
|
|
||||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
|
||||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
|
||||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
|
||||||
|
|
||||||
# Download and Install Apache Maven
|
|
||||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
|
||||||
Write-Verbose "Downloading from: $distributionUrl"
|
|
||||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
|
||||||
|
|
||||||
$webclient = New-Object System.Net.WebClient
|
|
||||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
|
||||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
|
||||||
}
|
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
||||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
|
||||||
|
|
||||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
|
||||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
|
||||||
if ($distributionSha256Sum) {
|
|
||||||
if ($USE_MVND) {
|
|
||||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
|
||||||
}
|
|
||||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
|
||||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
|
||||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# unzip and move
|
|
||||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
|
||||||
|
|
||||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
|
||||||
$actualDistributionDir = ""
|
|
||||||
|
|
||||||
# First try the expected directory name (for regular distributions)
|
|
||||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
|
||||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
|
||||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
|
||||||
$actualDistributionDir = $distributionUrlNameMain
|
|
||||||
}
|
|
||||||
|
|
||||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
|
||||||
if (!$actualDistributionDir) {
|
|
||||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
|
||||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
|
||||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
|
||||||
$actualDistributionDir = $_.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$actualDistributionDir) {
|
|
||||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
|
||||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
|
||||||
try {
|
|
||||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
|
||||||
} catch {
|
|
||||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
|
||||||
Write-Error "fail to move MAVEN_HOME"
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
|
||||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
|
||||||
@@ -12,13 +12,11 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<main.class>me.bobert.netneutrality.App</main.class>
|
<main.class>me.bobert.netneutrality.NetNeutrality</main.class>
|
||||||
<java.version>25</java.version>
|
<java.version>17</java.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>25</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<maven.compiler.target>25</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<mockito.version>5.23.0</mockito.version>
|
|
||||||
<testcontainers.version>2.0.5</testcontainers.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -30,143 +28,52 @@
|
|||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.14.1</version>
|
|
||||||
<configuration>
|
|
||||||
<release>${java.version}</release>
|
|
||||||
<proc>full</proc>
|
|
||||||
<annotationProcessorPaths>
|
|
||||||
<path>
|
|
||||||
<groupId>org.hibernate.orm</groupId>
|
|
||||||
<artifactId>hibernate-processor</artifactId>
|
|
||||||
<version>7.1.9.Final</version>
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<groupId>io.javalin.community.openapi</groupId>
|
|
||||||
<artifactId>openapi-annotation-processor</artifactId>
|
|
||||||
<version>7.2.3</version>
|
|
||||||
</path>
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>3.5.5</version>
|
|
||||||
<configuration>
|
|
||||||
<argLine>
|
|
||||||
-javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar"
|
|
||||||
</argLine>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-maven-plugin</artifactId>
|
<artifactId>flyway-maven-plugin</artifactId>
|
||||||
<version>11.20.3</version>
|
<version>10.17.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!--suppress UnresolvedMavenProperty -->
|
<url>jdbc:mysql://localhost:3306/netneutrality</url>
|
||||||
<url>${env.FLYWAY_URL}</url>
|
<user>root</user>
|
||||||
<!--suppress UnresolvedMavenProperty -->
|
<password></password>
|
||||||
<user>${env.FLYWAY_USER}</user>
|
|
||||||
<!--suppress UnresolvedMavenProperty -->
|
|
||||||
<password>${env.FLYWAY_PASSWORD}</password>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-resources-plugin</artifactId>
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.3.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<propertiesEncoding>UTF-8</propertiesEncoding>
|
<propertiesEncoding>UTF-8</propertiesEncoding>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.7.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<archive>
|
||||||
<manifest>
|
|
||||||
<mainClass>${main.class}</mainClass>
|
|
||||||
</manifest>
|
|
||||||
</archive>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.6.2</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<finalName>netneutrality</finalName>
|
|
||||||
<filters>
|
|
||||||
<filter>
|
|
||||||
<artifact>*:*</artifact>
|
|
||||||
<excludes>
|
|
||||||
<exclude>module-info.class</exclude>
|
|
||||||
<exclude>META-INF/versions/*/module-info.class</exclude>
|
|
||||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
|
||||||
<exclude>META-INF/LICENSE*</exclude>
|
|
||||||
<exclude>META-INF/NOTICE*</exclude>
|
|
||||||
<exclude>META-INF/DEPENDENCIES</exclude>
|
|
||||||
<exclude>META-INF/jandex.idx</exclude>
|
|
||||||
<exclude>META-INF/versions/9/OSGI-INF/MANIFEST.MF</exclude>
|
|
||||||
<exclude>META-INF/*.SF</exclude>
|
|
||||||
<exclude>META-INF/*.DSA</exclude>
|
|
||||||
<exclude>META-INF/*.RSA</exclude>
|
|
||||||
</excludes>
|
|
||||||
</filter>
|
|
||||||
<filter>
|
|
||||||
<artifact>com.squareup.okhttp3:okhttp</artifact>
|
|
||||||
<excludes>
|
|
||||||
<exclude>**</exclude>
|
|
||||||
</excludes>
|
|
||||||
</filter>
|
|
||||||
</filters>
|
|
||||||
<transformers>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<mainClass>${main.class}</mainClass>
|
|
||||||
</transformer>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<manifestEntries>
|
<manifestEntries>
|
||||||
<Multi-Release>true</Multi-Release>
|
<Multi-Release>true</Multi-Release>
|
||||||
</manifestEntries>
|
</manifestEntries>
|
||||||
</transformer>
|
<manifest>
|
||||||
</transformers>
|
<addClasspath>true</addClasspath>
|
||||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
<mainClass>${main.class}</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
<finalName>netneutrality</finalName>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
|
||||||
<version>3.6.0</version>
|
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>checkstyle</id>
|
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||||
<phase>test</phase>
|
<phase>package</phase> <!-- bind to the packaging phase -->
|
||||||
<goals>
|
<goals>
|
||||||
<goal>check</goal>
|
<goal>single</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
<configuration>
|
|
||||||
<configLocation>checkstyle.xml</configLocation>
|
|
||||||
<failOnViolation>true</failOnViolation>
|
|
||||||
<linkXRef>false</linkXRef>
|
|
||||||
<outputFile>target/checkstyle-result.xml</outputFile>
|
|
||||||
<outputFormat>xml</outputFormat>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
@@ -193,96 +100,71 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.dv8tion</groupId>
|
<groupId>net.dv8tion</groupId>
|
||||||
<artifactId>JDA</artifactId>
|
<artifactId>JDA</artifactId>
|
||||||
<version>6.4.1</version>
|
<version>5.1.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Lavalink v4 client; audio decoding happens on the remote node. -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.arbjerg</groupId>
|
<groupId>dev.arbjerg</groupId>
|
||||||
<artifactId>lavalink-client</artifactId>
|
<artifactId>lavaplayer</artifactId>
|
||||||
<version>3.4.0</version>
|
<version>2.2.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.lavalink.youtube</groupId>
|
||||||
|
<artifactId>v2</artifactId>
|
||||||
|
<version>1.11.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
<artifactId>okhttp-jvm</artifactId>
|
<artifactId>okhttp</artifactId>
|
||||||
<version>5.3.2</version>
|
<version>4.12.0</version>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.javalin</groupId>
|
|
||||||
<artifactId>javalin</artifactId>
|
|
||||||
<version>7.2.3</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.javalin.community.openapi</groupId>
|
|
||||||
<artifactId>openapi-specification</artifactId>
|
|
||||||
<version>7.2.3</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.javalin.community.openapi</groupId>
|
|
||||||
<artifactId>javalin-openapi-plugin</artifactId>
|
|
||||||
<version>7.2.3</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.javalin.community.openapi</groupId>
|
|
||||||
<artifactId>openapi-generator</artifactId>
|
|
||||||
<version>7.2.3</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.javalin.community.openapi</groupId>
|
|
||||||
<artifactId>javalin-swagger-plugin</artifactId>
|
|
||||||
<version>7.2.3</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate.orm</groupId>
|
<groupId>org.hibernate.orm</groupId>
|
||||||
<artifactId>hibernate-core</artifactId>
|
<artifactId>hibernate-core</artifactId>
|
||||||
<version>7.1.9.Final</version>
|
<version>6.5.2.Final</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate.orm</groupId>
|
<groupId>org.hibernate.orm</groupId>
|
||||||
<artifactId>hibernate-hikaricp</artifactId>
|
<artifactId>hibernate-hikaricp</artifactId>
|
||||||
<version>7.1.9.Final</version>
|
<version>6.5.2.Final</version>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>jakarta.data</groupId>
|
|
||||||
<artifactId>jakarta.data-api</artifactId>
|
|
||||||
<version>1.0.1</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-j</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
<version>9.7.0</version>
|
<version>9.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-core</artifactId>
|
<artifactId>flyway-core</artifactId>
|
||||||
<version>11.20.3</version>
|
<version>10.17.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-mysql</artifactId>
|
<artifactId>flyway-mysql</artifactId>
|
||||||
<version>11.20.3</version>
|
<version>10.17.0</version>
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.sentry</groupId>
|
<groupId>io.sentry</groupId>
|
||||||
<artifactId>sentry</artifactId>
|
<artifactId>sentry</artifactId>
|
||||||
<version>8.41.0</version>
|
<version>7.12.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.gitlab4j</groupId>
|
||||||
|
<artifactId>gitlab4j-api</artifactId>
|
||||||
|
<version>6.0.0-rc.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.fellbaum</groupId>
|
<groupId>net.fellbaum</groupId>
|
||||||
<artifactId>jemoji</artifactId>
|
<artifactId>jemoji</artifactId>
|
||||||
<version>1.7.6</version>
|
<version>1.7.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>org.json</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>json</artifactId>
|
||||||
<version>2.21.2</version>
|
<version>20240303</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -294,60 +176,41 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
<artifactId>log4j-api</artifactId>
|
<artifactId>log4j-api</artifactId>
|
||||||
<version>2.26.0</version>
|
<version>2.23.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
<artifactId>log4j-core</artifactId>
|
<artifactId>log4j-core</artifactId>
|
||||||
<version>2.26.0</version>
|
<version>2.23.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||||
<version>2.26.0</version>
|
<version>2.23.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
<version>3.19.0</version>
|
<version>3.15.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
<version>1.17.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.21.0</version>
|
<version>2.16.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<version>6.0.3</version>
|
<version>5.11.0</version>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mockito</groupId>
|
|
||||||
<artifactId>mockito-core</artifactId>
|
|
||||||
<version>${mockito.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mockito</groupId>
|
|
||||||
<artifactId>mockito-junit-jupiter</artifactId>
|
|
||||||
<version>${mockito.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.testcontainers</groupId>
|
|
||||||
<artifactId>testcontainers-junit-jupiter</artifactId>
|
|
||||||
<version>${testcontainers.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.testcontainers</groupId>
|
|
||||||
<artifactId>testcontainers-mysql</artifactId>
|
|
||||||
<version>${testcontainers.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
+1
-58
@@ -1,63 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"local>renovate/renovate-config",
|
"renovate/renovate-config"
|
||||||
"group:monorepos",
|
|
||||||
"group:flyway"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchPackageNames": [
|
|
||||||
"mariadb"
|
|
||||||
],
|
|
||||||
"automerge": false,
|
|
||||||
"dependencyDashboardApproval": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchManagers": ["maven"],
|
|
||||||
"extends": ["schedule:monthly"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"groupName": "maven plugins",
|
|
||||||
"matchManagers": ["maven"],
|
|
||||||
"matchDepTypes": ["plugin"],
|
|
||||||
"extends": ["schedule:weekly"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"groupName": "hibernate orm",
|
|
||||||
"matchPackageNames": [
|
|
||||||
"org.hibernate.orm:**"
|
|
||||||
],
|
|
||||||
"extends": ["schedule:monthly"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"groupName": "apache commons",
|
|
||||||
"matchPackageNames": [
|
|
||||||
"commons-**",
|
|
||||||
"org.apache.commons:**"
|
|
||||||
],
|
|
||||||
"extends": ["schedule:monthly"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchManagers": ["maven"],
|
|
||||||
"matchDepTypes": ["dependency", "parentPom"],
|
|
||||||
"matchPackageNames": [
|
|
||||||
"net.dv8tion:JDA",
|
|
||||||
"dev.arbjerg:lavaplayer",
|
|
||||||
"dev.lavalink.youtube:**",
|
|
||||||
"club.minnced:jdave-*"
|
|
||||||
],
|
|
||||||
"schedule": ["at any time"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"groupName": "github actions (minor/patch)",
|
|
||||||
"matchManagers": ["github-actions"],
|
|
||||||
"matchUpdateTypes": ["minor", "patch", "digest"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"groupName": "github actions (major)",
|
|
||||||
"matchManagers": ["github-actions"],
|
|
||||||
"matchUpdateTypes": ["major"]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+00:00";
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Database: `netneutrality`
|
-- Database: `netneutrality`
|
||||||
--
|
--
|
||||||
CREATE DATABASE IF NOT EXISTS `netneutrality` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE IF NOT EXISTS `netneutrality` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE `netneutrality`;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `blacklist`
|
-- Table structure for table `blacklist`
|
||||||
--
|
--
|
||||||
CREATE TABLE `blacklist` (
|
|
||||||
|
DROP TABLE IF EXISTS `blacklist`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `blacklist` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`guild_id` varchar(255) NOT NULL,
|
`guild_id` varchar(255) NOT NULL,
|
||||||
`word` varchar(255) NOT NULL,
|
`word` varchar(255) NOT NULL,
|
||||||
@@ -23,7 +36,9 @@ CREATE TABLE `blacklist` (
|
|||||||
--
|
--
|
||||||
-- Table structure for table `guilds`
|
-- Table structure for table `guilds`
|
||||||
--
|
--
|
||||||
CREATE TABLE `guilds` (
|
|
||||||
|
DROP TABLE IF EXISTS `guilds`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `guilds` (
|
||||||
`guild_id` varchar(255) NOT NULL,
|
`guild_id` varchar(255) NOT NULL,
|
||||||
`logging` tinyint(1) NOT NULL DEFAULT 0,
|
`logging` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`debug` tinyint(1) NOT NULL DEFAULT 1,
|
`debug` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
@@ -36,7 +51,9 @@ CREATE TABLE `guilds` (
|
|||||||
--
|
--
|
||||||
-- Table structure for table `guild_settings`
|
-- Table structure for table `guild_settings`
|
||||||
--
|
--
|
||||||
CREATE TABLE `guild_settings` (
|
|
||||||
|
DROP TABLE IF EXISTS `guild_settings`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `guild_settings` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`guild_id` varchar(255) NOT NULL,
|
`guild_id` varchar(255) NOT NULL,
|
||||||
`name` varchar(255) NOT NULL,
|
`name` varchar(255) NOT NULL,
|
||||||
@@ -50,7 +67,9 @@ CREATE TABLE `guild_settings` (
|
|||||||
--
|
--
|
||||||
-- Table structure for table `logging`
|
-- Table structure for table `logging`
|
||||||
--
|
--
|
||||||
CREATE TABLE `logging` (
|
|
||||||
|
DROP TABLE IF EXISTS `logging`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `logging` (
|
||||||
`guild_id` varchar(255) NOT NULL,
|
`guild_id` varchar(255) NOT NULL,
|
||||||
`channel_id` varchar(255) NOT NULL,
|
`channel_id` varchar(255) NOT NULL,
|
||||||
`enabled` tinyint(1) NOT NULL DEFAULT 0,
|
`enabled` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
@@ -62,7 +81,9 @@ CREATE TABLE `logging` (
|
|||||||
--
|
--
|
||||||
-- Table structure for table `log_message`
|
-- Table structure for table `log_message`
|
||||||
--
|
--
|
||||||
CREATE TABLE `log_message` (
|
|
||||||
|
DROP TABLE IF EXISTS `log_message`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `log_message` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`guild_id` varchar(255) NOT NULL,
|
`guild_id` varchar(255) NOT NULL,
|
||||||
`channel_id` varchar(255) NOT NULL,
|
`channel_id` varchar(255) NOT NULL,
|
||||||
@@ -81,7 +102,9 @@ CREATE TABLE `log_message` (
|
|||||||
--
|
--
|
||||||
-- Table structure for table `tasks`
|
-- Table structure for table `tasks`
|
||||||
--
|
--
|
||||||
CREATE TABLE `tasks` (
|
|
||||||
|
DROP TABLE IF EXISTS `tasks`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `tasks` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`task_id` varchar(255) NOT NULL,
|
`task_id` varchar(255) NOT NULL,
|
||||||
`task_class` varchar(255) NOT NULL,
|
`task_class` varchar(255) NOT NULL,
|
||||||
@@ -95,7 +118,9 @@ CREATE TABLE `tasks` (
|
|||||||
--
|
--
|
||||||
-- Table structure for table `web_users`
|
-- Table structure for table `web_users`
|
||||||
--
|
--
|
||||||
CREATE TABLE `web_users` (
|
|
||||||
|
DROP TABLE IF EXISTS `web_users`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `web_users` (
|
||||||
`id` varchar(255) NOT NULL,
|
`id` varchar(255) NOT NULL,
|
||||||
`email` mediumtext NOT NULL,
|
`email` mediumtext NOT NULL,
|
||||||
`token` varchar(255) NOT NULL,
|
`token` varchar(255) NOT NULL,
|
||||||
@@ -105,6 +130,10 @@ CREATE TABLE `web_users` (
|
|||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Constraints for table `blacklist`
|
-- Constraints for table `blacklist`
|
||||||
--
|
--
|
||||||
@@ -128,3 +157,8 @@ ALTER TABLE `logging`
|
|||||||
--
|
--
|
||||||
ALTER TABLE `log_message`
|
ALTER TABLE `log_message`
|
||||||
ADD CONSTRAINT `fk_log_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
ADD CONSTRAINT `fk_log_guilds` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`guild_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
package me.bobert.netneutrality;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
|
||||||
import io.sentry.Sentry;
|
|
||||||
import me.bobert.netneutrality.bootstrap.ApplicationContext;
|
|
||||||
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
|
||||||
import me.bobert.netneutrality.bootstrap.BotState;
|
|
||||||
import me.bobert.netneutrality.core.LogHandler;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandManager;
|
|
||||||
import me.bobert.netneutrality.db.SessionManager;
|
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManagerImpl;
|
|
||||||
import me.bobert.netneutrality.core.scheduled.ScheduledTaskManager;
|
|
||||||
import me.bobert.netneutrality.core.thread.ThreadManager;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProviderImpl;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandRegistry;
|
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
import me.bobert.netneutrality.hooks.EventsHandler;
|
|
||||||
import me.bobert.netneutrality.http.server.WebApi;
|
|
||||||
import me.bobert.netneutrality.music.MusicServiceImpl;
|
|
||||||
import me.bobert.netneutrality.core.thread.ExecutorProvider;
|
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.utils.ChannelMessagePurger;
|
|
||||||
import me.bobert.netneutrality.utils.UserFunction;
|
|
||||||
import me.bobert.netneutrality.core.command.precondition.DisabledCommandPrecondition;
|
|
||||||
import me.bobert.netneutrality.db.repository.BlacklistRepository;
|
|
||||||
import me.bobert.netneutrality.db.repository.GuildRepository;
|
|
||||||
import me.bobert.netneutrality.db.repository.GuildSettingRepository;
|
|
||||||
import me.bobert.netneutrality.db.repository.TaskRepository;
|
|
||||||
import me.bobert.netneutrality.utils.Settings;
|
|
||||||
import me.bobert.netneutrality.http.WebModule;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
|
||||||
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
|
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
|
||||||
import org.apache.logging.log4j.Level;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.apache.logging.log4j.core.config.Configurator;
|
|
||||||
|
|
||||||
public class App {
|
|
||||||
private static final Logger logger = LogManager.getLogger(App.class);
|
|
||||||
private static App instance;
|
|
||||||
private final Settings settings;
|
|
||||||
private JDA jda;
|
|
||||||
private MusicService music;
|
|
||||||
private final ApplicationContext context = new ApplicationContext();
|
|
||||||
|
|
||||||
static void main() {
|
|
||||||
Settings settings = new Settings();
|
|
||||||
String version = settings.getMavenVersion();
|
|
||||||
WebModule.setDefaultVersion(version);
|
|
||||||
logger.info("Starting App v{}", version);
|
|
||||||
|
|
||||||
String env = settings.get("NETNEUTRALITY_BOT_ENV", "dev");
|
|
||||||
String sentryDsn = settings.get("NETNEUTRALITY_SENTRY_DSN");
|
|
||||||
if (sentryDsn != null) {
|
|
||||||
logger.info("Found Sentry DSN in config, setting up the SDK..");
|
|
||||||
|
|
||||||
Sentry.init(options -> {
|
|
||||||
options.setDsn(sentryDsn);
|
|
||||||
options.setEnvironment(env);
|
|
||||||
options.setTracesSampleRate(1.0);
|
|
||||||
options.setDebug(false);
|
|
||||||
options.setRelease(version);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("dev".equals(env)) {
|
|
||||||
Configurator.setLevel("net.dv8tion.jda", Level.DEBUG);
|
|
||||||
Configurator.setLevel("dev.arbjerg.lavalink", Level.DEBUG);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = new App(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public App(Settings settings) {
|
|
||||||
this.settings = settings;
|
|
||||||
startup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startup() {
|
|
||||||
String token = settings.get("NETNEUTRALITY_BOT_TOKEN");
|
|
||||||
if (token == null) {
|
|
||||||
throw new RuntimeException("No bot token provided, exiting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
buildContext();
|
|
||||||
|
|
||||||
try {
|
|
||||||
context.start();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogHandler.log(e);
|
|
||||||
// Roll back the components that did start so no non-daemon thread keeps the JVM alive.
|
|
||||||
context.stop();
|
|
||||||
throw new IllegalStateException("Startup failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers domain services and remaining startup/shutdown adapters with the
|
|
||||||
* {@link ApplicationContext}. Ascending {@link Lifecycle#priority()} drives startup order; the
|
|
||||||
* context stops components in the exact reverse order.
|
|
||||||
*/
|
|
||||||
private void buildContext() {
|
|
||||||
// Config first so SettingsProvider is resolvable for constructor injection.
|
|
||||||
context.register((SettingsProvider) settings);
|
|
||||||
|
|
||||||
// Allow ApplicationContext itself to be injected into components that need it.
|
|
||||||
context.register(context);
|
|
||||||
|
|
||||||
// Persistence: run migrations + build the SessionFactory; released last on shutdown.
|
|
||||||
SessionManager sessions = context.register(new SessionManager(settings));
|
|
||||||
GuildRepository guildRepository = new GuildRepository(sessions);
|
|
||||||
BlacklistRepository blacklistRepository = new BlacklistRepository(sessions);
|
|
||||||
GuildSettingRepository guildSettingRepository = new GuildSettingRepository(sessions);
|
|
||||||
|
|
||||||
// Executors: no explicit start today; shut down after music players are disconnected.
|
|
||||||
ExecutorProvider executors = context.register(new ThreadManager());
|
|
||||||
ChannelMessagePurger.bind(executors);
|
|
||||||
|
|
||||||
// Scheduled tasks (priority 55): discovers and schedules all ScheduledTask implementations
|
|
||||||
// on start; cancelled early on shutdown, before executors.
|
|
||||||
context.register(new ScheduledTaskManager(executors, context));
|
|
||||||
|
|
||||||
// Command registry (priority 30).
|
|
||||||
CommandRegistry commands = context.register(new CommandManager(context));
|
|
||||||
|
|
||||||
// Task registry (priority 30).
|
|
||||||
TaskManager tasks = context.register(new TaskManagerImpl(context, new TaskRepository(sessions)));
|
|
||||||
|
|
||||||
// Per-guild service cache (not a Lifecycle component).
|
|
||||||
GuildServiceProvider guildServices = context.register(
|
|
||||||
new GuildServiceProviderImpl(guildRepository, blacklistRepository, guildSettingRepository));
|
|
||||||
LogHandler.bind(guildServices);
|
|
||||||
UserFunction.bind(guildServices);
|
|
||||||
DisabledCommandPrecondition.bind(guildServices);
|
|
||||||
|
|
||||||
// Music sources (priority 40): initialise the shared player manager; disconnect on shutdown.
|
|
||||||
music = context.register(new MusicServiceImpl(settings, guildServices, tasks, executors));
|
|
||||||
BotState.bind(guildServices, music);
|
|
||||||
|
|
||||||
// JDA gateway; disconnected on shutdown.
|
|
||||||
context.register(lifecycle(60, this::initJDA, this::disconnect));
|
|
||||||
|
|
||||||
// Register slash commands, attach the event handler and set the presence once JDA is ready.
|
|
||||||
context.register(lifecycle(65, () -> {
|
|
||||||
commands.registerSlashCommands(jda);
|
|
||||||
jda.addEventListener(new EventsHandler(settings, guildServices, commands, tasks));
|
|
||||||
jda.getPresence().setActivity(Activity.watching("you"));
|
|
||||||
}, () -> { }));
|
|
||||||
|
|
||||||
// Web API (priority 70) — Lifecycle with graceful stop.
|
|
||||||
context.register(new WebApi(settings, guildServices, music, this::getJDA, this::shutdown));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Lifecycle lifecycle(int priority, ThrowingRunnable start, Runnable stop) {
|
|
||||||
return new Lifecycle() {
|
|
||||||
@Override
|
|
||||||
public void start() throws Exception {
|
|
||||||
start.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
stop.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int priority() {
|
|
||||||
return priority;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
private interface ThrowingRunnable {
|
|
||||||
void run() throws Exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static App getInstance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initJDA() {
|
|
||||||
String token = settings.get("NETNEUTRALITY_BOT_TOKEN");
|
|
||||||
if (token == null) {
|
|
||||||
SessionManager sessions = context.get(SessionManager.class);
|
|
||||||
if (sessions != null) {
|
|
||||||
sessions.stop();
|
|
||||||
}
|
|
||||||
throw new RuntimeException("No bot token provided, exiting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
jda = JDABuilder
|
|
||||||
.createDefault(token)
|
|
||||||
.enableIntents(
|
|
||||||
GatewayIntent.GUILD_MEMBERS,
|
|
||||||
GatewayIntent.GUILD_MESSAGE_REACTIONS,
|
|
||||||
GatewayIntent.GUILD_VOICE_STATES,
|
|
||||||
GatewayIntent.MESSAGE_CONTENT)
|
|
||||||
// Voice state/server updates are handed to Lavalink instead of JDA's audio stack;
|
|
||||||
// DAVE encryption is handled by the node.
|
|
||||||
.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(music.getClient()))
|
|
||||||
.setActivity(Activity.watching("me load..."))
|
|
||||||
.build();
|
|
||||||
jda.awaitReady();
|
|
||||||
} catch (InvalidTokenException | IllegalArgumentException | InterruptedException e) {
|
|
||||||
if (e instanceof InterruptedException) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
LogHandler.log(e);
|
|
||||||
throw new IllegalStateException("Unable to connect to Discord", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void shutdown() {
|
|
||||||
logger.info("[Net Neutrality] Shutting down...");
|
|
||||||
context.stop();
|
|
||||||
logger.info("[Net Neutrality] Shutdown complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
public JDA getJDA() {
|
|
||||||
return jda;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disconnect() {
|
|
||||||
if (jda != null) {
|
|
||||||
jda.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package me.bobert.netneutrality;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import io.sentry.Sentry;
|
||||||
|
import me.bobert.netneutrality.core.LogHandler;
|
||||||
|
import me.bobert.netneutrality.core.command.CommandManager;
|
||||||
|
import me.bobert.netneutrality.core.db.SessionManager;
|
||||||
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
|
import me.bobert.netneutrality.core.thread.ThreadManager;
|
||||||
|
import me.bobert.netneutrality.hooks.EventsHandler;
|
||||||
|
import me.bobert.netneutrality.http.server.WebApi;
|
||||||
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
|
import me.bobert.netneutrality.thread.ShutdownThread;
|
||||||
|
import me.bobert.netneutrality.utils.Settings;
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
|
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
|
||||||
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
|
import org.apache.logging.log4j.Level;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.apache.logging.log4j.core.config.Configurator;
|
||||||
|
|
||||||
|
public class NetNeutrality {
|
||||||
|
private static final Logger logger = LogManager.getLogger(NetNeutrality.class);
|
||||||
|
public static final String VERSION = Settings.getVersion();
|
||||||
|
private static NetNeutrality instance;
|
||||||
|
private static JDA jda;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("Starting NetNeutrality v" + VERSION);
|
||||||
|
|
||||||
|
String env = Settings.getInstance().get("NETNEUTRALITY_BOT_ENV", "dev");
|
||||||
|
String sentryDsn = Settings.getInstance().get("NETNEUTRALITY_SENTRY_DSN");
|
||||||
|
if (sentryDsn != null) {
|
||||||
|
logger.info("Found Sentry DSN in config, setting up the SDK..");
|
||||||
|
|
||||||
|
Sentry.init(options -> {
|
||||||
|
options.setDsn(sentryDsn);
|
||||||
|
options.setEnvironment(env);
|
||||||
|
options.setTracesSampleRate(1.0);
|
||||||
|
options.setDebug(false);
|
||||||
|
options.setRelease(VERSION);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("dev".equals(env)) {
|
||||||
|
Configurator.setLevel("net.dv8tion.jda", Level.DEBUG);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = new NetNeutrality();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetNeutrality() {
|
||||||
|
startup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startup() {
|
||||||
|
String token = Settings.getInstance().get("NETNEUTRALITY_BOT_TOKEN");
|
||||||
|
if (token == null) {
|
||||||
|
throw new RuntimeException("No bot token provided, exiting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseMigration.migrate();
|
||||||
|
SessionManager.getSessionFactory();
|
||||||
|
CommandManager.init();
|
||||||
|
MusicManager.initPlayerManager();
|
||||||
|
TaskManager.getInstance().init();
|
||||||
|
initJDA();
|
||||||
|
CommandManager.registerSlashCommands(jda);
|
||||||
|
jda.addEventListener(new EventsHandler());
|
||||||
|
new WebApi(Integer.parseInt(Settings.getInstance().get("NETNEUTRALITY_API_PORT", "9000")));
|
||||||
|
jda.getPresence().setActivity(Activity.watching("you"));
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(new ShutdownThread(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NetNeutrality getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initJDA() {
|
||||||
|
String token = Settings.getInstance().get("NETNEUTRALITY_BOT_TOKEN");
|
||||||
|
if (token == null) {
|
||||||
|
SessionManager.shutdown();
|
||||||
|
throw new RuntimeException("No bot token provided, exiting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jda = JDABuilder
|
||||||
|
.createDefault(token)
|
||||||
|
.enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_VOICE_STATES, GatewayIntent.MESSAGE_CONTENT)
|
||||||
|
.setActivity(Activity.watching("me load..."))
|
||||||
|
.build();
|
||||||
|
jda.awaitReady();
|
||||||
|
} catch (InvalidTokenException | IllegalArgumentException | InterruptedException e) {
|
||||||
|
LogHandler.log(e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
System.out.println("[Net Neutrality] Shutting down...");
|
||||||
|
MusicManager.disconnectPlayers();
|
||||||
|
ThreadManager.shutdownExecutor();
|
||||||
|
disconnect();
|
||||||
|
SessionManager.shutdown();
|
||||||
|
System.out.println("[Net Neutrality] Shutdown complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
public JDA getJDA() {
|
||||||
|
return jda;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disconnect() {
|
||||||
|
if (jda != null) {
|
||||||
|
jda.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package me.bobert.netneutrality.bootstrap;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A small hand-rolled application container that owns wired instances and drives ordered
|
|
||||||
* startup/shutdown.
|
|
||||||
*
|
|
||||||
* <p>Components implementing {@link Lifecycle} are started in ascending {@link Lifecycle#priority()}
|
|
||||||
* order and stopped in the exact reverse order, giving symmetric, deterministic startup and
|
|
||||||
* shutdown without any DI framework.
|
|
||||||
*/
|
|
||||||
public final class ApplicationContext {
|
|
||||||
private static final Logger logger = LogManager.getLogger(ApplicationContext.class);
|
|
||||||
|
|
||||||
private final List<Lifecycle> components = new ArrayList<>();
|
|
||||||
private final Map<Class<?>, Object> instances = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a component with the context. If the component implements {@link Lifecycle} it will
|
|
||||||
* participate in ordered start/stop. The instance is also indexed by its concrete type and can
|
|
||||||
* be resolved later via {@link #get(Class)}.
|
|
||||||
*
|
|
||||||
* @param component the component to register
|
|
||||||
* @param <T> the component type
|
|
||||||
* @return the same component, for fluent wiring
|
|
||||||
*/
|
|
||||||
public <T> T register(T component) {
|
|
||||||
if (component instanceof Lifecycle lifecycle) {
|
|
||||||
components.add(lifecycle);
|
|
||||||
}
|
|
||||||
instances.put(component.getClass(), component);
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a previously registered instance assignable to the given type.
|
|
||||||
*
|
|
||||||
* @param type the requested type
|
|
||||||
* @param <T> the requested type
|
|
||||||
* @return the matching instance, or {@code null} if none was registered
|
|
||||||
*/
|
|
||||||
public <T> T get(Class<T> type) {
|
|
||||||
Object direct = instances.get(type);
|
|
||||||
if (direct != null) {
|
|
||||||
return type.cast(direct);
|
|
||||||
}
|
|
||||||
for (Object instance : instances.values()) {
|
|
||||||
if (type.isInstance(instance)) {
|
|
||||||
return type.cast(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts all registered {@link Lifecycle} components in ascending priority order.
|
|
||||||
*
|
|
||||||
* @throws Exception if any component fails to start
|
|
||||||
*/
|
|
||||||
public void start() throws Exception {
|
|
||||||
components.sort(Comparator.comparingInt(Lifecycle::priority));
|
|
||||||
for (Lifecycle component : components) {
|
|
||||||
logger.debug("Starting component {} (priority {})", component.name(), component.priority());
|
|
||||||
component.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops all registered {@link Lifecycle} components in the reverse of their start order. Failures
|
|
||||||
* are logged and do not prevent the remaining components from being stopped.
|
|
||||||
*/
|
|
||||||
public void stop() {
|
|
||||||
List<Lifecycle> reversed = new ArrayList<>(components);
|
|
||||||
reversed.sort(Comparator.comparingInt(Lifecycle::priority));
|
|
||||||
Collections.reverse(reversed);
|
|
||||||
for (Lifecycle component : reversed) {
|
|
||||||
try {
|
|
||||||
logger.debug("Stopping component {} (priority {})", component.name(), component.priority());
|
|
||||||
component.stop();
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Error while stopping component {}", component.name(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package me.bobert.netneutrality.bootstrap;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildService;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
|
||||||
import me.bobert.netneutrality.music.TrackScheduler;
|
|
||||||
import me.bobert.netneutrality.utils.Json;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class BotState {
|
|
||||||
public static boolean SAFE = false;
|
|
||||||
|
|
||||||
private static volatile GuildServiceProvider guildServices;
|
|
||||||
private static volatile MusicService musicService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds providers used by the optional state dump. Called once during bootstrap.
|
|
||||||
*
|
|
||||||
* @param guildServices the guild service provider
|
|
||||||
* @param musicService the music service
|
|
||||||
*/
|
|
||||||
public static void bind(GuildServiceProvider guildServices, MusicService musicService) {
|
|
||||||
BotState.guildServices = guildServices;
|
|
||||||
BotState.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: is it needed?
|
|
||||||
private static void saveGuildsState() {
|
|
||||||
if (guildServices == null || musicService == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, GuildService> services = guildServices.snapshot();
|
|
||||||
|
|
||||||
ObjectNode main = Json.object();
|
|
||||||
ArrayNode guildsArray = Json.array();
|
|
||||||
|
|
||||||
for (MusicManager manager : musicService.getManagers()) {
|
|
||||||
TrackScheduler trackScheduler = manager.getScheduler();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (GuildService service : services.values()) {
|
|
||||||
|
|
||||||
|
|
||||||
ObjectNode guild = Json.object();
|
|
||||||
ObjectNode music = Json.object();
|
|
||||||
guild.put("id", service.getGuildId());
|
|
||||||
|
|
||||||
// MessageChannel currentChannel = trackScheduler.getTextChannel();
|
|
||||||
// if (currentChannel == null) {
|
|
||||||
// music.put("current_channel", "null");
|
|
||||||
// } else {
|
|
||||||
// music.put("current_channel", currentChannel.getId());
|
|
||||||
// }
|
|
||||||
|
|
||||||
guild.set("music", music);
|
|
||||||
|
|
||||||
guildsArray.add(guild);
|
|
||||||
}
|
|
||||||
|
|
||||||
main.set("guilds", guildsArray);
|
|
||||||
|
|
||||||
File state = new File("state.json");
|
|
||||||
try {
|
|
||||||
if (!state.exists()) {
|
|
||||||
state.createNewFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
try (FileWriter writer = new FileWriter(state)) {
|
|
||||||
writer.write(main.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void dump() {
|
|
||||||
if (!SAFE) {
|
|
||||||
saveGuildsState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package me.bobert.netneutrality.bootstrap;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contract for a component whose lifecycle is owned and driven by the {@link ApplicationContext}.
|
|
||||||
*
|
|
||||||
* <p>Components are {@linkplain #start() started} in ascending {@link #priority()} order and
|
|
||||||
* {@linkplain #stop() stopped} in the exact reverse order, giving symmetric, deterministic startup
|
|
||||||
* and shutdown without any DI framework. A lower {@code priority()} means the component is started
|
|
||||||
* earlier and, consequently, stopped later.
|
|
||||||
*/
|
|
||||||
public interface Lifecycle {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Brings the component up. Called exactly once, during {@link ApplicationContext#start()}.
|
|
||||||
*
|
|
||||||
* @throws Exception if the component cannot be started; startup is aborted
|
|
||||||
*/
|
|
||||||
void start() throws Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tears the component down, releasing whatever {@link #start()} acquired. Called at most once,
|
|
||||||
* during {@link ApplicationContext#stop()}. Implementations should be null-safe / idempotent so
|
|
||||||
* they can cope with a component that never fully started.
|
|
||||||
*
|
|
||||||
* <p>The default is a no-op for components that hold no resources.
|
|
||||||
*
|
|
||||||
* @throws Exception if teardown fails; the context logs it and continues stopping the rest
|
|
||||||
*/
|
|
||||||
default void stop() throws Exception {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relative ordering weight. Lower values start earlier and stop later; equal values keep their
|
|
||||||
* registration order. Defaults to {@code 0}.
|
|
||||||
*
|
|
||||||
* @return the priority of this component
|
|
||||||
*/
|
|
||||||
default int priority() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Human-readable name used in lifecycle diagnostics/logging. Defaults to the simple class name.
|
|
||||||
*
|
|
||||||
* @return a short label identifying this component
|
|
||||||
*/
|
|
||||||
default String name() {
|
|
||||||
return getClass().getSimpleName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package me.bobert.netneutrality.bootstrap;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.reflections.Reflections;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class ReflectionHelper {
|
|
||||||
private static final Logger logger = LogManager.getLogger(ReflectionHelper.class);
|
|
||||||
private static final BiConsumer<Class<?>, Exception> DEFAULT_FAILURE = (aClass, e) -> logger.error("Failed to load class: {}", aClass.getName(), e);
|
|
||||||
|
|
||||||
public static <E> void loadClassesFromPackage(String packageName, Class<E> clazz, Consumer<E> onInstanciated, BiConsumer<Class<?>, Exception> onFailed) {
|
|
||||||
loadClassesFromPackage(packageName, clazz, null, onInstanciated, onFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers concrete subtypes of {@code clazz} under {@code packageName} and instantiates each
|
|
||||||
* one. When a non-null {@link ApplicationContext} is provided, prefers the public constructor
|
|
||||||
* with the most parameters whose every parameter type is resolvable via
|
|
||||||
* {@link ApplicationContext#get(Class)}; otherwise (or when no such constructor exists) falls
|
|
||||||
* back to the no-arg constructor.
|
|
||||||
*
|
|
||||||
* @param packageName root package to scan
|
|
||||||
* @param clazz base type of classes to load
|
|
||||||
* @param context optional DI context used for constructor injection; {@code null} keeps
|
|
||||||
* legacy no-arg construction only
|
|
||||||
* @param onInstanciated callback invoked with each successfully created instance
|
|
||||||
* @param onFailed callback invoked when a class cannot be instantiated
|
|
||||||
* @param <E> the base type
|
|
||||||
*/
|
|
||||||
public static <E> void loadClassesFromPackage(String packageName, Class<E> clazz, ApplicationContext context, Consumer<E> onInstanciated, BiConsumer<Class<?>, Exception> onFailed) {
|
|
||||||
Set<Class<? extends E>> classes = new Reflections(packageName).getSubTypesOf(clazz);
|
|
||||||
for (Class<? extends E> s : classes) {
|
|
||||||
try {
|
|
||||||
if (Modifier.isAbstract(s.getModifiers())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
E instance = instantiate(s, context);
|
|
||||||
onInstanciated.accept(instance);
|
|
||||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
|
||||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
|
||||||
onFailed.accept(s, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiates {@code type} using context-based constructor injection when possible.
|
|
||||||
*
|
|
||||||
* <p>Selection algorithm: among public constructors, pick the one with the most parameters for
|
|
||||||
* which every parameter type resolves to a non-null value via {@code context.get(paramType)}.
|
|
||||||
* If no such constructor exists (or {@code context} is null), fall back to the public no-arg
|
|
||||||
* constructor.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <E> E instantiate(Class<? extends E> type, ApplicationContext context)
|
|
||||||
throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
|
|
||||||
if (context != null) {
|
|
||||||
Constructor<?> best = null;
|
|
||||||
Object[] bestArgs = null;
|
|
||||||
|
|
||||||
for (Constructor<?> ctor : type.getConstructors()) {
|
|
||||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
|
||||||
if (paramTypes.length == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object[] args = new Object[paramTypes.length];
|
|
||||||
boolean resolvable = true;
|
|
||||||
for (int i = 0; i < paramTypes.length; i++) {
|
|
||||||
Object dependency = context.get(paramTypes[i]);
|
|
||||||
if (dependency == null) {
|
|
||||||
resolvable = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
args[i] = dependency;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvable && (best == null || paramTypes.length > best.getParameterCount())) {
|
|
||||||
best = ctor;
|
|
||||||
bestArgs = args;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (best != null) {
|
|
||||||
return (E) best.newInstance(bestArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return type.getConstructor().newInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <E> void loadCommands(Class<E> clazz, Consumer<E> onInstanciated, BiConsumer<Class<?>, Exception> onFailed) {
|
|
||||||
loadClassesFromPackage("me.bobert.netneutrality", clazz, onInstanciated, onFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <E> void loadCommands(Class<E> clazz, Consumer<E> onInstanciated) {
|
|
||||||
loadClassesFromPackage("me.bobert.netneutrality", clazz, onInstanciated, DEFAULT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <E> void loadCommands(Class<E> clazz, ApplicationContext context, Consumer<E> onInstanciated) {
|
|
||||||
loadClassesFromPackage("me.bobert.netneutrality", clazz, context, onInstanciated, DEFAULT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <E> void loadTasks(Class<E> clazz, Consumer<E> onInstanciated, BiConsumer<Class<?>, Exception> onFailed) {
|
|
||||||
loadClassesFromPackage("me.bobert.netneutrality", clazz, onInstanciated, onFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <E> void loadTasks(Class<E> clazz, Consumer<E> onInstanciated) {
|
|
||||||
loadClassesFromPackage("me.bobert.netneutrality", clazz, onInstanciated, DEFAULT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <E> void loadTasks(Class<E> clazz, ApplicationContext context, Consumer<E> onInstanciated) {
|
|
||||||
loadClassesFromPackage("me.bobert.netneutrality", clazz, context, onInstanciated, DEFAULT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,33 +5,20 @@ package me.bobert.netneutrality.command.admin;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractAdminCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.AdminCommand;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.db.service.BlacklistEntry;
|
import me.bobert.netneutrality.db.entities.OBlacklist;
|
||||||
import me.bobert.netneutrality.db.service.BlacklistResult;
|
|
||||||
import me.bobert.netneutrality.db.service.BlacklistService;
|
import me.bobert.netneutrality.db.service.BlacklistService;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class BlacklistCommand extends AdminCommand {
|
|
||||||
private static final Logger logger = LogManager.getLogger(BlacklistCommand.class);
|
|
||||||
|
|
||||||
private final GuildServiceProvider guildServices;
|
|
||||||
|
|
||||||
public BlacklistCommand(GuildServiceProvider guildServices) {
|
|
||||||
Objects.requireNonNull(guildServices, "GuildServiceProvider must not be null");
|
|
||||||
this.guildServices = guildServices;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class BlacklistCommand extends AbstractAdminCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -68,16 +55,16 @@ public class BlacklistCommand extends AdminCommand {
|
|||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
String sub = context.getSubcommand();
|
String sub = context.getSubcommand();
|
||||||
String word = context.getOption("word", "", CommandOption::getAsString);
|
String word = context.getOption("word", "", CommandOption::getAsString);
|
||||||
BlacklistService service = guildServices.get(context.getGuild().getId()).getBlacklistService();
|
BlacklistService service = GuildService.get(context.getGuild()).getBlacklistService();
|
||||||
|
|
||||||
switch (sub) {
|
switch (sub) {
|
||||||
case "get" -> context.async("Retrieving list, please wait...", asyncMessage -> {
|
case "get" -> context.async("Retrieving list, please wait...", asyncMessage -> {
|
||||||
List<BlacklistEntry> blacklists = service.list();
|
List<OBlacklist> blacklists = service.getBlacklistForGuild();
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("List of blacklisted words for this server: \n");
|
sb.append("List of blacklisted words for this server: \n");
|
||||||
for (BlacklistEntry bl : blacklists) {
|
for (OBlacklist bl : blacklists) {
|
||||||
sb.append(bl.word()).append(" -> ").append(bl.enabled() ? "enabled" : "disabled").append("\n");
|
sb.append(bl.getWord()).append(" -> ").append(bl.getEnabled() ? "enabled" : "disabled").append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blacklists.isEmpty()) {
|
if (blacklists.isEmpty()) {
|
||||||
@@ -87,32 +74,13 @@ public class BlacklistCommand extends AdminCommand {
|
|||||||
asyncMessage.edit(sb.toString());
|
asyncMessage.edit(sb.toString());
|
||||||
});
|
});
|
||||||
case "add" -> {
|
case "add" -> {
|
||||||
try {
|
return service.add(context.getGuildUser(), word);
|
||||||
return toReply(service.add(context.getGuildUser().getId(), word), word);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
logger.error("Failed to add word to blacklist", e);
|
|
||||||
return CommandExecutionResult.warn("Failed to add word to blacklist: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "remove" -> {
|
case "remove" -> {
|
||||||
try {
|
return service.delete(word);
|
||||||
return toReply(service.remove(word), word);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
logger.error("Failed to remove word from blacklist", e);
|
|
||||||
return CommandExecutionResult.warn("Failed to remove word from blacklist: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
}
|
}
|
||||||
|
|
||||||
static CommandExecutionResult toReply(BlacklistResult result, String word) {
|
|
||||||
return switch (result) {
|
|
||||||
case ADDED, REENABLED -> CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
|
|
||||||
case ALREADY_EXISTS -> CommandExecutionResult.warn(String.format("The word [%s] is already in the blacklist.", word));
|
|
||||||
case REMOVED -> CommandExecutionResult.success("Removed the word [%s] from the blacklist.", word);
|
|
||||||
case NOT_FOUND -> CommandExecutionResult.warn(String.format("The word [%s] is not in the blacklist.", word));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package me.bobert.netneutrality.command.admin;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.AbstractCommand;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
import me.bobert.netneutrality.utils.ChannelMessagePurger;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
|
||||||
|
|
||||||
public class PurgeCommand extends AbstractCommand {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean needAdmin() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isGuildOnly() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCommandName() {
|
|
||||||
return "purge";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Delete messages from the channel you are currently on";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerCommandOptions(CommandOptionsBuilder builder) {
|
|
||||||
builder.addOptions(
|
|
||||||
new OptionData(OptionType.INTEGER, "to-purge", "Number of messages to delete (max 2500)", true)
|
|
||||||
.setRequiredRange(1, 2500));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandCategory getCategory() {
|
|
||||||
return CommandCategory.ADMINISTRATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
|
||||||
int toDelete = context.getOption("to-purge", 0, CommandOption::getAsInt);
|
|
||||||
context.ephemeral();
|
|
||||||
|
|
||||||
if (toDelete > 0) {
|
|
||||||
GuildMessageChannel messageChannel = (GuildMessageChannel) context.getChannel();
|
|
||||||
ChannelMessagePurger channelMessagePurger = ChannelMessagePurger.getPurger(messageChannel);
|
|
||||||
boolean lock = channelMessagePurger.canLockBeAcquired();
|
|
||||||
channelMessagePurger.purgeMessages(context, toDelete);
|
|
||||||
|
|
||||||
context.reply("Processing purge command.. This can take some time.");
|
|
||||||
|
|
||||||
if (!lock) {
|
|
||||||
context.reply("Another purge is currently in progress on this channel, please wait before purging again");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CommandExecutionResult.none();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,13 @@ package me.bobert.netneutrality.command.admin;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractAdminCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.AdminCommand;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.tasks.reaction.EmojiReactionRoleTask;
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
|
import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
@@ -22,17 +23,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
|
|
||||||
public class ReactionCommand extends AdminCommand {
|
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
|
|
||||||
public ReactionCommand(TaskManager taskManager) {
|
|
||||||
Objects.requireNonNull(taskManager, "TaskManager must not be null");
|
|
||||||
this.taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class ReactionCommand extends AbstractAdminCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -101,11 +93,11 @@ public class ReactionCommand extends AdminCommand {
|
|||||||
|
|
||||||
private CommandExecutionResult executeEmoji(CommandContext context, String guildId, String messageId, String sub) {
|
private CommandExecutionResult executeEmoji(CommandContext context, String guildId, String messageId, String sub) {
|
||||||
String id = String.format("%s:%s", guildId, messageId);
|
String id = String.format("%s:%s", guildId, messageId);
|
||||||
EmojiReactionRoleTask task = (EmojiReactionRoleTask) taskManager.getTask(id);
|
EmojiReactionRoleTask task = (EmojiReactionRoleTask) TaskManager.getInstance().getTask(id);
|
||||||
|
|
||||||
switch (sub) {
|
switch (sub) {
|
||||||
case "list" -> {
|
case "list" -> {
|
||||||
List<EmojiReactionRoleTask> tasks = taskManager.findTasksByPartialId(EmojiReactionRoleTask.class, guildId);
|
List<EmojiReactionRoleTask> tasks = TaskManager.getInstance().findTasksByPartialId(EmojiReactionRoleTask.class, guildId);
|
||||||
if (tasks.isEmpty()) {
|
if (tasks.isEmpty()) {
|
||||||
return CommandExecutionResult.info("No tasks found for this guild");
|
return CommandExecutionResult.info("No tasks found for this guild");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,40 +5,23 @@ package me.bobert.netneutrality.command.basic;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
import me.bobert.netneutrality.core.command.*;
|
||||||
import me.bobert.netneutrality.core.command.CommandContainer;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.AbstractCommand;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.tasks.trait.WithButton;
|
import me.bobert.netneutrality.core.tasks.BuiltInTask;
|
||||||
import me.bobert.netneutrality.core.command.CommandRegistry;
|
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
import me.bobert.netneutrality.tasks.help.BotInfo;
|
import me.bobert.netneutrality.tasks.help.BotInfo;
|
||||||
import me.bobert.netneutrality.tasks.help.CommandHelp;
|
import me.bobert.netneutrality.tasks.help.CommandHelp;
|
||||||
import me.bobert.netneutrality.tasks.help.ReportBug;
|
import me.bobert.netneutrality.tasks.help.ReportBug;
|
||||||
import me.bobert.netneutrality.utils.MessageFunction;
|
import me.bobert.netneutrality.utils.MessageFunction;
|
||||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
|
||||||
import net.dv8tion.jda.api.components.buttons.Button;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
|
import net.dv8tion.jda.api.interactions.components.ActionRow;
|
||||||
|
import net.dv8tion.jda.api.interactions.components.buttons.Button;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class HelpCommand extends AbstractCommand {
|
public class HelpCommand extends AbstractCommand {
|
||||||
|
|
||||||
private final CommandRegistry commands;
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
|
|
||||||
public HelpCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HelpCommand(CommandRegistry commands, TaskManager taskManager) {
|
|
||||||
this.commands = commands;
|
|
||||||
this.taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean needAdmin() {
|
public boolean needAdmin() {
|
||||||
return false;
|
return false;
|
||||||
@@ -78,15 +61,15 @@ public class HelpCommand extends AbstractCommand {
|
|||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
String command = context.getOption("command", null, CommandOption::getAsString);
|
String command = context.getOption("command", null, CommandOption::getAsString);
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
Button commandsBtn = createTaskButton(CommandHelp.class);
|
Button commands = BuiltInTask.createButton(CommandHelp.class);
|
||||||
Button info = createTaskButton(BotInfo.class);
|
Button info = BuiltInTask.createButton(BotInfo.class);
|
||||||
Button reportbug = createTaskButton(ReportBug.class);
|
Button reportbug = BuiltInTask.createButton(ReportBug.class);
|
||||||
ActionRow row = ActionRow.of(commandsBtn, info, reportbug);
|
ActionRow row = ActionRow.of(commands, info, reportbug);
|
||||||
context.replyEphemeral("Thanks for using the Net Neutrality Bot! What do you need help with?", List.of(row));
|
context.replyEphemeral("Thanks for using the Net Neutrality Bot! What do you need help with?", List.of(row));
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandContainer container = commands.getCommandContainer(command);
|
CommandContainer container = CommandManager.getCommandContainer(command);
|
||||||
if (container.getCommand() == null) {
|
if (container.getCommand() == null) {
|
||||||
return CommandExecutionResult.warn("Command does not exist");
|
return CommandExecutionResult.warn("Command does not exist");
|
||||||
}
|
}
|
||||||
@@ -94,12 +77,4 @@ public class HelpCommand extends AbstractCommand {
|
|||||||
context.replyEphemeral(MessageFunction.buildCommandHelpMessage(container));
|
context.replyEphemeral(MessageFunction.buildCommandHelpMessage(container));
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Button createTaskButton(Class<? extends WithButton> clazz) {
|
|
||||||
WithButton withButton = taskManager.findButtonTaskByClass(clazz);
|
|
||||||
if (withButton == null) {
|
|
||||||
throw new IllegalArgumentException("Task " + clazz.getName() + " was not registered by the task manager.");
|
|
||||||
}
|
|
||||||
return withButton.createButton();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,10 @@ package me.bobert.netneutrality.command.basic;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
import me.bobert.netneutrality.core.command.CommandCategory;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.AbstractCommand;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
|
||||||
public class PingCommand extends AbstractCommand {
|
public class PingCommand extends AbstractCommand {
|
||||||
|
|||||||
@@ -5,47 +5,25 @@ package me.bobert.netneutrality.command.bot;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractAdminCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.AdminCommand;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.db.GSettings;
|
import me.bobert.netneutrality.db.GSettings;
|
||||||
import me.bobert.netneutrality.db.service.GuildService;
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
import me.bobert.netneutrality.db.service.GuildSettingService;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||||
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class ConfigureCommand extends AdminCommand {
|
|
||||||
private static final Logger logger = LogManager.getLogger(ConfigureCommand.class);
|
|
||||||
|
|
||||||
private final GuildServiceProvider guildServices;
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public ConfigureCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfigureCommand(GuildServiceProvider guildServices, MusicService musicService) {
|
|
||||||
this.guildServices = guildServices;
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class ConfigureCommand extends AbstractAdminCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -78,186 +56,69 @@ public class ConfigureCommand extends AdminCommand {
|
|||||||
new SubcommandData("music-role", "Role needed to use music commands")
|
new SubcommandData("music-role", "Role needed to use music commands")
|
||||||
.addOption(OptionType.ROLE, "role", "Name of role", true),
|
.addOption(OptionType.ROLE, "role", "Name of role", true),
|
||||||
new SubcommandData("music-channel", "Only allow music commands on this channel")
|
new SubcommandData("music-channel", "Only allow music commands on this channel")
|
||||||
.addOption(OptionType.CHANNEL, "channel", "Leave empty to allow everywhere"),
|
.addOption(OptionType.CHANNEL, "channel", "Leave empty to allow everywhere")
|
||||||
new SubcommandData("music-max-playlist-size", "Playlist size above which confirmation is asked before queueing")
|
|
||||||
.addOptions(new OptionData(OptionType.INTEGER, "size", "Number of tracks", true)
|
|
||||||
.setRequiredRange(1, 1000)),
|
|
||||||
new SubcommandData("music-dj-role", "Role that may control music playback without being the current DJ")
|
|
||||||
.addOption(OptionType.ROLE, "role", "Leave empty to clear"),
|
|
||||||
new SubcommandData("music-vote-skip-threshold", "Percentage of listeners required to approve a vote-skip")
|
|
||||||
.addOptions(new OptionData(OptionType.INTEGER, "percent", "Percentage (1-100)", true)
|
|
||||||
.setRequiredRange(1, 100)),
|
|
||||||
new SubcommandData("music-idle-timeout", "Minutes of inactivity before the bot auto-disconnects")
|
|
||||||
.addOptions(new OptionData(OptionType.INTEGER, "minutes", "Number of minutes", true)
|
|
||||||
.setRequiredRange(1, 1440)),
|
|
||||||
new SubcommandData("command-prefix", "Override the bot's message command prefix on this server")
|
|
||||||
.addOption(OptionType.STRING, "prefix", "Leave empty to use the bot-wide default"),
|
|
||||||
new SubcommandData("admin-role", "Role granted admin permissions, in addition to Discord permissions")
|
|
||||||
.addOption(OptionType.ROLE, "role", "Leave empty to clear"),
|
|
||||||
new SubcommandData("disabled-commands", "Comma-separated list of command names disabled on this server")
|
|
||||||
.addOption(OptionType.STRING, "commands", "Leave empty to re-enable everything"),
|
|
||||||
new SubcommandData("disable-command", "Disable a single command on this server")
|
|
||||||
.addOption(OptionType.STRING, "name", "Name of the command to disable", true),
|
|
||||||
new SubcommandData("enable-command", "Re-enable a single command on this server")
|
|
||||||
.addOption(OptionType.STRING, "name", "Name of the command to re-enable", true),
|
|
||||||
new SubcommandData("log-level", "Minimum severity of events sent to the log channel")
|
|
||||||
.addOptions(new OptionData(OptionType.STRING, "level", "Severity level", true)
|
|
||||||
.addChoice("ERROR", "ERROR").addChoice("WARN", "WARN").addChoice("INFO", "INFO")),
|
|
||||||
new SubcommandData("locale", "Preferred locale for the bot's replies on this server")
|
|
||||||
.addOption(OptionType.STRING, "locale", "Locale code, e.g. en, fr", true),
|
|
||||||
new SubcommandData("timezone", "Timezone used to display timestamps on this server")
|
|
||||||
.addOption(OptionType.STRING, "timezone", "Timezone id, e.g. UTC, Europe/Paris", true),
|
|
||||||
new SubcommandData("music-search-engine", "Default search engine used when a play query is not a direct url")
|
|
||||||
.addOptions(new OptionData(OptionType.STRING, "engine", "Search engine", true)
|
|
||||||
.addChoice("YouTube", "YOUTUBE").addChoice("YouTube Music", "YTMUSIC").addChoice("SoundCloud", "SOUNDCLOUD"))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
try {
|
|
||||||
return configure(context);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
logger.error("Failed to update configuration", e);
|
|
||||||
return CommandExecutionResult.warn("Failed to update configuration: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandExecutionResult configure(CommandContext context) {
|
|
||||||
String sub = context.getSubcommand();
|
String sub = context.getSubcommand();
|
||||||
GuildService guildService = guildServices.get(context.getGuild().getId());
|
GuildService guildService = GuildService.get(context.getGuild().getId());
|
||||||
GuildSettingService settingService = guildService.getGuildSettingService();
|
|
||||||
context.ephemeral();
|
context.ephemeral();
|
||||||
|
|
||||||
return switch (sub) {
|
if ("music-role".equals(sub)) {
|
||||||
case "music-role" -> configureMusicRole(context, settingService);
|
|
||||||
case "music-channel" -> configureMusicChannel(context, settingService);
|
|
||||||
case "music-volume" -> configureMusicVolume(context, settingService);
|
|
||||||
case "error-logging" -> configureErrorLogging(context, guildService);
|
|
||||||
case "music-max-playlist-size" -> configureIntSetting(context, settingService, GSettings.MUSIC_MAXPLAYLISTSIZE, "size", "Maximum playlist size before confirmation is now %d");
|
|
||||||
case "music-dj-role" -> configureRoleSetting(context, settingService, GSettings.MUSIC_DJ_ROLE, "No dedicated DJ role is set anymore", "Successfully set %s as the DJ role");
|
|
||||||
case "music-vote-skip-threshold" -> configureIntSetting(context, settingService, GSettings.MUSIC_VOTE_SKIP_THRESHOLD, "percent", "Vote-skip threshold is now %d%%");
|
|
||||||
case "music-idle-timeout" -> configureIntSetting(context, settingService, GSettings.MUSIC_IDLE_TIMEOUT, "minutes", "Idle disconnect timeout is now %d minutes");
|
|
||||||
case "command-prefix" -> configureStringSetting(context, settingService, GSettings.COMMAND_PREFIX, "prefix", "Command prefix is now reset to the bot-wide default", "Command prefix is now set to %s");
|
|
||||||
case "admin-role" -> configureRoleSetting(context, settingService, GSettings.ADMIN_ROLE, "No dedicated admin role is set anymore", "Successfully set %s as the admin role");
|
|
||||||
case "disabled-commands" -> configureStringSetting(context, settingService, GSettings.DISABLED_COMMANDS, "commands", "All commands are now re-enabled", "Disabled commands are now: %s");
|
|
||||||
case "disable-command" -> toggleDisabledCommand(context, settingService, true);
|
|
||||||
case "enable-command" -> toggleDisabledCommand(context, settingService, false);
|
|
||||||
case "log-level" -> configureStringSetting(context, settingService, GSettings.LOG_LEVEL, "level", "Log level reset to default", "Log level is now %s");
|
|
||||||
case "locale" -> configureStringSetting(context, settingService, GSettings.LOCALE, "locale", "Locale reset to default", "Locale is now %s");
|
|
||||||
case "timezone" -> configureStringSetting(context, settingService, GSettings.TIMEZONE, "timezone", "Timezone reset to default", "Timezone is now %s");
|
|
||||||
case "music-search-engine" -> configureStringSetting(context, settingService, GSettings.MUSIC_SEARCH_ENGINE, "engine", "Default search engine reset to YouTube", "Default search engine is now %s");
|
|
||||||
case "show-all" -> showAll(context, settingService);
|
|
||||||
default -> CommandExecutionResult.error("Unknown subcommand: " + sub);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandExecutionResult configureIntSetting(CommandContext context, GuildSettingService settingService, GSettings setting, String optionName, String successFormat) {
|
|
||||||
int value = context.getOption(optionName, Integer.parseInt(setting.getDefaultValue()), CommandOption::getAsInt);
|
|
||||||
settingService.set(setting, String.valueOf(value));
|
|
||||||
|
|
||||||
return CommandExecutionResult.success(successFormat, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandExecutionResult configureStringSetting(CommandContext context, GuildSettingService settingService, GSettings setting, String optionName, String clearedMessage, String successFormat) {
|
|
||||||
String value = context.getOption(optionName, "", CommandOption::getAsString);
|
|
||||||
|
|
||||||
if (value.isEmpty()) {
|
|
||||||
settingService.set(setting, null);
|
|
||||||
return CommandExecutionResult.success(clearedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
settingService.set(setting, value);
|
|
||||||
return CommandExecutionResult.success(successFormat, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandExecutionResult toggleDisabledCommand(CommandContext context, GuildSettingService settingService, boolean disable) {
|
|
||||||
String name = context.getOption("name", "", CommandOption::getAsString).trim();
|
|
||||||
|
|
||||||
Set<String> disabledCommands = Arrays.stream(settingService.getOrDefault(GSettings.DISABLED_COMMANDS).split(","))
|
|
||||||
.map(String::trim)
|
|
||||||
.filter(value -> !value.isEmpty())
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
|
|
||||||
if (disable) {
|
|
||||||
if (disabledCommands.stream().noneMatch(value -> value.equalsIgnoreCase(name))) {
|
|
||||||
disabledCommands.add(name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
disabledCommands.removeIf(value -> value.equalsIgnoreCase(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
settingService.set(GSettings.DISABLED_COMMANDS, disabledCommands.isEmpty() ? null : String.join(",", disabledCommands));
|
|
||||||
|
|
||||||
return CommandExecutionResult.success(disable ? "Command %s is now disabled on this server" : "Command %s is now re-enabled on this server", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandExecutionResult configureRoleSetting(CommandContext context, GuildSettingService settingService, GSettings setting, String clearedMessage, String successFormat) {
|
|
||||||
Role role = context.getOption("role", CommandOption::getAsRole);
|
|
||||||
|
|
||||||
if (role == null) {
|
|
||||||
settingService.set(setting, null);
|
|
||||||
return CommandExecutionResult.success(clearedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
settingService.set(setting, role.getId());
|
|
||||||
return CommandExecutionResult.success(successFormat, role.getAsMention());
|
|
||||||
}
|
|
||||||
|
|
||||||
private CommandExecutionResult configureMusicRole(CommandContext context, GuildSettingService settingService) {
|
|
||||||
Role role = context.getOption("role", CommandOption::getAsRole);
|
Role role = context.getOption("role", CommandOption::getAsRole);
|
||||||
assert role != null;
|
assert role != null;
|
||||||
|
|
||||||
if (role.getPosition() == -1) {
|
if (role.getPosition() == -1) {
|
||||||
settingService.set(GSettings.MUSIC_ROLE, null);
|
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null);
|
||||||
|
|
||||||
return CommandExecutionResult.success("Everyone can now use music commands");
|
return CommandExecutionResult.success("Everyone can now use music commands");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role.getGuild().getId().equals(context.getGuild().getId())) {
|
if (role.getGuild().getId().equals(context.getGuild().getId())) {
|
||||||
settingService.set(GSettings.MUSIC_ROLE, role.getId());
|
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, role.getId());
|
||||||
|
|
||||||
return CommandExecutionResult.success(String.format("Successfully set %s as the role needed for music command", role.getAsMention()));
|
return CommandExecutionResult.success(String.format("Successfully set %s as the role needed for music command", role.getAsMention()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.error("Unknown subcommand: music-role");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommandExecutionResult configureMusicChannel(CommandContext context, GuildSettingService settingService) {
|
if ("music-channel".equals(sub)) {
|
||||||
GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel);
|
GuildChannelUnion channel = context.getOption("channel", CommandOption::getAsChannel);
|
||||||
|
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
settingService.set(GSettings.MUSIC_CHANNEL, null);
|
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, null);
|
||||||
|
|
||||||
return CommandExecutionResult.success("No channel restrictions for music commands");
|
return CommandExecutionResult.success("No channel restrictions for music commands");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel instanceof TextChannel && channel.getGuild().getId().equals(context.getGuild().getId())) {
|
if (channel instanceof TextChannel) {
|
||||||
settingService.set(GSettings.MUSIC_CHANNEL, channel.getId());
|
if (channel.getGuild().getId().equals(context.getGuild().getId())) {
|
||||||
|
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_CHANNEL, channel.getId());
|
||||||
|
|
||||||
return CommandExecutionResult.success(String.format("Successfully set %s as the channel required to execute music command", channel.getAsMention()));
|
return CommandExecutionResult.success(String.format("Successfully set %s as the channel required to execute music command", channel.getAsMention()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.error("Unknown subcommand: music-channel");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommandExecutionResult configureMusicVolume(CommandContext context, GuildSettingService settingService) {
|
}
|
||||||
|
|
||||||
|
if ("music-volume".equals(sub)) {
|
||||||
int volume = context.getOption("volume", 30, CommandOption::getAsInt);
|
int volume = context.getOption("volume", 30, CommandOption::getAsInt);
|
||||||
settingService.set(GSettings.MUSIC_VOLUME, String.valueOf(volume));
|
guildService.getGuildSettingService().setSettingValue(GSettings.MUSIC_VOLUME, String.valueOf(volume));
|
||||||
musicService.get(context.getGuild()).setVolume(volume);
|
MusicManager.get(context.getGuild()).setVolume(volume);
|
||||||
|
|
||||||
return CommandExecutionResult.success(String.format("Default volume for music player is now %d", volume));
|
return CommandExecutionResult.success(String.format("Default volume for music player is now %d", volume));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommandExecutionResult configureErrorLogging(CommandContext context, GuildService guildService) {
|
if ("error-logging".equals(sub)) {
|
||||||
boolean debug = context.getOption("debug", true, CommandOption::getAsBoolean);
|
boolean debug = context.getOption("debug", true, CommandOption::getAsBoolean);
|
||||||
guildService.updateDebugMode(debug);
|
guildService.updateDebugMode(debug);
|
||||||
|
|
||||||
return CommandExecutionResult.success("Error logging is now %s for this guild", (debug ? "enabled" : "disabled"));
|
return CommandExecutionResult.success("Error logging is now %s for this guild", (debug ? "enabled" : "disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommandExecutionResult showAll(CommandContext context, GuildSettingService settingService) {
|
if ("show-all".equals(sub)) {
|
||||||
Map<String, String> settings = settingService.snapshot();
|
Map<String, String> settings = guildService.getGuildSettingService().getSettings();
|
||||||
MessageCreateBuilder builder = new MessageCreateBuilder();
|
MessageCreateBuilder builder = new MessageCreateBuilder();
|
||||||
|
|
||||||
for (GSettings o : GSettings.values()) {
|
for (GSettings o : GSettings.values()) {
|
||||||
@@ -267,12 +128,13 @@ public class ConfigureCommand extends AdminCommand {
|
|||||||
value = settings.get(o.getName());
|
value = settings.get(o.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.addContent(o.getName()).addContent(" (").addContent(o.getDescription()).addContent("): ").addContent(value);
|
builder.addContent(o.getName()).addContent(": ").addContent(value);
|
||||||
builder.addContent("\n");
|
builder.addContent("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
context.replyEphemeral(builder.build());
|
context.replyEphemeral(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.success("Displayed current configuration");
|
return CommandExecutionResult.error("Unknown subcommand: " + sub);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,17 @@ package me.bobert.netneutrality.command.crypto;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import me.bobert.netneutrality.core.LogHandler;
|
import me.bobert.netneutrality.core.LogHandler;
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
import me.bobert.netneutrality.core.command.CommandCategory;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.AbstractCommand;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.http.WebModule;
|
import me.bobert.netneutrality.http.WebModule;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
@@ -70,7 +69,7 @@ public class CryptoPriceCommand extends AbstractCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
Locale usa = Locale.US;
|
Locale usa = new Locale("en", "US");
|
||||||
NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(usa);
|
NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(usa);
|
||||||
dollarFormat.setMaximumFractionDigits(24);
|
dollarFormat.setMaximumFractionDigits(24);
|
||||||
|
|
||||||
@@ -92,19 +91,14 @@ public class CryptoPriceCommand extends AbstractCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WebModule http = new WebModule.Builder(String.format(baseURL, symbol, finalCurrency)).cache(30, TimeUnit.MINUTES).build().request();
|
WebModule http = new WebModule.Builder(String.format(baseURL, symbol, finalCurrency)).cache(30, TimeUnit.MINUTES).build().request();
|
||||||
ObjectNode json = http.getAsObject();
|
JSONObject json = http.getAsObject();
|
||||||
|
|
||||||
if (json.isEmpty() || json.path("Response").asText("").equalsIgnoreCase("Error")) {
|
if (json.isEmpty() || (json.has("Response") && json.getString("Response").equalsIgnoreCase("Error"))) {
|
||||||
asyncMessage.edit("Crypto doesn't exist.. yet");
|
asyncMessage.edit("Crypto doesn't exist.. yet");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode priceNode = json.path(finalCurrency);
|
double value = json.getDouble(finalCurrency);
|
||||||
if (!priceNode.isNumber()) {
|
|
||||||
throw new IllegalStateException("Missing price for currency " + finalCurrency);
|
|
||||||
}
|
|
||||||
|
|
||||||
double value = priceNode.asDouble();
|
|
||||||
String price = dollarFormat.format(value);
|
String price = dollarFormat.format(value);
|
||||||
|
|
||||||
asyncMessage.edit(String.format("The current price of %s in %s is %s", symbol, finalCurrency, price));
|
asyncMessage.edit(String.format("The current price of %s in %s is %s", symbol, finalCurrency, price));
|
||||||
@@ -121,12 +115,12 @@ public class CryptoPriceCommand extends AbstractCommand {
|
|||||||
private boolean validateSymbol(String symbol) {
|
private boolean validateSymbol(String symbol) {
|
||||||
try {
|
try {
|
||||||
WebModule http = new WebModule.Builder(exchangeApi).get().cache(1, TimeUnit.DAYS).build().request();
|
WebModule http = new WebModule.Builder(exchangeApi).get().cache(1, TimeUnit.DAYS).build().request();
|
||||||
ObjectNode json = http.getAsObject();
|
JSONObject json = http.getAsObject();
|
||||||
|
|
||||||
JsonNode pairs = json.path("Data")
|
JSONObject pairs = json.getJSONObject("Data")
|
||||||
.path("exchanges")
|
.getJSONObject("exchanges")
|
||||||
.path(exchange)
|
.getJSONObject(exchange)
|
||||||
.path("pairs");
|
.getJSONObject("pairs");
|
||||||
|
|
||||||
return pairs.has(symbol);
|
return pairs.has(symbol);
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
**/*.java
|
*.java
|
||||||
@@ -5,30 +5,19 @@ package me.bobert.netneutrality.command.git;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.context.command.SlashCommandContext;
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
import me.bobert.netneutrality.core.command.CommandCategory;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.AbstractCommand;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.utils.GiteaClient;
|
import me.bobert.netneutrality.http.WebModule;
|
||||||
import me.bobert.netneutrality.tasks.git.ReportIssueTask;
|
import me.bobert.netneutrality.tasks.git.ReportIssueTask;
|
||||||
|
import me.bobert.netneutrality.utils.Settings;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.concurrent.TimeUnit;
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
|
||||||
|
|
||||||
public class ReportIssueCommand extends AbstractCommand {
|
public class ReportIssueCommand extends AbstractCommand {
|
||||||
|
|
||||||
private final SettingsProvider settings;
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
|
|
||||||
public ReportIssueCommand(SettingsProvider settings, TaskManager taskManager) {
|
|
||||||
Objects.requireNonNull(settings, "SettingsProvider must not be null");
|
|
||||||
Objects.requireNonNull(taskManager, "TaskManager must not be null");
|
|
||||||
this.settings = settings;
|
|
||||||
this.taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean needAdmin() {
|
public boolean needAdmin() {
|
||||||
return false;
|
return false;
|
||||||
@@ -71,17 +60,16 @@ public class ReportIssueCommand extends AbstractCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
String host = settings.get("GITEA_HOST");
|
String gitlabHost = Settings.getInstance().get("GITLAB_HOST");
|
||||||
String token = settings.get("GITEA_TOKEN");
|
String gitlabToken = Settings.getInstance().get("GITLAB_TOKEN");
|
||||||
String owner = settings.get("GITEA_OWNER");
|
String projectId = Settings.getInstance().get("GITLAB_PROJECT_ID");
|
||||||
String repo = settings.get("GITEA_REPO");
|
|
||||||
|
|
||||||
if (host == null || token == null || owner == null || repo == null) {
|
if (gitlabHost == null || gitlabToken == null || projectId == null) {
|
||||||
return CommandExecutionResult.error("Command is not configured, failed to create issue.");
|
return CommandExecutionResult.error("Command is not configured, failed to create issue.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new GiteaClient(host, token, owner, repo).ping()) {
|
if (new WebModule.Builder(gitlabHost).ping().timeout(2, TimeUnit.SECONDS).build().request().isSuccess()) {
|
||||||
return CommandExecutionResult.task(new ReportIssueTask(taskManager, context, settings));
|
return CommandExecutionResult.task(new ReportIssueTask(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.error("Unable to create bug reports at this time, please try again later.");
|
return CommandExecutionResult.error("Unable to create bug reports at this time, please try again later.");
|
||||||
|
|||||||
@@ -6,31 +6,14 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
import me.bobert.netneutrality.core.command.CommandCategory;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
|
|
||||||
public class BitconnectCommand extends MusicCommand {
|
public class BitconnectCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public BitconnectCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BitconnectCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BitconnectCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -59,7 +42,7 @@ public class BitconnectCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ephemeral();
|
context.ephemeral();
|
||||||
context.deleteMessage();
|
context.deleteMessage();
|
||||||
|
|
||||||
|
|||||||
@@ -6,31 +6,14 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class ClearQueueCommand extends MusicCommand {
|
public class ClearQueueCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public ClearQueueCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClearQueueCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClearQueueCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean needAdmin() {
|
public boolean needAdmin() {
|
||||||
@@ -59,11 +42,11 @@ public class ClearQueueCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().clearQueue());
|
return manager.getQueueManager().clearQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
|
|||||||
@@ -6,32 +6,15 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MessageFunction;
|
import me.bobert.netneutrality.utils.MessageFunction;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class CurrentlyPlayingCommand extends MusicCommand {
|
public class CurrentlyPlayingCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public CurrentlyPlayingCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CurrentlyPlayingCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CurrentlyPlayingCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -55,10 +38,10 @@ public class CurrentlyPlayingCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
if (!manager.isCurrentlyPlaying()) {
|
if (!manager.isCurrentlyPlaying()) {
|
||||||
return CommandExecutionResult.info("Bot isn't playing any tracks right now");
|
return CommandExecutionResult.info("Bot isn't playing any tracks right now");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,31 +6,13 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class JoinCommand extends MusicCommand {
|
public class JoinCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public JoinCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JoinCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JoinCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -54,9 +36,7 @@ public class JoinCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
if (MusicUtils.connect(context)) {
|
||||||
|
|
||||||
if (MusicUtils.connect(context, manager)) {
|
|
||||||
context.reply("Joining current voice channel");
|
context.reply("Joining current voice channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,38 +6,14 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
/**
|
public class LeaveCommand extends AbstractMusicCommand {
|
||||||
* Leaves the voice channel for the current guild.
|
|
||||||
*
|
|
||||||
* <p>Representative command migrated to constructor injection of {@link MusicService} (and optional
|
|
||||||
* {@link GuildServiceProvider} for the {@link MusicCommand} base). The no-arg constructor remains as
|
|
||||||
* a fallback for non-DI loaders.
|
|
||||||
*/
|
|
||||||
public class LeaveCommand extends MusicCommand {
|
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public LeaveCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LeaveCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LeaveCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -61,9 +37,9 @@ public class LeaveCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
context.reply("Leaving voice channel and clearing queue");
|
context.reply("Leaving voice channel and clearing queue");
|
||||||
manager.disconnect();
|
manager.disconnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,32 +6,15 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
|
|
||||||
public class PlayCommand extends MusicCommand {
|
public class PlayCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public PlayCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -55,7 +38,7 @@ public class PlayCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
String query = context.getOption("query", CommandOption::getAsString);
|
String query = context.getOption("query", CommandOption::getAsString);
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MessageFunction;
|
import me.bobert.netneutrality.utils.MessageFunction;
|
||||||
@@ -20,22 +18,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
|
|
||||||
public class QueueCommand extends MusicCommand {
|
public class QueueCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public QueueCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueueCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueueCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -75,26 +58,26 @@ public class QueueCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
String sub = context.getSubcommand();
|
String sub = context.getSubcommand();
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
|
|
||||||
switch (sub) {
|
switch (sub) {
|
||||||
case "clear-all" -> {
|
case "clear-all" -> {
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().clearQueue());
|
return manager.getQueueManager().clearQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
case "delete-track" -> {
|
case "delete-track" -> {
|
||||||
int position = context.getOption("position", 0, CommandOption::getAsInt);
|
int position = context.getOption("position", 0, CommandOption::getAsInt);
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().removeFromQueue(position));
|
return manager.getQueueManager().removeFromQueue(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "show-queue" -> context.reply(MessageFunction.buildViewQueueMessage(manager));
|
case "show-queue" -> context.reply(MessageFunction.buildViewQueueMessage(manager));
|
||||||
|
|
||||||
case "shuffle" -> {
|
case "shuffle" -> {
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().shuffle());
|
return manager.getQueueManager().shuffle();
|
||||||
}
|
}
|
||||||
|
|
||||||
case "from-user" -> {
|
case "from-user" -> {
|
||||||
@@ -112,7 +95,7 @@ public class QueueCommand extends MusicCommand {
|
|||||||
return CommandExecutionResult.warn("The user isn't from this guild");
|
return CommandExecutionResult.warn("The user isn't from this guild");
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().removeTracksFromUser(member));
|
return manager.getQueueManager().removeTracksFromUser(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,37 +4,14 @@ package me.bobert.netneutrality.command.music;/*
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class ResumeCommand extends MusicCommand {
|
public class ResumeCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public ResumeCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResumeCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResumeCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return true;
|
return true;
|
||||||
@@ -57,10 +34,10 @@ public class ResumeCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
return CommandExecutionResult.from(manager.resume());
|
return manager.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
|
|||||||
@@ -6,31 +6,14 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class ShuffleCommand extends MusicCommand {
|
public class ShuffleCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public ShuffleCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShuffleCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShuffleCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return true;
|
return true;
|
||||||
@@ -53,11 +36,11 @@ public class ShuffleCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().shuffle());
|
return manager.getQueueManager().shuffle();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
@@ -19,22 +17,7 @@ import me.bobert.netneutrality.utils.UserFunction;
|
|||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
|
||||||
public class SkipCommand extends MusicCommand {
|
public class SkipCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public SkipCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SkipCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SkipCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -59,15 +42,15 @@ public class SkipCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
int skips = context.getOption("skip", 1, CommandOption::getAsInt);
|
int skips = context.getOption("skip", 1, CommandOption::getAsInt);
|
||||||
if (UserFunction.isAdmin(context) || MusicUtils.isUserDj(context, manager)) {
|
if (UserFunction.isAdmin(context) || MusicUtils.isUserDj(context)) {
|
||||||
return CommandExecutionResult.from(manager.getQueueManager().skip(skips));
|
return manager.getQueueManager().skip(skips);
|
||||||
} else {
|
} else {
|
||||||
return CommandExecutionResult.from(manager.voteSkip(context.getGuildUser()));
|
return manager.voteSkip(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,32 +6,15 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
import me.bobert.netneutrality.utils.UserFunction;
|
import me.bobert.netneutrality.utils.UserFunction;
|
||||||
|
|
||||||
public class StopCommand extends MusicCommand {
|
public class StopCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public StopCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StopCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StopCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -55,10 +38,10 @@ public class StopCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
if (UserFunction.isAdmin(context)) {
|
if (UserFunction.isAdmin(context)) {
|
||||||
manager.stopTrack();
|
manager.stopTrack();
|
||||||
context.reply("Stopping current track");
|
context.reply("Stopping current track");
|
||||||
|
|||||||
@@ -5,32 +5,15 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MessageFunction;
|
import me.bobert.netneutrality.utils.MessageFunction;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class ViewQueueCommand extends MusicCommand {
|
public class ViewQueueCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public ViewQueueCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ViewQueueCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ViewQueueCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -54,10 +37,10 @@ public class ViewQueueCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
context.ack();
|
context.ack();
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
int queueSize = manager.getQueueSize();
|
int queueSize = manager.getQueueSize();
|
||||||
if (queueSize == 0) {
|
if (queueSize == 0) {
|
||||||
return CommandExecutionResult.warn("Queue is empty!");
|
return CommandExecutionResult.warn("Queue is empty!");
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
@@ -19,22 +17,7 @@ import me.bobert.netneutrality.utils.UserFunction;
|
|||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
|
||||||
public class VolumeCommand extends MusicCommand {
|
public class VolumeCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public VolumeCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VolumeCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VolumeCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -59,11 +42,11 @@ public class VolumeCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
int volume = context.getOption("volume", 30, CommandOption::getAsInt);
|
int volume = context.getOption("volume", 30, CommandOption::getAsInt);
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
if (UserFunction.isAdmin(context) || MusicUtils.isUserDj(context, manager)) {
|
if (UserFunction.isAdmin(context) || MusicUtils.isUserDj(context)) {
|
||||||
manager.setVolume(volume);
|
manager.setVolume(volume);
|
||||||
return CommandExecutionResult.success(String.format("Volume set to %s", volume));
|
return CommandExecutionResult.success(String.format("Volume set to %s", volume));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,31 +5,14 @@ package me.bobert.netneutrality.command.music;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMusicCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.impl.MusicCommand;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.music.MusicService;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.music.MusicManager;
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
|
|
||||||
public class VoteSkipCommand extends MusicCommand {
|
public class VoteSkipCommand extends AbstractMusicCommand {
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
|
|
||||||
public VoteSkipCommand() {
|
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VoteSkipCommand(MusicService musicService) {
|
|
||||||
this(musicService, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public VoteSkipCommand(MusicService musicService, GuildServiceProvider guildServices) {
|
|
||||||
super(guildServices);
|
|
||||||
this.musicService = musicService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -53,10 +36,10 @@ public class VoteSkipCommand extends MusicCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(CommandContext context) {
|
public CommandExecutionResult execute(CommandContext context) {
|
||||||
MusicManager manager = musicService.get(context.getGuild());
|
MusicManager manager = MusicManager.get(context.getGuild());
|
||||||
|
|
||||||
if (MusicUtils.checkIfConnected(context, manager)) {
|
if (MusicUtils.checkIfConnected(context)) {
|
||||||
return CommandExecutionResult.from(manager.voteSkip(context.getGuildUser()));
|
return manager.voteSkip(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
|
|||||||
@@ -6,35 +6,25 @@ package me.bobert.netneutrality.command.react;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMessageMenuCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.impl.MessageMenuCommand;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.tasks.reaction.EmojiReactionRoleTask;
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
|
import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask;
|
||||||
import me.bobert.netneutrality.tasks.reaction.EmojiRoleFirstStep;
|
import me.bobert.netneutrality.tasks.reaction.EmojiRoleFirstStep;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class CreateEmojiReaction extends MessageMenuCommand {
|
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
|
|
||||||
public CreateEmojiReaction(TaskManager taskManager) {
|
|
||||||
Objects.requireNonNull(taskManager, "TaskManager must not be null");
|
|
||||||
this.taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class CreateEmojiReaction extends AbstractMessageMenuCommand {
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(Message target, CommandContext context) {
|
public CommandExecutionResult execute(Message target, CommandContext context) {
|
||||||
EmojiReactionRoleTask task = taskManager.findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null);
|
EmojiReactionRoleTask task = TaskManager.getInstance().findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null);
|
||||||
|
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
context.replyEphemeral("This message is already registered with the bot, please use the modify command instead");
|
context.replyEphemeral("This message is already registered with the bot, please use the modify command instead");
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.task(new EmojiRoleFirstStep(taskManager, target));
|
return CommandExecutionResult.task(new EmojiRoleFirstStep(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,35 +6,25 @@ package me.bobert.netneutrality.command.react;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.AbstractMessageMenuCommand;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.command.impl.MessageMenuCommand;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.tasks.reaction.EmojiReactionRoleTask;
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
|
import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask;
|
||||||
import me.bobert.netneutrality.tasks.reaction.EmojiRoleModify;
|
import me.bobert.netneutrality.tasks.reaction.EmojiRoleModify;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class ModifyEmojiReaction extends MessageMenuCommand {
|
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
|
|
||||||
public ModifyEmojiReaction(TaskManager taskManager) {
|
|
||||||
Objects.requireNonNull(taskManager, "TaskManager must not be null");
|
|
||||||
this.taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class ModifyEmojiReaction extends AbstractMessageMenuCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandExecutionResult execute(Message target, CommandContext context) {
|
public CommandExecutionResult execute(Message target, CommandContext context) {
|
||||||
EmojiReactionRoleTask task = taskManager.findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null);
|
EmojiReactionRoleTask task = TaskManager.getInstance().findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null);
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
context.replyEphemeral("This message is not registered with the bot, please use the create command instead");
|
context.replyEphemeral("This message is not registered with the bot, please use the create command instead");
|
||||||
return CommandExecutionResult.none();
|
return CommandExecutionResult.none();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CommandExecutionResult.task(new EmojiRoleModify(taskManager, target, task));
|
return CommandExecutionResult.task(new EmojiRoleModify(target, task));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+6
-5
@@ -1,16 +1,17 @@
|
|||||||
package me.bobert.netneutrality.core.context;
|
package me.bobert.netneutrality.context;
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
*
|
*
|
||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.components.MessageTopLevelComponent;
|
import me.bobert.netneutrality.core.context.AbstractContext;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.events.Event;
|
import net.dv8tion.jda.api.events.Event;
|
||||||
import net.dv8tion.jda.api.interactions.InteractionHook;
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
import net.dv8tion.jda.api.modals.Modal;
|
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
|
||||||
|
import net.dv8tion.jda.api.interactions.modals.Modal;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ public class AsyncContext extends AbstractContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (hook != null) {
|
if (hook != null) {
|
||||||
hook.sendMessage(messageCreateData).queue();
|
hook.sendMessage(messageCreateData).queue();
|
||||||
}
|
}
|
||||||
@@ -117,7 +118,7 @@ public class AsyncContext extends AbstractContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void edit(MessageEditData messageEditData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
public void edit(MessageEditData messageEditData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (hook != null) {
|
if (hook != null) {
|
||||||
hook.editOriginal(messageEditData).queue();
|
hook.editOriginal(messageEditData).queue();
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,11 @@ package me.bobert.netneutrality.context;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.context.AbstractContext;
|
import me.bobert.netneutrality.core.context.AbstractContext;
|
||||||
import me.bobert.netneutrality.core.context.AsyncContext;
|
|
||||||
import me.bobert.netneutrality.core.context.NoMessageContext;
|
import me.bobert.netneutrality.core.context.NoMessageContext;
|
||||||
import net.dv8tion.jda.api.components.MessageTopLevelComponent;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
||||||
import net.dv8tion.jda.api.events.Event;
|
import net.dv8tion.jda.api.events.Event;
|
||||||
|
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -44,7 +43,7 @@ public class MusicChannelContext extends AbstractContext implements NoMessageCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (getChannel().canTalk()) {
|
if (getChannel().canTalk()) {
|
||||||
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
|
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-184
@@ -9,12 +9,8 @@ import me.bobert.netneutrality.core.command.options.CommandOption;
|
|||||||
import me.bobert.netneutrality.core.command.options.MessageCommandOption;
|
import me.bobert.netneutrality.core.command.options.MessageCommandOption;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.context.MessageContext;
|
import me.bobert.netneutrality.core.context.MessageContext;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
import me.bobert.netneutrality.utils.Settings;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.*;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.Mentions;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
|
||||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||||
@@ -23,28 +19,16 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
|
|||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.StringJoiner;
|
import java.util.StringJoiner;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class MessageCommandContext extends CommandContext implements MessageContext {
|
public class MessageCommandContext extends CommandContext implements MessageContext {
|
||||||
private static final Pattern INTEGER_PATTERN = Pattern.compile("[-+]?\\d+");
|
|
||||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("[-+]?\\d+(\\.\\d+)?");
|
|
||||||
private static final Pattern MENTION_PATTERN = Pattern.compile("<(@[!&]?|#)\\d+>");
|
|
||||||
private static final Set<OptionType> MENTION_TYPES =
|
|
||||||
EnumSet.of(OptionType.USER, OptionType.ROLE, OptionType.MENTIONABLE, OptionType.CHANNEL);
|
|
||||||
private static final OptionData LEFTOVER_ARGUMENTS = new OptionData(OptionType.STRING, "arguments", "arguments");
|
|
||||||
|
|
||||||
private final MessageReceivedEvent event;
|
private final MessageReceivedEvent event;
|
||||||
private final Message message;
|
private final Message message;
|
||||||
|
|
||||||
private final SettingsProvider settings;
|
public MessageCommandContext(MessageReceivedEvent event) {
|
||||||
|
super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor());
|
||||||
public MessageCommandContext(MessageReceivedEvent event, SettingsProvider settings) {
|
|
||||||
super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor(), settings);
|
|
||||||
this.settings = settings;
|
|
||||||
this.event = event;
|
this.event = event;
|
||||||
this.message = event.getMessage();
|
this.message = event.getMessage();
|
||||||
}
|
}
|
||||||
@@ -78,48 +62,27 @@ public class MessageCommandContext extends CommandContext implements MessageCont
|
|||||||
int channelMentionIndex = 0;
|
int channelMentionIndex = 0;
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
|
||||||
for (int i = 0; i < commandOptions.size(); i++) {
|
for (OptionData data : commandOptions) {
|
||||||
OptionData data = commandOptions.get(i);
|
|
||||||
OptionType type = data.getType();
|
OptionType type = data.getType();
|
||||||
Set<ChannelType> requiredChannelTypes = data.getChannelTypes();
|
Set<ChannelType> requiredChannelTypes = data.getChannelTypes();
|
||||||
List<Command.Choice> choices = data.getChoices();
|
List<Command.Choice> choices = data.getChoices();
|
||||||
int position = index + 1;
|
|
||||||
Object value = null;
|
Object value = null;
|
||||||
|
|
||||||
if (index >= args.length) {
|
try {
|
||||||
if (data.isRequired()) {
|
|
||||||
invalidateContext(data, "missing required argument at index " + position);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case STRING -> {
|
case STRING -> {
|
||||||
String str;
|
String str = args[index];
|
||||||
if (i + 1 < commandOptions.size() && commandOptions.get(i + 1).getType() == OptionType.STRING) {
|
|
||||||
// The next option is also a string, so only the current argument belongs to this one
|
|
||||||
str = args[index++];
|
|
||||||
} else {
|
|
||||||
StringJoiner joiner = new StringJoiner(" ");
|
|
||||||
while (index < args.length && !stopsTextCapture(args[index], commandOptions, i)) {
|
|
||||||
joiner.add(args[index++]);
|
|
||||||
}
|
|
||||||
|
|
||||||
str = joiner.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (str.isEmpty()) {
|
|
||||||
if (data.isRequired()) {
|
|
||||||
invalidateContext(data, "missing required argument at index " + position);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if ((str.contains("<@") || str.contains("<#")) && str.contains(">")) {
|
||||||
|
invalidateContext(data, "expected a string at position " + (index + 1) + " but a role/user/channel was provided");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!choices.isEmpty() && choices.stream().noneMatch(choice -> choice.getAsString().equalsIgnoreCase(str))) {
|
if (!choices.isEmpty()) {
|
||||||
|
List<Command.Choice> matched = choices.stream().filter(choice -> choice.getAsString().equalsIgnoreCase(str)).toList();
|
||||||
|
if (matched.isEmpty()) {
|
||||||
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
|
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer min = data.getMinLength();
|
Integer min = data.getMinLength();
|
||||||
@@ -127,202 +90,156 @@ public class MessageCommandContext extends CommandContext implements MessageCont
|
|||||||
|
|
||||||
if (min != null && str.length() < min) {
|
if (min != null && str.length() < min) {
|
||||||
invalidateContext(data, "minimum length for argument is " + min);
|
invalidateContext(data, "minimum length for argument is " + min);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (max != null && str.length() > max) {
|
if (max != null && str.length() > max) {
|
||||||
invalidateContext(data, "maximum length for argument is " + max);
|
invalidateContext(data, "maximum length for argument is " + max);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value = str;
|
value = str;
|
||||||
}
|
}
|
||||||
case INTEGER -> {
|
case INTEGER -> {
|
||||||
String str = args[index++];
|
long num = Long.parseLong(args[index]);
|
||||||
if (!INTEGER_PATTERN.matcher(str).matches()) {
|
if (!choices.isEmpty()) {
|
||||||
invalidateContext(data, "the argument provided is not a number.");
|
List<Command.Choice> matched = choices.stream().filter(choice -> choice.getAsLong() == num).toList();
|
||||||
break;
|
if (matched.isEmpty()) {
|
||||||
}
|
|
||||||
|
|
||||||
long num = Long.parseLong(str);
|
|
||||||
if (!choices.isEmpty() && choices.stream().noneMatch(choice -> choice.getAsLong() == num)) {
|
|
||||||
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
|
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
value = num;
|
||||||
|
|
||||||
Number min = data.getMinValue();
|
|
||||||
Number max = data.getMaxValue();
|
Number max = data.getMaxValue();
|
||||||
|
Number min = data.getMinValue();
|
||||||
|
|
||||||
if (min != null && num < min.longValue()) {
|
if (min != null && num < min.longValue()) {
|
||||||
invalidateContext(data, "minimum value for argument is " + min.longValue());
|
value = min.longValue();
|
||||||
break;
|
} else if (max != null && num > max.longValue()) {
|
||||||
}
|
value = max.longValue();
|
||||||
|
|
||||||
if (max != null && num > max.longValue()) {
|
|
||||||
invalidateContext(data, "maximum value for argument is " + max.longValue());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value = num;
|
value = num;
|
||||||
}
|
}
|
||||||
case BOOLEAN -> {
|
case BOOLEAN -> {
|
||||||
String str = args[index++];
|
String str = args[index];
|
||||||
if (!str.equalsIgnoreCase("true") && !str.equalsIgnoreCase("false")) {
|
if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("false")) {
|
||||||
invalidateContext(data, "expected true or false");
|
value = Boolean.parseBoolean(str);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Boolean.parseBoolean(str);
|
invalidateContext(data, "expected true or false");
|
||||||
}
|
}
|
||||||
case NUMBER -> {
|
case NUMBER -> {
|
||||||
String str = args[index++];
|
double num = Long.parseLong(args[index]);
|
||||||
if (!NUMBER_PATTERN.matcher(str).matches()) {
|
if (!choices.isEmpty()) {
|
||||||
invalidateContext(data, "the argument provided is not a number.");
|
List<Command.Choice> matched = choices.stream().filter(choice -> choice.getAsDouble() == num).toList();
|
||||||
break;
|
if (matched.isEmpty()) {
|
||||||
}
|
|
||||||
|
|
||||||
double num = Double.parseDouble(str);
|
|
||||||
if (!choices.isEmpty() && choices.stream().noneMatch(choice -> choice.getAsDouble() == num)) {
|
|
||||||
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
|
invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Number min = data.getMinValue();
|
|
||||||
Number max = data.getMaxValue();
|
Number max = data.getMaxValue();
|
||||||
|
Number min = data.getMinValue();
|
||||||
|
value = num;
|
||||||
|
|
||||||
if (min != null && num < min.doubleValue()) {
|
if (min != null && num < min.doubleValue()) {
|
||||||
invalidateContext(data, "minimum value for argument is " + min.doubleValue());
|
value = min.doubleValue();
|
||||||
break;
|
} else if (max != null && num > max.doubleValue()) {
|
||||||
|
value = max.doubleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (max != null && num > max.doubleValue()) {
|
|
||||||
invalidateContext(data, "maximum value for argument is " + max.doubleValue());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = num;
|
|
||||||
}
|
}
|
||||||
case ROLE -> {
|
case ROLE -> {
|
||||||
index++;
|
|
||||||
Role role = mentions.getRoles().size() > roleMentionIndex ? mentions.getRoles().get(roleMentionIndex++) : null;
|
|
||||||
if (role == null || !isFromCurrentGuild(role.getGuild())) {
|
|
||||||
invalidateContext(data, "provided argument at position " + position + " is not a valid role.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = role;
|
|
||||||
}
|
|
||||||
case USER -> {
|
|
||||||
index++;
|
|
||||||
value = resolveUser(mentions, memberMentionIndex, userMentionIndex);
|
|
||||||
if (value instanceof Member) {
|
|
||||||
memberMentionIndex++;
|
|
||||||
} else if (value != null) {
|
|
||||||
userMentionIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
invalidateContext(data, "provided argument at position " + position + " is not a valid user.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case MENTIONABLE -> {
|
|
||||||
index++;
|
|
||||||
if (mentions.getRoles().size() > roleMentionIndex) {
|
if (mentions.getRoles().size() > roleMentionIndex) {
|
||||||
Role role = mentions.getRoles().get(roleMentionIndex++);
|
Role role = mentions.getRoles().get(roleMentionIndex++);
|
||||||
if (!isFromCurrentGuild(role.getGuild())) {
|
if (role.getGuild().getId().equals(getGuild().getId())) {
|
||||||
invalidateContext(data, "provided argument at position " + position + " is not a valid role.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = role;
|
value = role;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = resolveUser(mentions, memberMentionIndex, userMentionIndex);
|
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid role.");
|
||||||
if (value instanceof Member) {
|
}
|
||||||
memberMentionIndex++;
|
case USER -> {
|
||||||
} else if (value != null) {
|
if (mentions.getMembers().size() > memberMentionIndex) {
|
||||||
userMentionIndex++;
|
Member member = mentions.getMembers().get(memberMentionIndex++);
|
||||||
|
if (member.getGuild().getId().equals(getGuild().getId())) {
|
||||||
|
value = member;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == null) {
|
if (mentions.getUsers().size() > userMentionIndex) {
|
||||||
invalidateContext(data, "provided argument at position " + position + " is not a valid user or role.");
|
value = mentions.getUsers().get(userMentionIndex++);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid user.");
|
||||||
|
}
|
||||||
|
case MENTIONABLE -> {
|
||||||
|
if (mentions.getRoles().size() > roleMentionIndex) {
|
||||||
|
Role role = mentions.getRoles().get(roleMentionIndex++);
|
||||||
|
if (role.getGuild().getId().equals(getGuild().getId())) {
|
||||||
|
value = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentions.getMembers().size() > memberMentionIndex) {
|
||||||
|
Member member = mentions.getMembers().get(memberMentionIndex++);
|
||||||
|
if (member.getGuild().getId().equals(getGuild().getId())) {
|
||||||
|
value = member;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentions.getUsers().size() > userMentionIndex) {
|
||||||
|
value = mentions.getUsers().get(userMentionIndex++);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid user or role.");
|
||||||
}
|
}
|
||||||
case CHANNEL -> {
|
case CHANNEL -> {
|
||||||
index++;
|
if (mentions.getChannels().size() > channelMentionIndex) {
|
||||||
GuildChannel channel = mentions.getChannels().size() > channelMentionIndex ? mentions.getChannels().get(channelMentionIndex++) : null;
|
GuildChannel channel = mentions.getChannels().get(channelMentionIndex++);
|
||||||
if (channel == null || !isFromCurrentGuild(channel.getGuild())) {
|
|
||||||
invalidateContext(data, "provided argument at position " + position + " is not a valid channel.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requiredChannelTypes.isEmpty() && !requiredChannelTypes.contains(channel.getType())) {
|
if (!requiredChannelTypes.contains(channel.getType())) {
|
||||||
invalidateContext(data, "not a valid channel type at index " + position +
|
invalidateContext(data, "not a valid channel type at index " + (index + 1) +
|
||||||
" , valid types are: " + validTypes(requiredChannelTypes));
|
" , valid types are: " + validTypes(requiredChannelTypes));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channel.getGuild().getId().equals(getGuild().getId())) {
|
||||||
value = channel;
|
value = channel;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
default -> index++;
|
}
|
||||||
|
|
||||||
|
invalidateContext(data, "provided argument at position " + (index + 1) + " is not a valid channel.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IndexOutOfBoundsException e) {
|
||||||
|
if (data.isRequired()) {
|
||||||
|
// This should never happen because required options are validated before calling parseOptions()
|
||||||
|
throw new IllegalArgumentException("Required option " + data.getName() + " was not found at index " + index);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
invalidateContext(data, "the argument provided is not a number.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
options.add(new MessageCommandOption(data, value));
|
options.add(new MessageCommandOption(data, value));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are leftover arguments that weren't consumed
|
index++;
|
||||||
if (index < args.length) {
|
|
||||||
OptionData lastOption = commandOptions.isEmpty() ? LEFTOVER_ARGUMENTS : commandOptions.getLast();
|
|
||||||
invalidateContext(lastOption, "too many arguments");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A string option greedily consumes the remaining arguments, it has to stop as soon as an argument
|
|
||||||
* belongs to one of the following options.
|
|
||||||
*/
|
|
||||||
private boolean stopsTextCapture(String arg, List<OptionData> commandOptions, int currentOptionIndex) {
|
|
||||||
if (NUMBER_PATTERN.matcher(arg).matches()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OptionData> remaining = commandOptions.subList(currentOptionIndex + 1, commandOptions.size());
|
|
||||||
|
|
||||||
if (arg.equalsIgnoreCase("true") || arg.equalsIgnoreCase("false")) {
|
|
||||||
return remaining.stream().anyMatch(option -> option.getType() == OptionType.BOOLEAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MENTION_PATTERN.matcher(arg).matches()) {
|
|
||||||
return remaining.stream().anyMatch(option -> MENTION_TYPES.contains(option.getType()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolveUser(Mentions mentions, int memberMentionIndex, int userMentionIndex) {
|
|
||||||
if (mentions.getMembers().size() > memberMentionIndex) {
|
|
||||||
Member member = mentions.getMembers().get(memberMentionIndex);
|
|
||||||
if (isFromCurrentGuild(member.getGuild())) {
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mentions.getUsers().size() > userMentionIndex) {
|
|
||||||
return mentions.getUsers().get(userMentionIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFromCurrentGuild(Guild guild) {
|
|
||||||
return getGuild() != null && guild != null && guild.getId().equals(getGuild().getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String validChoices(List<Command.Choice> choices) {
|
private String validChoices(List<Command.Choice> choices) {
|
||||||
StringJoiner joiner = new StringJoiner(", ");
|
StringJoiner joiner = new StringJoiner(", ");
|
||||||
for (Command.Choice choice : choices) {
|
for (Command.Choice choice : choices) {
|
||||||
@@ -344,7 +261,7 @@ public class MessageCommandContext extends CommandContext implements MessageCont
|
|||||||
@Override
|
@Override
|
||||||
public String parseCommandName() {
|
public String parseCommandName() {
|
||||||
String rawMessage = message.getContentRaw().trim();
|
String rawMessage = message.getContentRaw().trim();
|
||||||
String prefix = settings.getPrefix();
|
String prefix = Settings.getInstance().getPrefix();
|
||||||
|
|
||||||
if (rawMessage.startsWith(prefix)) {
|
if (rawMessage.startsWith(prefix)) {
|
||||||
return getFullCommand().split(" ", 2)[0];
|
return getFullCommand().split(" ", 2)[0];
|
||||||
@@ -356,7 +273,7 @@ public class MessageCommandContext extends CommandContext implements MessageCont
|
|||||||
@Override
|
@Override
|
||||||
public String getFullCommand() {
|
public String getFullCommand() {
|
||||||
String rawMessage = message.getContentRaw().trim();
|
String rawMessage = message.getContentRaw().trim();
|
||||||
String prefix = settings.getPrefix();
|
String prefix = Settings.getInstance().getPrefix();
|
||||||
|
|
||||||
return rawMessage.replaceFirst("^[" + prefix + "\\s]+", "").trim();
|
return rawMessage.replaceFirst("^[" + prefix + "\\s]+", "").trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package me.bobert.netneutrality.context.command;
|
|||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.events.Event;
|
import net.dv8tion.jda.api.events.Event;
|
||||||
@@ -21,13 +20,8 @@ import java.util.List;
|
|||||||
public class MessageMenuContext extends CommandContext implements InteractionContext {
|
public class MessageMenuContext extends CommandContext implements InteractionContext {
|
||||||
private final MessageContextInteractionEvent event;
|
private final MessageContextInteractionEvent event;
|
||||||
|
|
||||||
@Override
|
public MessageMenuContext(MessageContextInteractionEvent event) {
|
||||||
public boolean isInteraction() {
|
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser());
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageMenuContext(MessageContextInteractionEvent event, SettingsProvider settings) {
|
|
||||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings);
|
|
||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import me.bobert.netneutrality.core.command.options.CommandOption;
|
|||||||
import me.bobert.netneutrality.core.command.options.SlashCommandOption;
|
import me.bobert.netneutrality.core.command.options.SlashCommandOption;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
@@ -20,16 +19,11 @@ import java.util.stream.Collectors;
|
|||||||
public class SlashCommandContext extends CommandContext implements InteractionContext {
|
public class SlashCommandContext extends CommandContext implements InteractionContext {
|
||||||
private final SlashCommandInteractionEvent event;
|
private final SlashCommandInteractionEvent event;
|
||||||
|
|
||||||
public SlashCommandContext(SlashCommandInteractionEvent event, SettingsProvider settings) {
|
public SlashCommandContext(SlashCommandInteractionEvent event) {
|
||||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings);
|
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser());
|
||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInteraction() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SlashCommandInteractionEvent getEvent() {
|
public SlashCommandInteractionEvent getEvent() {
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package me.bobert.netneutrality.context.command;
|
|||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
import net.dv8tion.jda.api.entities.User;
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
@@ -22,13 +21,8 @@ import java.util.List;
|
|||||||
public class UserMenuContext extends CommandContext implements InteractionContext {
|
public class UserMenuContext extends CommandContext implements InteractionContext {
|
||||||
private final UserContextInteractionEvent event;
|
private final UserContextInteractionEvent event;
|
||||||
|
|
||||||
@Override
|
public UserMenuContext(UserContextInteractionEvent event) {
|
||||||
public boolean isInteraction() {
|
super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser());
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserMenuContext(UserContextInteractionEvent event, SettingsProvider settings) {
|
|
||||||
super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser(), settings);
|
|
||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ package me.bobert.netneutrality.context.tasks;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.context.AsyncContext;
|
import me.bobert.netneutrality.context.AsyncContext;
|
||||||
import me.bobert.netneutrality.core.context.NoMessageContext;
|
import me.bobert.netneutrality.core.context.NoMessageContext;
|
||||||
import me.bobert.netneutrality.core.context.TaskContext;
|
import me.bobert.netneutrality.core.context.TaskContext;
|
||||||
import net.dv8tion.jda.api.components.MessageTopLevelComponent;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
|
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
|
||||||
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
|
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
|
||||||
|
import net.dv8tion.jda.api.interactions.modals.Modal;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -42,7 +45,7 @@ public class EmojiTaskContext extends TaskContext implements NoMessageContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
public void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (getChannel() != null && getChannel().canTalk()) {
|
if (getChannel() != null && getChannel().canTalk()) {
|
||||||
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
|
getChannel().sendMessage(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package me.bobert.netneutrality.core;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
|
import me.bobert.netneutrality.music.MusicManager;
|
||||||
|
import me.bobert.netneutrality.music.TrackScheduler;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class BotState {
|
||||||
|
public static boolean SAFE = false;
|
||||||
|
|
||||||
|
//TODO: is it needed?
|
||||||
|
private static void saveGuildsState() {
|
||||||
|
Map<String, GuildService> services = GuildService.getServices();
|
||||||
|
|
||||||
|
JSONObject main = new JSONObject();
|
||||||
|
JSONArray guildsArray = new JSONArray();
|
||||||
|
|
||||||
|
for (MusicManager manager : MusicManager.getManagers()) {
|
||||||
|
TrackScheduler trackScheduler = manager.getScheduler();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GuildService service : services.values()) {
|
||||||
|
|
||||||
|
|
||||||
|
JSONObject guild = new JSONObject();
|
||||||
|
JSONObject music = new JSONObject();
|
||||||
|
guild.put("id", service.getEntity().getGuildId());
|
||||||
|
|
||||||
|
// MessageChannel currentChannel = trackScheduler.getTextChannel();
|
||||||
|
// if (currentChannel == null) {
|
||||||
|
// music.put("current_channel", "null");
|
||||||
|
// } else {
|
||||||
|
// music.put("current_channel", currentChannel.getId());
|
||||||
|
// }
|
||||||
|
|
||||||
|
guild.put("music", music);
|
||||||
|
|
||||||
|
guildsArray.put(guild);
|
||||||
|
}
|
||||||
|
|
||||||
|
main.put("guilds", guildsArray);
|
||||||
|
|
||||||
|
File state = new File("state.json");
|
||||||
|
try {
|
||||||
|
if (!state.exists()) {
|
||||||
|
state.createNewFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(state)) {
|
||||||
|
writer.write(main.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dump() {
|
||||||
|
if (!SAFE) {
|
||||||
|
saveGuildsState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,11 @@ package me.bobert.netneutrality.core;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.log.DiscordDebugChannelLogSink;
|
import io.sentry.Sentry;
|
||||||
import me.bobert.netneutrality.core.log.Log4jLogSink;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.log.LogSink;
|
import me.bobert.netneutrality.core.context.Context;
|
||||||
import me.bobert.netneutrality.core.log.LoggedException;
|
import me.bobert.netneutrality.core.context.TaskContext;
|
||||||
import me.bobert.netneutrality.core.log.SentryLogSink;
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.exceptions.ContextException;
|
import me.bobert.netneutrality.exceptions.ContextException;
|
||||||
import me.bobert.netneutrality.utils.MessageFunction;
|
import me.bobert.netneutrality.utils.MessageFunction;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
@@ -18,52 +17,9 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
|
|
||||||
public class LogHandler {
|
public class LogHandler {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(LogHandler.class);
|
private static final Logger logger = LogManager.getLogger(LogHandler.class);
|
||||||
private static final CopyOnWriteArrayList<LogSink> sinks = new CopyOnWriteArrayList<>();
|
|
||||||
private static volatile DiscordDebugChannelLogSink discordSink;
|
|
||||||
|
|
||||||
static {
|
|
||||||
sinks.add(new Log4jLogSink());
|
|
||||||
sinks.add(new SentryLogSink());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the guild-service provider used for debug-channel logging. Called once during bootstrap.
|
|
||||||
* Internally registers (or replaces) the Discord debug-channel {@link LogSink}.
|
|
||||||
*
|
|
||||||
* @param provider the live guild service provider
|
|
||||||
*/
|
|
||||||
public static void bind(GuildServiceProvider provider) {
|
|
||||||
if (discordSink != null) {
|
|
||||||
sinks.remove(discordSink);
|
|
||||||
}
|
|
||||||
|
|
||||||
discordSink = new DiscordDebugChannelLogSink(provider);
|
|
||||||
sinks.add(discordSink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an additional log destination. Sinks are notified for every exception logged via
|
|
||||||
* {@link #log(Exception)}.
|
|
||||||
*
|
|
||||||
* @param sink the sink to register
|
|
||||||
*/
|
|
||||||
public static void addSink(LogSink sink) {
|
|
||||||
sinks.add(sink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters a previously registered log destination.
|
|
||||||
*
|
|
||||||
* @param sink the sink to remove
|
|
||||||
*/
|
|
||||||
public static void removeSink(LogSink sink) {
|
|
||||||
sinks.remove(sink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an exception accordingly
|
* Log an exception accordingly
|
||||||
@@ -71,15 +27,17 @@ public class LogHandler {
|
|||||||
* @param e The exceptions to log
|
* @param e The exceptions to log
|
||||||
*/
|
*/
|
||||||
public static void log(Exception e) {
|
public static void log(Exception e) {
|
||||||
LoggedException event = LoggedException.of(e);
|
Sentry.captureException(e);
|
||||||
|
|
||||||
for (LogSink sink : sinks) {
|
|
||||||
safelyHandle(sink, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e instanceof ContextException exception) {
|
if (e instanceof ContextException exception) {
|
||||||
|
logInternally(exception);
|
||||||
|
tryLogToDebugChannel(exception);
|
||||||
tryGracefulContextError(exception);
|
tryGracefulContextError(exception);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void log(String s, Exception e) {
|
public static void log(String s, Exception e) {
|
||||||
@@ -100,11 +58,36 @@ public class LogHandler {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void safelyHandle(LogSink sink, LoggedException event) {
|
private static void logInternally(ContextException exception) {
|
||||||
try {
|
Context context = exception.getContext();
|
||||||
sink.handle(event);
|
String guildId = context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId();
|
||||||
} catch (Throwable t) {
|
|
||||||
logger.error("Log sink {} failed to handle exception", sink.getClass().getSimpleName(), t);
|
logger.error("Exception thrown from context (guild: {}, user: {}): {}", guildId,
|
||||||
|
context.getDiscordUser().getId(),
|
||||||
|
exception.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context instanceof CommandContext commandContext) {
|
||||||
|
logger.error("Caused by command: {}", commandContext.getCommand().getCommandName(), exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context instanceof TaskContext taskContext) {
|
||||||
|
logger.error("Caused by task with ID: {}", taskContext.getId(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void tryLogToDebugChannel(ContextException exception) {
|
||||||
|
Context context = exception.getContext();
|
||||||
|
|
||||||
|
if (context.getGuild() != null) {
|
||||||
|
if (GuildService.get(context.getGuild()).isDebugEnabled()) {
|
||||||
|
context.getGuild()
|
||||||
|
.getTextChannels()
|
||||||
|
.stream()
|
||||||
|
.filter(textChannel -> textChannel.getName().equals("debug") || textChannel.getName().equals("log"))
|
||||||
|
.filter(TextChannel::canTalk)
|
||||||
|
.forEach(textChannel -> textChannel.sendMessageEmbeds(MessageFunction.buildDebugMessage(exception)).queue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Severity ordering used to gate what gets sent to a guild's log channel, from least to most verbose:
|
|
||||||
* {@code ERROR} < {@code WARN} < {@code INFO}.
|
|
||||||
*/
|
|
||||||
public enum LogSeverity {
|
|
||||||
ERROR,
|
|
||||||
WARN,
|
|
||||||
INFO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a severity name, falling back to {@link #ERROR} (the most restrictive level) when the
|
|
||||||
* value is missing or unrecognized.
|
|
||||||
*
|
|
||||||
* @param value the raw severity name (case-insensitive)
|
|
||||||
* @return the matching severity, or {@link #ERROR} as a safe default
|
|
||||||
*/
|
|
||||||
public static LogSeverity parse(String value) {
|
|
||||||
if (value == null || value.isEmpty()) {
|
|
||||||
return ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return LogSeverity.valueOf(value.toUpperCase());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param configured the guild's configured minimum severity
|
|
||||||
* @return {@code true} when this severity should be sent given the configured minimum
|
|
||||||
*/
|
|
||||||
public boolean meetsThreshold(LogSeverity configured) {
|
|
||||||
return this.ordinal() <= configured.ordinal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-4
@@ -1,13 +1,11 @@
|
|||||||
package me.bobert.netneutrality.core.command.impl;
|
package me.bobert.netneutrality.core.command;
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
*
|
*
|
||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
public abstract class AbstractAdminCommand extends AbstractCommand {
|
||||||
|
|
||||||
public abstract class AdminCommand extends AbstractCommand {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean needAdmin() {
|
public boolean needAdmin() {
|
||||||
+1
-2
@@ -1,4 +1,4 @@
|
|||||||
package me.bobert.netneutrality.core.command.impl;
|
package me.bobert.netneutrality.core.command;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
@@ -6,7 +6,6 @@ package me.bobert.netneutrality.core.command.impl;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
|
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
|
||||||
+3
-5
@@ -1,4 +1,4 @@
|
|||||||
package me.bobert.netneutrality.core.command.impl;
|
package me.bobert.netneutrality.core.command;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
@@ -7,13 +7,11 @@ package me.bobert.netneutrality.core.command.impl;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.context.command.MessageMenuContext;
|
import me.bobert.netneutrality.context.command.MessageMenuContext;
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
|
|
||||||
public abstract class MessageMenuCommand extends AbstractCommand {
|
public abstract class AbstractMessageMenuCommand extends AbstractCommand {
|
||||||
@Override
|
@Override
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return null;
|
return null;
|
||||||
+6
-46
@@ -1,12 +1,10 @@
|
|||||||
package me.bobert.netneutrality.core.command.impl;
|
package me.bobert.netneutrality.core.command;
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
*
|
*
|
||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.CommandCategory;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
import me.bobert.netneutrality.db.GSettings;
|
import me.bobert.netneutrality.db.GSettings;
|
||||||
import me.bobert.netneutrality.db.service.GuildService;
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
import me.bobert.netneutrality.utils.UserFunction;
|
import me.bobert.netneutrality.utils.UserFunction;
|
||||||
@@ -17,44 +15,7 @@ import net.dv8tion.jda.api.entities.channel.ChannelType;
|
|||||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
|
|
||||||
import java.util.Objects;
|
public abstract class AbstractMusicCommand extends AbstractCommand {
|
||||||
|
|
||||||
public abstract class MusicCommand extends AbstractCommand {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injected guild-service provider when constructed via DI.
|
|
||||||
*/
|
|
||||||
private final GuildServiceProvider guildServices;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferred constructor for DI.
|
|
||||||
*
|
|
||||||
* @param guildServices the shared guild service provider, must not be {@code null}
|
|
||||||
*/
|
|
||||||
public MusicCommand(GuildServiceProvider guildServices) {
|
|
||||||
Objects.requireNonNull(guildServices, "GuildServiceProvider must not be null");
|
|
||||||
this.guildServices = guildServices;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the {@link GuildService} for a guild via the injected provider.
|
|
||||||
*
|
|
||||||
* @param guild the guild
|
|
||||||
* @return the guild service
|
|
||||||
*/
|
|
||||||
protected final GuildService guildService(Guild guild) {
|
|
||||||
return guildServices.get(guild.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the {@link GuildService} for a guild id via the injected provider.
|
|
||||||
*
|
|
||||||
* @param guildId the guild id
|
|
||||||
* @return the guild service
|
|
||||||
*/
|
|
||||||
protected final GuildService guildService(String guildId) {
|
|
||||||
return guildServices.get(guildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean needAdmin() {
|
public boolean needAdmin() {
|
||||||
@@ -79,7 +40,7 @@ public abstract class MusicCommand extends AbstractCommand {
|
|||||||
|
|
||||||
Guild guild = member.getGuild();
|
Guild guild = member.getGuild();
|
||||||
|
|
||||||
String roleDb = guildService(guild.getId()).getGuildSettingService().getOrDefault(GSettings.MUSIC_ROLE);
|
String roleDb = GuildService.get(guild.getId()).getGuildSettingService().getSetting(GSettings.MUSIC_ROLE);
|
||||||
|
|
||||||
if (roleDb != null && !roleDb.isEmpty()) {
|
if (roleDb != null && !roleDb.isEmpty()) {
|
||||||
if (UserFunction.isAdmin(member)) {
|
if (UserFunction.isAdmin(member)) {
|
||||||
@@ -89,7 +50,7 @@ public abstract class MusicCommand extends AbstractCommand {
|
|||||||
Role neededRole = guild.getRoleById(roleDb);
|
Role neededRole = guild.getRoleById(roleDb);
|
||||||
if (neededRole == null) {
|
if (neededRole == null) {
|
||||||
// Role doesn't exist on guild anymore, remove from db
|
// Role doesn't exist on guild anymore, remove from db
|
||||||
guildService(guild.getId()).getGuildSettingService().set(GSettings.MUSIC_ROLE, null);
|
GuildService.get(guild.getId()).getGuildSettingService().setSettingValue(GSettings.MUSIC_ROLE, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,11 +63,10 @@ public abstract class MusicCommand extends AbstractCommand {
|
|||||||
@Override
|
@Override
|
||||||
public boolean canBeExecutedFrom(MessageChannel channel) {
|
public boolean canBeExecutedFrom(MessageChannel channel) {
|
||||||
if (channel instanceof GuildMessageChannel guildMessageChannel) {
|
if (channel instanceof GuildMessageChannel guildMessageChannel) {
|
||||||
if (channel.getType() != ChannelType.TEXT) {
|
if (channel.getType() != ChannelType.TEXT)
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
String channelDb = guildService(guildMessageChannel.getGuild()).getGuildSettingService().getOrDefault(GSettings.MUSIC_CHANNEL);
|
String channelDb = GuildService.get(guildMessageChannel.getGuild()).getGuildSettingService().getSetting(GSettings.MUSIC_CHANNEL);
|
||||||
if (channelDb != null && !channelDb.isEmpty()) {
|
if (channelDb != null && !channelDb.isEmpty()) {
|
||||||
return channelDb.equals(channel.getId());
|
return channelDb.equals(channel.getId());
|
||||||
}
|
}
|
||||||
+3
-4
@@ -1,4 +1,4 @@
|
|||||||
package me.bobert.netneutrality.core.command.impl;
|
package me.bobert.netneutrality.core.command;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
@@ -7,12 +7,11 @@ package me.bobert.netneutrality.core.command.impl;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.context.command.UserMenuContext;
|
import me.bobert.netneutrality.context.command.UserMenuContext;
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import net.dv8tion.jda.api.entities.User;
|
import net.dv8tion.jda.api.entities.User;
|
||||||
|
|
||||||
public abstract class UserMenuCommand extends AbstractCommand {
|
public abstract class AbstractUserMenuCommand extends AbstractCommand {
|
||||||
@Override
|
@Override
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return null;
|
return null;
|
||||||
@@ -5,6 +5,7 @@ package me.bobert.netneutrality.core.command;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
|
|||||||
@@ -5,178 +5,35 @@ package me.bobert.netneutrality.core.command;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import me.bobert.netneutrality.core.command.options.CommandOptions;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
|
||||||
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.StringJoiner;
|
|
||||||
|
|
||||||
public class CommandContainer {
|
public class CommandContainer {
|
||||||
public static final CommandContainer NULL_COMMAND = new CommandContainer(null);
|
public static final CommandContainer NULL_COMMAND = new CommandContainer(null);
|
||||||
private final Command command;
|
private final Command command;
|
||||||
private CommandDataImpl commandData;
|
private CommandOptions commandOptions;
|
||||||
private List<OptionData> options;
|
|
||||||
private List<SubcommandData> subcommands;
|
|
||||||
private List<SubcommandGroupData> subcommandGroups;
|
|
||||||
|
|
||||||
public CommandContainer(Command command) {
|
public CommandContainer(Command command) {
|
||||||
this.command = command;
|
this.command = command;
|
||||||
|
|
||||||
if (command != null) {
|
|
||||||
CommandOptionsBuilder commandOptionsBuilder = new CommandOptionsBuilder(command.getType(), command);
|
|
||||||
command.registerCommandOptions(commandOptionsBuilder);
|
|
||||||
initContainer(commandOptionsBuilder);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command getCommand() {
|
public Command getCommand() {
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SlashCommandData getSlashCommandData() {
|
public CommandOptions getCommandOptions() {
|
||||||
return commandData;
|
return commandOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<OptionData> getOptions() {
|
public void init() {
|
||||||
return options;
|
initOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SubcommandData> getSubcommands() {
|
private void initOptions() {
|
||||||
return subcommands;
|
if (command != null) {
|
||||||
}
|
CommandOptionsBuilder commandOptionsBuilder = new CommandOptionsBuilder(command.getType(), command);
|
||||||
|
command.registerCommandOptions(commandOptionsBuilder);
|
||||||
public List<SubcommandGroupData> getSubcommandGroups() {
|
commandOptions = commandOptionsBuilder.build();
|
||||||
return subcommandGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initContainer(CommandOptionsBuilder builder) {
|
|
||||||
this.commandData = builder.build();
|
|
||||||
this.options = commandData.getOptions();
|
|
||||||
this.subcommands = commandData.getSubcommands();
|
|
||||||
this.subcommandGroups = commandData.getSubcommandGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return options.isEmpty() && subcommands.isEmpty() && subcommandGroups.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String printUsage() {
|
|
||||||
StringBuilder usage = new StringBuilder();
|
|
||||||
if (!options.isEmpty()) {
|
|
||||||
usage.append("- Usage: /").append(command.getCommandName());
|
|
||||||
for (OptionData option : options) {
|
|
||||||
usage.append(" <").append(option.getName()).append(">");
|
|
||||||
}
|
|
||||||
|
|
||||||
usage.append(printOptions(options, null, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subcommands.isEmpty()) {
|
|
||||||
for (SubcommandData subcommand : subcommands) {
|
|
||||||
usage.append("- Usage: /").append(command.getCommandName()).append(" ").append(subcommand.getName());
|
|
||||||
|
|
||||||
for (OptionData option : subcommand.getOptions()) {
|
|
||||||
usage.append(" <").append(option.getName()).append(">");
|
|
||||||
}
|
|
||||||
|
|
||||||
usage.append(" (").append(subcommand.getDescription()).append(")");
|
|
||||||
usage.append(printOptions(subcommand.getOptions(), null, false));
|
|
||||||
|
|
||||||
usage.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subcommandGroups.isEmpty()) {
|
|
||||||
for (SubcommandGroupData subcommandGroup : subcommandGroups) {
|
|
||||||
usage.append("- Subcommand: /").append(command.getCommandName())
|
|
||||||
.append(" ").append(subcommandGroup.getName())
|
|
||||||
.append(" (").append(subcommandGroup.getDescription()).append(")")
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
for (SubcommandData subcommand : subcommandGroup.getSubcommands()) {
|
|
||||||
usage.append(" - Usage: /").append(command.getCommandName())
|
|
||||||
.append(" ").append(subcommandGroup.getName())
|
|
||||||
.append(" ").append(subcommand.getName());
|
|
||||||
|
|
||||||
for (OptionData option : subcommand.getOptions()) {
|
|
||||||
usage.append(" <").append(option.getName()).append(">");
|
|
||||||
}
|
|
||||||
|
|
||||||
usage.append(printOptions(subcommand.getOptions(), subcommand.getDescription(), true));
|
|
||||||
|
|
||||||
usage.append("\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEmpty()) {
|
|
||||||
usage.append("\n\n-# *: required argument");
|
|
||||||
}
|
|
||||||
|
|
||||||
return usage.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String printOptions(List<OptionData> options, String description, boolean group) {
|
|
||||||
StringBuilder usage = new StringBuilder();
|
|
||||||
|
|
||||||
if (description != null) {
|
|
||||||
usage.append(" (").append(description).append(")");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (OptionData option : options) {
|
|
||||||
usage.append("\n");
|
|
||||||
if (group) {
|
|
||||||
usage.append(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
usage.append(" - ");
|
|
||||||
if (option.isRequired()) {
|
|
||||||
usage.append("*");
|
|
||||||
}
|
|
||||||
usage.append(option.getName()).append(": ").append(option.getDescription());
|
|
||||||
|
|
||||||
if (option.getMinValue() != null) {
|
|
||||||
usage.append(" [Min value: ").append(option.getMinValue().longValue()).append("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.getMaxValue() != null) {
|
|
||||||
usage.append(" [Max value: ").append(option.getMaxValue().longValue()).append("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.getMinLength() != null) {
|
|
||||||
usage.append(" [Min length: ").append(option.getMinLength().longValue()).append("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.getMaxLength() != null) {
|
|
||||||
usage.append(" [Max length: ").append(option.getMaxLength().longValue()).append("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<net.dv8tion.jda.api.interactions.commands.Command.Choice> choices = option.getChoices();
|
|
||||||
if (!choices.isEmpty()) {
|
|
||||||
StringJoiner joiner = new StringJoiner(", ");
|
|
||||||
for (net.dv8tion.jda.api.interactions.commands.Command.Choice choice : choices) {
|
|
||||||
joiner.add(choice.getAsString());
|
|
||||||
}
|
|
||||||
|
|
||||||
usage.append(" [Available choices: ").append(joiner).append("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<ChannelType> requiredChannelTypes = option.getChannelTypes();
|
|
||||||
if (!requiredChannelTypes.isEmpty()) {
|
|
||||||
StringJoiner joiner = new StringJoiner(", ");
|
|
||||||
for (ChannelType type : requiredChannelTypes) {
|
|
||||||
joiner.add(type.name());
|
|
||||||
}
|
|
||||||
|
|
||||||
usage.append(" [Required channel types: ").append(joiner).append("]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return usage.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ package me.bobert.netneutrality.core.command;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.tasks.Task;
|
import me.bobert.netneutrality.core.tasks.Task;
|
||||||
import me.bobert.netneutrality.music.QueueActionResult;
|
|
||||||
|
|
||||||
public class CommandExecutionResult {
|
public class CommandExecutionResult {
|
||||||
|
|
||||||
@@ -81,18 +80,4 @@ public class CommandExecutionResult {
|
|||||||
public static CommandExecutionResult task(Task task) {
|
public static CommandExecutionResult task(Task task) {
|
||||||
return new CommandExecutionResult(Result.TASK, null, task, null);
|
return new CommandExecutionResult(Result.TASK, null, task, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapts a {@link QueueActionResult} returned by {@link me.bobert.netneutrality.music.QueueManager}
|
|
||||||
* into a {@link CommandExecutionResult} so it can be surfaced as a Discord reply or HTTP response.
|
|
||||||
*/
|
|
||||||
public static CommandExecutionResult from(QueueActionResult result) {
|
|
||||||
return switch (result.getStatus()) {
|
|
||||||
case SUCCESS -> success(result.getMessage());
|
|
||||||
case INFO -> info(result.getMessage());
|
|
||||||
case WARN -> warn(result.getMessage());
|
|
||||||
case ERROR -> error(result.getMessage());
|
|
||||||
case NOACTION -> none();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,56 +5,24 @@ package me.bobert.netneutrality.core.command;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import me.bobert.netneutrality.bootstrap.ApplicationContext;
|
|
||||||
import me.bobert.netneutrality.bootstrap.ReflectionHelper;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.interactions.InteractionContextType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
|
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
|
||||||
import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
|
import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.reflections.Reflections;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.lang.reflect.Modifier;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
/**
|
public class CommandManager {
|
||||||
* Instance-based {@link CommandRegistry} owning the discovered command map.
|
|
||||||
*/
|
|
||||||
public class CommandManager implements CommandRegistry {
|
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(CommandManager.class);
|
private static final Logger logger = LogManager.getLogger(CommandManager.class);
|
||||||
|
public static final Map<String, CommandContainer> commands = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Map<String, CommandContainer> commands = new ConcurrentHashMap<>();
|
public static void init() {
|
||||||
private final ApplicationContext context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a command manager that can inject context-registered dependencies into reflectively
|
|
||||||
* loaded commands.
|
|
||||||
*
|
|
||||||
* @param context the application context; must not be {@code null}
|
|
||||||
*/
|
|
||||||
public CommandManager(ApplicationContext context) {
|
|
||||||
Objects.requireNonNull(context, "ApplicationContext must not be null");
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int priority() {
|
|
||||||
return 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init() {
|
|
||||||
logger.info("Loading commands..");
|
logger.info("Loading commands..");
|
||||||
|
|
||||||
initCommands();
|
initCommands();
|
||||||
@@ -62,56 +30,48 @@ public class CommandManager implements CommandRegistry {
|
|||||||
logger.info("Loaded {} commands!", commands.size());
|
logger.info("Loaded {} commands!", commands.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static Command getCommand(String commandString) {
|
||||||
public CommandContainer getCommandContainer(String name) {
|
return getCommandContainer(commandString).getCommand();
|
||||||
return commands.getOrDefault(name, CommandContainer.NULL_COMMAND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static CommandContainer getCommandContainer(String commandString) {
|
||||||
public boolean hasCommand(String name) {
|
return commands.getOrDefault(commandString, CommandContainer.NULL_COMMAND);
|
||||||
return commands.containsKey(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static void initCommands() {
|
||||||
* Exposes the live command map for call sites (and tests) that still iterate or mutate it
|
Set<Class<? extends Command>> classes = new Reflections("me.bobert.netneutrality.command").getSubTypesOf(Command.class);
|
||||||
* directly. Prefer {@link #hasCommand(String)} / {@link #getCommandContainer(String)} where
|
for (Class<? extends Command> s : classes) {
|
||||||
* possible.
|
try {
|
||||||
*
|
if (Modifier.isAbstract(s.getModifiers())) {
|
||||||
* @return the registered commands keyed by name
|
continue;
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Map<String, CommandContainer> getCommands() {
|
|
||||||
return commands;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initCommands() {
|
Command command = s.getConstructor().newInstance();
|
||||||
Consumer<Command> onLoaded = command -> {
|
|
||||||
if (!command.isEnabled()) {
|
if (!command.isEnabled()) {
|
||||||
logger.info("Skipping command {} as it is disabled", command.getClass().getName());
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.getCommandName() == null) {
|
if (command.getCommandName() == null) {
|
||||||
logger.error("Failed to load command {}: Command name is null", command.getClass().getName());
|
throw new IllegalArgumentException("Command name is null");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.getDescription() == null && command.getType() == Command.Type.SLASH) {
|
if (command.getDescription() == null && command.getType() == Command.Type.SLASH) {
|
||||||
logger.error("Failed to load command {}: Command description is null", command.getClass().getName());
|
throw new IllegalArgumentException("Command description is null");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandContainer container = new CommandContainer(command);
|
CommandContainer container = new CommandContainer(command);
|
||||||
|
container.init();
|
||||||
|
|
||||||
if (!commands.containsKey(command.getCommandName())) {
|
if (!commands.containsKey(command.getCommandName())) {
|
||||||
commands.put(command.getCommandName(), container);
|
commands.put(command.getCommandName(), container);
|
||||||
}
|
}
|
||||||
};
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to load command {}: {}", s.getSimpleName(), e.getMessage());
|
||||||
ReflectionHelper.loadCommands(Command.class, context, onLoaded);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static void registerSlashCommands(JDA jda) {
|
||||||
public void registerSlashCommands(JDA jda) {
|
|
||||||
CommandListUpdateAction commandListUpdateAction = jda.updateCommands();
|
CommandListUpdateAction commandListUpdateAction = jda.updateCommands();
|
||||||
List<CommandData> slashCommands = new ArrayList<>();
|
List<CommandData> slashCommands = new ArrayList<>();
|
||||||
|
|
||||||
@@ -123,9 +83,9 @@ public class CommandManager implements CommandRegistry {
|
|||||||
logger.debug("Registering slash command: {}", command.getCommandName());
|
logger.debug("Registering slash command: {}", command.getCommandName());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SlashCommandData data = container.getSlashCommandData();
|
SlashCommandData data = container.getCommandOptions().getSlashCommandData();
|
||||||
data.setDefaultPermissions(command.getSlashCommandPermission());
|
data.setDefaultPermissions(command.getSlashCommandPermission());
|
||||||
data.setContexts(command.isGuildOnly() ? Set.of(InteractionContextType.GUILD) : Set.of(InteractionContextType.GUILD, InteractionContextType.BOT_DM));
|
data.setGuildOnly(command.isGuildOnly());
|
||||||
|
|
||||||
slashCommands.add(data);
|
slashCommands.add(data);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -6,84 +6,74 @@ package me.bobert.netneutrality.core.command;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import io.sentry.Sentry;
|
import io.sentry.Sentry;
|
||||||
|
import me.bobert.netneutrality.context.command.SlashCommandContext;
|
||||||
import me.bobert.netneutrality.core.LogHandler;
|
import me.bobert.netneutrality.core.LogHandler;
|
||||||
import me.bobert.netneutrality.core.command.precondition.CommandPrecondition;
|
import me.bobert.netneutrality.core.command.options.CommandOptions;
|
||||||
import me.bobert.netneutrality.core.command.precondition.DisabledCommandPrecondition;
|
|
||||||
import me.bobert.netneutrality.core.command.precondition.GuildOnlyPrecondition;
|
|
||||||
import me.bobert.netneutrality.core.command.precondition.PermissionPrecondition;
|
|
||||||
import me.bobert.netneutrality.core.command.precondition.SlashCommandOnlyPrecondition;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||||
|
import me.bobert.netneutrality.core.tasks.Task;
|
||||||
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
import me.bobert.netneutrality.exceptions.CommandExecutionException;
|
import me.bobert.netneutrality.exceptions.CommandExecutionException;
|
||||||
import me.bobert.netneutrality.exceptions.GracefulContextError;
|
import me.bobert.netneutrality.exceptions.GracefulContextError;
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
import me.bobert.netneutrality.utils.UserFunction;
|
import me.bobert.netneutrality.utils.UserFunction;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
|
import static java.util.function.Predicate.not;
|
||||||
|
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.*;
|
||||||
|
|
||||||
public class CommandProcessor {
|
public class CommandProcessor {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(CommandProcessor.class);
|
private static final Logger logger = LogManager.getLogger(CommandProcessor.class);
|
||||||
private final CommandContext context;
|
private final CommandContext context;
|
||||||
private final CommandRegistry commands;
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
private final List<CommandPrecondition> preconditions;
|
|
||||||
private final UsageValidator usageValidator;
|
|
||||||
private final CommandResultHandler resultHandler;
|
|
||||||
|
|
||||||
public CommandProcessor(CommandContext context, CommandRegistry commands, TaskManager taskManager) {
|
public CommandProcessor(CommandContext context) {
|
||||||
this(context, commands, taskManager, defaultPreconditions(), new UsageValidator(), new CommandResultHandler());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandProcessor(CommandContext context, CommandRegistry commands, TaskManager taskManager,
|
|
||||||
List<CommandPrecondition> preconditions, UsageValidator usageValidator,
|
|
||||||
CommandResultHandler resultHandler) {
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.commands = Objects.requireNonNull(commands, "commands");
|
|
||||||
this.taskManager = Objects.requireNonNull(taskManager, "taskManager");
|
|
||||||
this.preconditions = Objects.requireNonNull(preconditions, "preconditions");
|
|
||||||
this.usageValidator = Objects.requireNonNull(usageValidator, "usageValidator");
|
|
||||||
this.resultHandler = Objects.requireNonNull(resultHandler, "resultHandler");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<CommandPrecondition> defaultPreconditions() {
|
|
||||||
return List.of(
|
|
||||||
new GuildOnlyPrecondition(),
|
|
||||||
new DisabledCommandPrecondition(),
|
|
||||||
new SlashCommandOnlyPrecondition(),
|
|
||||||
new PermissionPrecondition()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispatch() {
|
public void dispatch() {
|
||||||
if (!commands.hasCommand(context.parseCommandName())) {
|
if (!CommandManager.commands.containsKey(context.parseCommandName())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandContainer container = commands.getCommandContainer(context.parseCommandName());
|
CommandContainer container = CommandManager.getCommandContainer(context.parseCommandName());
|
||||||
Command command = container.getCommand();
|
Command command = container.getCommand();
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
throw new IllegalArgumentException("Context has no command");
|
throw new IllegalArgumentException("Context has no command");
|
||||||
}
|
}
|
||||||
|
|
||||||
context.setCommand(command);
|
context.setCommand(command);
|
||||||
|
if (command.isGuildOnly() && !context.isFromGuild()) {
|
||||||
for (CommandPrecondition precondition : preconditions) {
|
context.replyEphemeral("This command is only available in a guild");
|
||||||
if (!precondition.check(context, command)) {
|
context.deleteMessage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.isSlashCommandOnly() && !(context instanceof InteractionContext)) {
|
||||||
|
context.replyEphemeral("This command is only available as a slash command");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug("Context ID: {}", context.getId());
|
logger.info("Context ID: {}", context.getId());
|
||||||
logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId());
|
logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId());
|
||||||
configureSentry(command);
|
configureSentry(command);
|
||||||
|
|
||||||
if (command.getType() == Command.Type.SLASH && !usageValidator.validate(context, container)) {
|
if (command.getType() == Command.Type.SLASH && !canExecute(command)) {
|
||||||
|
context.reply("You are not powerful enough, scrub");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.getType() == Command.Type.SLASH && !checkUsage(container.getCommandOptions())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +88,31 @@ public class CommandProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CommandExecutionResult commandExecutionResult = command.execute(context);
|
CommandExecutionResult commandExecutionResult = command.execute(context);
|
||||||
resultHandler.handle(context, commandExecutionResult, taskManager);
|
if (commandExecutionResult == null) {
|
||||||
|
throw new RuntimeException("CommandExecutionResult is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandExecutionResult.Result result = commandExecutionResult.getResult();
|
||||||
|
if (result == NOACTION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == TASK) {
|
||||||
|
TaskManager.getInstance().insert(context, commandExecutionResult.getTask());
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = commandExecutionResult.getMessage();
|
||||||
|
if (result == ERROR) {
|
||||||
|
logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == WARN) {
|
||||||
|
logger.warn("Context[{}]: Warning from command: {}", context.getId(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
context.reply(message);
|
||||||
|
}
|
||||||
} catch (GracefulContextError exception) {
|
} catch (GracefulContextError exception) {
|
||||||
logger.error(exception.getMessage());
|
logger.error(exception.getMessage());
|
||||||
context.replyEphemeral(exception.getMessage());
|
context.replyEphemeral(exception.getMessage());
|
||||||
@@ -109,6 +123,127 @@ public class CommandProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkUsage(CommandOptions commandOptions) {
|
||||||
|
List<SubcommandGroupData> groups = commandOptions.getSubcommandGroups();
|
||||||
|
List<SubcommandData> subcommands = commandOptions.getSubcommands();
|
||||||
|
List<OptionData> options = commandOptions.getOptions();
|
||||||
|
String[] possibleCommands = Arrays.stream(context.getFullCommand().split(" ")).skip(1).toArray(String[]::new);
|
||||||
|
if (possibleCommands.length == 0) {
|
||||||
|
if (!groups.isEmpty()) {
|
||||||
|
// missing subcommand group
|
||||||
|
context.reply(invalidUsage("missing subcommand group"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subcommands.isEmpty()) {
|
||||||
|
// missing subcommand
|
||||||
|
context.reply(invalidUsage("missing subcommand"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkOptions(possibleCommands, 0, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
SubcommandGroupData group = groups.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
||||||
|
if (group == null) {
|
||||||
|
SubcommandData subcommand = subcommands.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
||||||
|
if (subcommand == null) {
|
||||||
|
if (!groups.isEmpty()) {
|
||||||
|
// no group found but one is expected
|
||||||
|
context.reply(invalidUsage("unknown subcommand group"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subcommands.isEmpty()) {
|
||||||
|
// no subcommand found but one is expected
|
||||||
|
context.reply(invalidUsage("unknown subcommand"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no group found, no subcommand found, command with options only
|
||||||
|
return checkOptions(possibleCommands, 0, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// no group found, subcommand found
|
||||||
|
context.setSubcommand(subcommand);
|
||||||
|
return checkOptions(possibleCommands, 1, subcommand.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (possibleCommands.length < 2) {
|
||||||
|
// missing subcommand for group
|
||||||
|
context.reply(invalidUsage("missing subcommand for command group"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubcommandData subcommand = group.getSubcommands().stream().filter(data -> data.getName().equals(possibleCommands[1])).findFirst().orElse(null);
|
||||||
|
if (subcommand == null) {
|
||||||
|
// group found, no subcommand found
|
||||||
|
context.reply(invalidUsage("unknown subcommand in command group"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// group found, subcommand found
|
||||||
|
context.setSubcommandGroup(group);
|
||||||
|
context.setSubcommand(subcommand);
|
||||||
|
return checkOptions(possibleCommands, 2, subcommand.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkOptions(String[] args, int skip, List<OptionData> options) {
|
||||||
|
if (context instanceof InteractionContext) {
|
||||||
|
context.setCurrentCommandOptions(args, options);
|
||||||
|
return true; // slash commands are validated by discord
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> arguments = new ArrayList<>(Arrays.stream(args).skip(skip).toList());
|
||||||
|
int required = options.stream().filter(OptionData::isRequired).toList().size();
|
||||||
|
int optional = options.stream().filter(not(OptionData::isRequired)).toList().size();
|
||||||
|
|
||||||
|
if (arguments.size() < required) {
|
||||||
|
context.reply(invalidUsage("missing one or more required arguments"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if last option is a string, if it is then combine all remaining strings into one
|
||||||
|
// ["@user", "you", "are", "banned"] becomes ["@user", "you are banned"]
|
||||||
|
if (!options.isEmpty() && arguments.size() > 1) {
|
||||||
|
OptionData last = options.get(options.size() - 1);
|
||||||
|
if (last.getType() == OptionType.STRING) {
|
||||||
|
List<String> lastArgs = arguments.stream().skip(options.size() - 1).toList();
|
||||||
|
arguments.removeAll(lastArgs);
|
||||||
|
arguments.add(String.join(" ", lastArgs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.size() > required + optional) {
|
||||||
|
context.reply(invalidUsage("too many arguments"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setCurrentCommandOptions(arguments.toArray(String[]::new), options);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canExecute(Command command) {
|
||||||
|
if (command.canBeExecutedFrom(context.getChannel())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context instanceof SlashCommandContext) {
|
||||||
|
return true; // no further checks since slash perms are handled by guild settings on discord
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.canBeExecutedBy(context.getGuildUser())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.needAdmin() && UserFunction.isAdmin(context.getGuildUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String invalidUsage(String message) {
|
||||||
|
return String.format("Invalid usage for command: %s\n" +
|
||||||
|
"Use **/help %s** for information on how to use this command", message, context.getCommand().getCommandName());
|
||||||
|
}
|
||||||
|
|
||||||
private void configureSentry(Command command) {
|
private void configureSentry(Command command) {
|
||||||
Sentry.configureScope(scope -> {
|
Sentry.configureScope(scope -> {
|
||||||
scope.setTag("command", command.getCommandName());
|
scope.setTag("command", command.getCommandName());
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Owns the bot command registry and slash-command registration against Discord.
|
|
||||||
*
|
|
||||||
* <p>As a {@link Lifecycle} component it discovers and loads commands on {@link #start()}.
|
|
||||||
*/
|
|
||||||
public interface CommandRegistry extends Lifecycle {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the container for the named command, or a null-object container if unknown.
|
|
||||||
*
|
|
||||||
* @param name the command name
|
|
||||||
* @return the command container
|
|
||||||
*/
|
|
||||||
CommandContainer getCommandContainer(String name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a command with the given name is registered.
|
|
||||||
*
|
|
||||||
* @param name the command name
|
|
||||||
* @return {@code true} if registered
|
|
||||||
*/
|
|
||||||
boolean hasCommand(String name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pushes every enabled slash command up to Discord via the given JDA instance.
|
|
||||||
*
|
|
||||||
* @param jda the connected JDA client
|
|
||||||
*/
|
|
||||||
void registerSlashCommands(JDA jda);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the live command map keyed by command name.
|
|
||||||
*
|
|
||||||
* @return the registered commands
|
|
||||||
*/
|
|
||||||
Map<String, CommandContainer> getCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.ERROR;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.NOACTION;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.TASK;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.WARN;
|
|
||||||
|
|
||||||
public class CommandResultHandler {
|
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(CommandResultHandler.class);
|
|
||||||
|
|
||||||
public void handle(CommandContext context, CommandExecutionResult commandExecutionResult, TaskManager taskManager) {
|
|
||||||
if (commandExecutionResult == null) {
|
|
||||||
throw new RuntimeException("CommandExecutionResult is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandExecutionResult.Result result = commandExecutionResult.getResult();
|
|
||||||
if (result == NOACTION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == TASK) {
|
|
||||||
taskManager.insert(context, commandExecutionResult.getTask());
|
|
||||||
}
|
|
||||||
|
|
||||||
String message = commandExecutionResult.getMessage();
|
|
||||||
if (result == ERROR) {
|
|
||||||
logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == WARN) {
|
|
||||||
logger.warn("Context[{}]: Warning from command: {}", context.getId(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message != null) {
|
|
||||||
context.reply(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class UsageValidator {
|
|
||||||
|
|
||||||
public boolean validate(CommandContext context, CommandContainer container) {
|
|
||||||
List<SubcommandGroupData> groups = container.getSubcommandGroups();
|
|
||||||
List<SubcommandData> subcommands = container.getSubcommands();
|
|
||||||
List<OptionData> options = container.getOptions();
|
|
||||||
String[] possibleCommands = Arrays.stream(context.getFullCommand().split(" ")).skip(1).toArray(String[]::new);
|
|
||||||
if (possibleCommands.length == 0) {
|
|
||||||
if (!groups.isEmpty()) {
|
|
||||||
context.reply(invalidUsage("missing subcommand group", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subcommands.isEmpty()) {
|
|
||||||
context.reply(invalidUsage("missing subcommand", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkOptions(context, possibleCommands, 0, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
SubcommandGroupData group = groups.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
|
||||||
if (group == null) {
|
|
||||||
SubcommandData subcommand = subcommands.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
|
||||||
if (subcommand == null) {
|
|
||||||
if (!groups.isEmpty()) {
|
|
||||||
context.reply(invalidUsage("unknown subcommand group", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subcommands.isEmpty()) {
|
|
||||||
context.reply(invalidUsage("unknown subcommand", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkOptions(context, possibleCommands, 0, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.setSubcommand(subcommand);
|
|
||||||
return checkOptions(context, possibleCommands, 1, subcommand.getOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (possibleCommands.length < 2) {
|
|
||||||
context.reply(invalidUsage("missing subcommand for command group", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SubcommandData subcommand = group.getSubcommands().stream().filter(data -> data.getName().equals(possibleCommands[1])).findFirst().orElse(null);
|
|
||||||
if (subcommand == null) {
|
|
||||||
context.reply(invalidUsage("unknown subcommand in command group", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.setSubcommandGroup(group);
|
|
||||||
context.setSubcommand(subcommand);
|
|
||||||
return checkOptions(context, possibleCommands, 2, subcommand.getOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkOptions(CommandContext context, String[] args, int skip, List<OptionData> options) {
|
|
||||||
if (context.isInteraction()) {
|
|
||||||
context.setCurrentCommandOptions(args, options);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> arguments = new ArrayList<>(Arrays.stream(args).skip(skip).toList());
|
|
||||||
int required = options.stream().filter(OptionData::isRequired).toList().size();
|
|
||||||
|
|
||||||
if (arguments.size() < required) {
|
|
||||||
context.reply(invalidUsage("missing one or more required arguments", context));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.setCurrentCommandOptions(arguments.toArray(String[]::new), options);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String invalidUsage(String message, CommandContext context) {
|
|
||||||
return String.format("Invalid usage for command: %s\n" +
|
|
||||||
"Use **/help %s** for information on how to use this command", message, context.getCommand().getCommandName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,7 @@ package me.bobert.netneutrality.core.command.options;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.entities.IMentionable;
|
import net.dv8tion.jda.api.entities.*;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
@@ -33,8 +29,10 @@ public interface CommandOption {
|
|||||||
* <br>This is represented as an {@link Message.Attachment#isEphemeral() ephemeral} attachment which will only be hosted for up to 2 weeks.
|
* <br>This is represented as an {@link Message.Attachment#isEphemeral() ephemeral} attachment which will only be hosted for up to 2 weeks.
|
||||||
* If you want a permanent reference, you must download it.
|
* If you want a permanent reference, you must download it.
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option {@link #getType() type} is not {@link OptionType#ATTACHMENT}
|
||||||
|
*
|
||||||
* @return {@link net.dv8tion.jda.api.entities.Message.Attachment Attachment}
|
* @return {@link net.dv8tion.jda.api.entities.Message.Attachment Attachment}
|
||||||
* @throws IllegalStateException If this option {@link #getType() type} is not {@link OptionType#ATTACHMENT}
|
|
||||||
*/
|
*/
|
||||||
Message.Attachment getAsAttachment();
|
Message.Attachment getAsAttachment();
|
||||||
|
|
||||||
@@ -50,8 +48,10 @@ public interface CommandOption {
|
|||||||
/**
|
/**
|
||||||
* The boolean value.
|
* The boolean value.
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option is not of type {@link OptionType#BOOLEAN BOOLEAN}
|
||||||
|
*
|
||||||
* @return The boolean value
|
* @return The boolean value
|
||||||
* @throws IllegalStateException If this option is not of type {@link OptionType#BOOLEAN BOOLEAN}
|
|
||||||
*/
|
*/
|
||||||
boolean getAsBoolean();
|
boolean getAsBoolean();
|
||||||
|
|
||||||
@@ -59,9 +59,12 @@ public interface CommandOption {
|
|||||||
* The long value for this option.
|
* The long value for this option.
|
||||||
* <br>This will be the ID of any resolved entity such as {@link Role} or {@link Member}.
|
* <br>This will be the ID of any resolved entity such as {@link Role} or {@link Member}.
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option {@link #getType() type} cannot be converted to a long
|
||||||
|
* @throws NumberFormatException
|
||||||
|
* If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value
|
||||||
|
*
|
||||||
* @return The long value
|
* @return The long value
|
||||||
* @throws IllegalStateException If this option {@link #getType() type} cannot be converted to a long
|
|
||||||
* @throws NumberFormatException If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value
|
|
||||||
*/
|
*/
|
||||||
long getAsLong();
|
long getAsLong();
|
||||||
|
|
||||||
@@ -71,27 +74,36 @@ public interface CommandOption {
|
|||||||
*
|
*
|
||||||
* <p><b>It is highly recommended to assert int values by using {@link OptionData#setRequiredRange(long, long)}</b>
|
* <p><b>It is highly recommended to assert int values by using {@link OptionData#setRequiredRange(long, long)}</b>
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option {@link #getType() type} cannot be converted to a long
|
||||||
|
* @throws NumberFormatException
|
||||||
|
* If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value
|
||||||
|
* @throws ArithmeticException
|
||||||
|
* If the provided integer value cannot fit into a 32bit signed int
|
||||||
|
*
|
||||||
* @return The int value
|
* @return The int value
|
||||||
* @throws IllegalStateException If this option {@link #getType() type} cannot be converted to a long
|
|
||||||
* @throws NumberFormatException If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value
|
|
||||||
* @throws ArithmeticException If the provided integer value cannot fit into a 32bit signed int
|
|
||||||
*/
|
*/
|
||||||
int getAsInt();
|
int getAsInt();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The double value for this option.
|
* The double value for this option.
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option {@link #getType() type} cannot be converted to a double
|
||||||
|
* @throws NumberFormatException
|
||||||
|
* If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid double value
|
||||||
|
*
|
||||||
* @return The double value
|
* @return The double value
|
||||||
* @throws IllegalStateException If this option {@link #getType() type} cannot be converted to a double
|
|
||||||
* @throws NumberFormatException If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid double value
|
|
||||||
*/
|
*/
|
||||||
double getAsDouble();
|
double getAsDouble();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The resolved {@link IMentionable} instance for this option value.
|
* The resolved {@link IMentionable} instance for this option value.
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If the mentioned entity is not resolvable
|
||||||
|
*
|
||||||
* @return The resolved {@link IMentionable}
|
* @return The resolved {@link IMentionable}
|
||||||
* @throws IllegalStateException If the mentioned entity is not resolvable
|
|
||||||
*/
|
*/
|
||||||
IMentionable getAsMentionable();
|
IMentionable getAsMentionable();
|
||||||
|
|
||||||
@@ -99,34 +111,42 @@ public interface CommandOption {
|
|||||||
* The resolved {@link Member} for this option value.
|
* The resolved {@link Member} for this option value.
|
||||||
* <br>Note that {@link OptionType#USER OptionType.USER} can also accept users that are not members of a guild, in which case this will be null!
|
* <br>Note that {@link OptionType#USER OptionType.USER} can also accept users that are not members of a guild, in which case this will be null!
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option is not of type {@link OptionType#USER USER} or {@link OptionType#MENTIONABLE MENTIONABLE}
|
||||||
|
*
|
||||||
* @return The resolved {@link Member}, or null
|
* @return The resolved {@link Member}, or null
|
||||||
* @throws IllegalStateException If this option is not of type {@link OptionType#USER USER} or {@link OptionType#MENTIONABLE MENTIONABLE}
|
|
||||||
*/
|
*/
|
||||||
Member getAsMember();
|
Member getAsMember();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The resolved {@link User} for this option value.
|
* The resolved {@link User} for this option value.
|
||||||
*
|
*
|
||||||
* @return The resolved {@link User}
|
* @throws IllegalStateException
|
||||||
* @throws IllegalStateException If this option is not of type {@link OptionType#USER USER} or
|
* If this option is not of type {@link OptionType#USER USER} or
|
||||||
* {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved user
|
* {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved user
|
||||||
|
*
|
||||||
|
* @return The resolved {@link User}
|
||||||
*/
|
*/
|
||||||
User getAsUser();
|
User getAsUser();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The resolved {@link Role} for this option value.
|
* The resolved {@link Role} for this option value.
|
||||||
*
|
*
|
||||||
* @return The resolved {@link Role}
|
* @throws IllegalStateException
|
||||||
* @throws IllegalStateException If this option is not of type {@link OptionType#ROLE ROLE} or
|
* If this option is not of type {@link OptionType#ROLE ROLE} or
|
||||||
* {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved role
|
* {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved role
|
||||||
|
*
|
||||||
|
* @return The resolved {@link Role}
|
||||||
*/
|
*/
|
||||||
Role getAsRole();
|
Role getAsRole();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link ChannelType} for the resolved channel.
|
* The {@link ChannelType} for the resolved channel.
|
||||||
*
|
*
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* If this option is not of type {@link OptionType#CHANNEL CHANNEL}
|
||||||
|
*
|
||||||
* @return The {@link ChannelType}
|
* @return The {@link ChannelType}
|
||||||
* @throws IllegalStateException If this option is not of type {@link OptionType#CHANNEL CHANNEL}
|
|
||||||
*/
|
*/
|
||||||
ChannelType getChannelType();
|
ChannelType getChannelType();
|
||||||
|
|
||||||
@@ -134,9 +154,11 @@ public interface CommandOption {
|
|||||||
* The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel} for this option value.
|
* The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel} for this option value.
|
||||||
* <br>Note that {@link OptionType#CHANNEL OptionType.CHANNEL} can accept channels of any type!
|
* <br>Note that {@link OptionType#CHANNEL OptionType.CHANNEL} can accept channels of any type!
|
||||||
*
|
*
|
||||||
* @return The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel}
|
* @throws IllegalStateException
|
||||||
* @throws IllegalStateException If this option is not of type {@link OptionType#CHANNEL CHANNEL}
|
* If this option is not of type {@link OptionType#CHANNEL CHANNEL}
|
||||||
* or could not be resolved for unexpected reasons
|
* or could not be resolved for unexpected reasons
|
||||||
|
*
|
||||||
|
* @return The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel}
|
||||||
*/
|
*/
|
||||||
GuildChannelUnion getAsChannel();
|
GuildChannelUnion getAsChannel();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package me.bobert.netneutrality.core.command.options;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
||||||
|
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
public class CommandOptions {
|
||||||
|
private final Command command;
|
||||||
|
private final CommandDataImpl commandData;
|
||||||
|
private final List<OptionData> options;
|
||||||
|
private final List<SubcommandData> subcommands;
|
||||||
|
private final List<SubcommandGroupData> subcommandGroups;
|
||||||
|
|
||||||
|
public CommandOptions(Command command, CommandDataImpl commandData) {
|
||||||
|
this.command = command;
|
||||||
|
this.commandData = commandData;
|
||||||
|
this.options = commandData.getOptions();
|
||||||
|
this.subcommands = commandData.getSubcommands();
|
||||||
|
this.subcommandGroups = commandData.getSubcommandGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Command getCommand() {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SlashCommandData getSlashCommandData() {
|
||||||
|
return commandData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OptionData> getOptions() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SubcommandData> getSubcommands() {
|
||||||
|
return subcommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SubcommandGroupData> getSubcommandGroups() {
|
||||||
|
return subcommandGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return options.isEmpty() && subcommands.isEmpty() && subcommandGroups.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String printUsage() {
|
||||||
|
StringBuilder usage = new StringBuilder();
|
||||||
|
if (!options.isEmpty()) {
|
||||||
|
usage.append("- Usage: /").append(command.getCommandName());
|
||||||
|
for (OptionData option : options) {
|
||||||
|
usage.append(" <").append(option.getName()).append(">");
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.append(printOptions(options, null, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subcommands.isEmpty()) {
|
||||||
|
for (SubcommandData subcommand : subcommands) {
|
||||||
|
usage.append("- Usage: /").append(command.getCommandName()).append(" ").append(subcommand.getName());
|
||||||
|
|
||||||
|
for (OptionData option : subcommand.getOptions()) {
|
||||||
|
usage.append(" <").append(option.getName()).append(">");
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.append(" (").append(subcommand.getDescription()).append(")");
|
||||||
|
usage.append(printOptions(subcommand.getOptions(), null, false));
|
||||||
|
|
||||||
|
usage.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subcommandGroups.isEmpty()) {
|
||||||
|
for (SubcommandGroupData subcommandGroup : subcommandGroups) {
|
||||||
|
usage.append("- Subcommand: /").append(command.getCommandName())
|
||||||
|
.append(" ").append(subcommandGroup.getName())
|
||||||
|
.append(" (").append(subcommandGroup.getDescription()).append(")")
|
||||||
|
.append("\n");
|
||||||
|
|
||||||
|
for (SubcommandData subcommand : subcommandGroup.getSubcommands()) {
|
||||||
|
usage.append(" - Usage: /").append(command.getCommandName())
|
||||||
|
.append(" ").append(subcommandGroup.getName())
|
||||||
|
.append(" ").append(subcommand.getName());
|
||||||
|
|
||||||
|
for (OptionData option : subcommand.getOptions()) {
|
||||||
|
usage.append(" <").append(option.getName()).append(">");
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.append(printOptions(subcommand.getOptions(), subcommand.getDescription(), true));
|
||||||
|
|
||||||
|
usage.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty()) {
|
||||||
|
usage.append("\n\n-# *: required argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
return usage.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String printOptions(List<OptionData> options, String description, boolean group) {
|
||||||
|
StringBuilder usage = new StringBuilder();
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
usage.append(" (").append(description).append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (OptionData option : options) {
|
||||||
|
usage.append("\n");
|
||||||
|
if (group) usage.append(" ");
|
||||||
|
usage.append(" - ");
|
||||||
|
if (option.isRequired()) {
|
||||||
|
usage.append("*");
|
||||||
|
}
|
||||||
|
usage.append(option.getName()).append(": ").append(option.getDescription());
|
||||||
|
|
||||||
|
if (option.getMinValue() != null) {
|
||||||
|
usage.append(" [Min value: ").append(option.getMinValue().longValue()).append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.getMaxValue() != null) {
|
||||||
|
usage.append(" [Max value: ").append(option.getMaxValue().longValue()).append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.getMinLength() != null) {
|
||||||
|
usage.append(" [Min length: ").append(option.getMinLength().longValue()).append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.getMaxLength() != null) {
|
||||||
|
usage.append(" [Max length: ").append(option.getMaxLength().longValue()).append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<net.dv8tion.jda.api.interactions.commands.Command.Choice> choices = option.getChoices();
|
||||||
|
if (!choices.isEmpty()) {
|
||||||
|
StringJoiner joiner = new StringJoiner(", ");
|
||||||
|
for (net.dv8tion.jda.api.interactions.commands.Command.Choice choice : choices) {
|
||||||
|
joiner.add(choice.getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.append(" [Available choices: ").append(joiner).append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ChannelType> requiredChannelTypes = option.getChannelTypes();
|
||||||
|
if (!requiredChannelTypes.isEmpty()) {
|
||||||
|
StringJoiner joiner = new StringJoiner(", ");
|
||||||
|
for (ChannelType type : requiredChannelTypes) {
|
||||||
|
joiner.add(type.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.append(" [Required channel types: ").append(joiner).append("]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usage.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-26
@@ -1,35 +1,30 @@
|
|||||||
package me.bobert.netneutrality.core.command;
|
package me.bobert.netneutrality.core.command.options;
|
||||||
/*
|
/*
|
||||||
* Copyright (c) anthorob
|
* Copyright (c) anthorob
|
||||||
*
|
*
|
||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
import net.dv8tion.jda.api.interactions.DiscordLocale;
|
import net.dv8tion.jda.api.interactions.DiscordLocale;
|
||||||
import net.dv8tion.jda.api.interactions.IntegrationType;
|
|
||||||
import net.dv8tion.jda.api.interactions.InteractionContextType;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
|
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
import net.dv8tion.jda.api.interactions.commands.build.*;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction;
|
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction;
|
||||||
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationMap;
|
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationMap;
|
||||||
import net.dv8tion.jda.api.utils.data.DataObject;
|
import net.dv8tion.jda.api.utils.data.DataObject;
|
||||||
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
|
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class CommandOptionsBuilder implements SlashCommandData {
|
public class CommandOptionsBuilder implements SlashCommandData {
|
||||||
|
private final Command command;
|
||||||
private CommandDataImpl commandData;
|
private CommandDataImpl commandData;
|
||||||
|
|
||||||
public CommandOptionsBuilder(Command.Type type, Command command) {
|
public CommandOptionsBuilder(Command.Type type, Command command) {
|
||||||
|
this.command = command;
|
||||||
|
|
||||||
if (type == Command.Type.SLASH) {
|
if (type == Command.Type.SLASH) {
|
||||||
commandData = (CommandDataImpl) Commands.slash(command.getCommandName(), command.getDescription());
|
commandData = (CommandDataImpl) Commands.slash(command.getCommandName(), command.getDescription());
|
||||||
@@ -44,8 +39,8 @@ public class CommandOptionsBuilder implements SlashCommandData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandDataImpl build() {
|
public CommandOptions build() {
|
||||||
return commandData;
|
return new CommandOptions(command, commandData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -74,13 +69,8 @@ public class CommandOptionsBuilder implements SlashCommandData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nonnull SlashCommandData setContexts(@Nonnull Collection<InteractionContextType> contexts) {
|
public @Nonnull SlashCommandData setGuildOnly(boolean guildOnly) {
|
||||||
return commandData.setContexts(contexts);
|
return commandData.setGuildOnly(guildOnly);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nonnull SlashCommandData setIntegrationTypes(@Nonnull Collection<IntegrationType> integrationTypes) {
|
|
||||||
return commandData.setIntegrationTypes(integrationTypes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -109,13 +99,8 @@ public class CommandOptionsBuilder implements SlashCommandData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nonnull Set<InteractionContextType> getContexts() {
|
public boolean isGuildOnly() {
|
||||||
return commandData.getContexts();
|
return commandData.isGuildOnly();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nonnull Set<IntegrationType> getIntegrationTypes() {
|
|
||||||
return commandData.getIntegrationTypes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
+1
-9
@@ -5,11 +5,7 @@ package me.bobert.netneutrality.core.command.options;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.entities.IMentionable;
|
import net.dv8tion.jda.api.entities.*;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
import net.dv8tion.jda.api.interactions.commands.OptionType;
|
||||||
@@ -120,10 +116,6 @@ public class MessageCommandOption implements CommandOption {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User getAsUser() {
|
public User getAsUser() {
|
||||||
if (value instanceof Member) {
|
|
||||||
return ((Member) value).getUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof User) {
|
if (value instanceof User) {
|
||||||
return (User) value;
|
return (User) value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ package me.bobert.netneutrality.core.command.options;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.entities.IMentionable;
|
import net.dv8tion.jda.api.entities.*;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
|
||||||
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
|
||||||
|
|||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command.precondition;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
|
|
||||||
public interface CommandPrecondition {
|
|
||||||
/**
|
|
||||||
* Returns true if the precondition passes and execution should continue,
|
|
||||||
* or false if it failed (the implementation is responsible for sending any reply).
|
|
||||||
*/
|
|
||||||
boolean check(CommandContext context, Command command);
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command.precondition;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
import me.bobert.netneutrality.db.GSettings;
|
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class DisabledCommandPrecondition implements CommandPrecondition {
|
|
||||||
|
|
||||||
private static volatile GuildServiceProvider guildServices;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the guild-service provider used to resolve the guild's configured {@code DISABLED_COMMANDS}.
|
|
||||||
* Called once during bootstrap.
|
|
||||||
*
|
|
||||||
* @param provider the live guild service provider
|
|
||||||
*/
|
|
||||||
public static void bind(GuildServiceProvider provider) {
|
|
||||||
guildServices = provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(CommandContext context, Command command) {
|
|
||||||
GuildServiceProvider provider = guildServices;
|
|
||||||
if (provider == null || context.getGuild() == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String disabledCommands = provider.get(context.getGuild().getId()).getGuildSettingService().getOrDefault(GSettings.DISABLED_COMMANDS);
|
|
||||||
if (disabledCommands.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean disabled = Arrays.stream(disabledCommands.split(","))
|
|
||||||
.map(String::trim)
|
|
||||||
.anyMatch(name -> name.equalsIgnoreCase(command.getCommandName()));
|
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
context.replyEphemeral("This command is disabled on this server");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command.precondition;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
|
|
||||||
public class GuildOnlyPrecondition implements CommandPrecondition {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(CommandContext context, Command command) {
|
|
||||||
if (command.isGuildOnly() && !context.isFromGuild()) {
|
|
||||||
context.replyEphemeral("This command is only available in a guild");
|
|
||||||
context.deleteMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command.precondition;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
import me.bobert.netneutrality.utils.UserFunction;
|
|
||||||
|
|
||||||
public class PermissionPrecondition implements CommandPrecondition {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(CommandContext context, Command command) {
|
|
||||||
if (command.getType() != Command.Type.SLASH) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.canSkipPermissionCheck()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!command.canBeExecutedFrom(context.getChannel())) {
|
|
||||||
context.replyEphemeral("This command cannot be executed in the current channel");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.needAdmin() && !UserFunction.isAdmin(context.getGuildUser())) {
|
|
||||||
context.replyEphemeral("You are not powerful enough, scrub");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!command.canBeExecutedBy(context.getGuildUser())) {
|
|
||||||
context.replyEphemeral("You are not powerful enough, scrub");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package me.bobert.netneutrality.core.command.precondition;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
|
||||||
|
|
||||||
public class SlashCommandOnlyPrecondition implements CommandPrecondition {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(CommandContext context, Command command) {
|
|
||||||
if (command.isSlashCommandOnly() && !context.isInteraction()) {
|
|
||||||
context.replyEphemeral("This command is only available as a slash command");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ package me.bobert.netneutrality.core.context;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import io.sentry.Sentry;
|
import io.sentry.Sentry;
|
||||||
|
import me.bobert.netneutrality.context.command.SlashCommandContext;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package me.bobert.netneutrality.core.context;
|
|||||||
|
|
||||||
import me.bobert.netneutrality.core.command.Command;
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
import me.bobert.netneutrality.utils.Settings;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
@@ -17,12 +17,7 @@ import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
|||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -33,12 +28,10 @@ public abstract class CommandContext extends AbstractContext {
|
|||||||
private SubcommandData subcommand;
|
private SubcommandData subcommand;
|
||||||
private final Map <OptionData, String> invalidOptions = new HashMap<>();
|
private final Map <OptionData, String> invalidOptions = new HashMap<>();
|
||||||
private final List<CommandOption> options;
|
private final List<CommandOption> options;
|
||||||
private final SettingsProvider settings;
|
|
||||||
|
|
||||||
public CommandContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user, SettingsProvider settings) {
|
public CommandContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
|
||||||
super(jda, guild, channel, member, user);
|
super(jda, guild, channel, member, user);
|
||||||
this.options = new ArrayList<>();
|
this.options = new ArrayList<>();
|
||||||
this.settings = settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command getCommand() {
|
public Command getCommand() {
|
||||||
@@ -77,20 +70,16 @@ public abstract class CommandContext extends AbstractContext {
|
|||||||
public <T> T getOption(String name, Supplier<? extends T> fallback, Function<? super CommandOption, ? extends T> resolver) {
|
public <T> T getOption(String name, Supplier<? extends T> fallback, Function<? super CommandOption, ? extends T> resolver) {
|
||||||
Objects.requireNonNull(resolver, "resolver cannot be null");
|
Objects.requireNonNull(resolver, "resolver cannot be null");
|
||||||
CommandOption mapping = getOption(name);
|
CommandOption mapping = getOption(name);
|
||||||
if (mapping != null) {
|
if (mapping != null)
|
||||||
return resolver.apply(mapping);
|
return resolver.apply(mapping);
|
||||||
}
|
|
||||||
|
|
||||||
return fallback == null ? null : fallback.get();
|
return fallback == null ? null : fallback.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T getOption(String name, T fallback, Function<? super CommandOption, ? extends T> resolver) {
|
public <T> T getOption(String name, T fallback, Function<? super CommandOption, ? extends T> resolver) {
|
||||||
Objects.requireNonNull(resolver, "resolver cannot be null");
|
Objects.requireNonNull(resolver, "resolver cannot be null");
|
||||||
CommandOption mapping = getOption(name);
|
CommandOption mapping = getOption(name);
|
||||||
if (mapping != null) {
|
if (mapping != null)
|
||||||
return resolver.apply(mapping);
|
return resolver.apply(mapping);
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +89,7 @@ public abstract class CommandContext extends AbstractContext {
|
|||||||
|
|
||||||
public CommandOption getOption(String name) {
|
public CommandOption getOption(String name) {
|
||||||
List<CommandOption> options = getOptionsByName(name);
|
List<CommandOption> options = getOptionsByName(name);
|
||||||
return options.isEmpty() ? null : options.getFirst();
|
return options.isEmpty() ? null : options.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CommandOption> getOptionsByName(String name) {
|
public List<CommandOption> getOptionsByName(String name) {
|
||||||
@@ -133,7 +122,7 @@ public abstract class CommandContext extends AbstractContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMessageFromCreator() {
|
public boolean isMessageFromCreator() {
|
||||||
String creatorId = settings.get("NETNEUTRALITY_BOT_OWNER");
|
String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER");
|
||||||
|
|
||||||
return creatorId.equals(getDiscordUser().getId());
|
return creatorId.equals(getDiscordUser().getId());
|
||||||
}
|
}
|
||||||
@@ -144,11 +133,6 @@ public abstract class CommandContext extends AbstractContext {
|
|||||||
|
|
||||||
public abstract String getFullCommand();
|
public abstract String getFullCommand();
|
||||||
|
|
||||||
public boolean isInteraction() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean canSkipPermissionCheck() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,19 +5,16 @@ package me.bobert.netneutrality.core.context;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.context.AsyncContext;
|
||||||
import me.bobert.netneutrality.utils.ContextUtils;
|
import me.bobert.netneutrality.utils.ContextUtils;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.components.MessageTopLevelComponent;
|
import net.dv8tion.jda.api.entities.*;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||||
import net.dv8tion.jda.api.events.Event;
|
import net.dv8tion.jda.api.events.Event;
|
||||||
import net.dv8tion.jda.api.interactions.InteractionHook;
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
import net.dv8tion.jda.api.modals.Modal;
|
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
|
||||||
|
import net.dv8tion.jda.api.interactions.modals.Modal;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
import net.dv8tion.jda.api.utils.messages.MessageEditData;
|
||||||
|
|
||||||
@@ -51,7 +48,7 @@ public interface Context {
|
|||||||
|
|
||||||
void reply(Modal modal);
|
void reply(Modal modal);
|
||||||
|
|
||||||
void reply(MessageCreateData messageCreateData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer);
|
void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer);
|
||||||
|
|
||||||
void defer(boolean ephemeral, Consumer<AsyncContext> consumer);
|
void defer(boolean ephemeral, Consumer<AsyncContext> consumer);
|
||||||
|
|
||||||
@@ -69,7 +66,7 @@ public interface Context {
|
|||||||
reply(messageCreateData, false, List.of(), asyncMessage -> {});
|
reply(messageCreateData, false, List.of(), asyncMessage -> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
default void reply(MessageCreateData messageCreateData, List<MessageTopLevelComponent> components) {
|
default void reply(MessageCreateData messageCreateData, List<LayoutComponent> components) {
|
||||||
reply(messageCreateData, false, components, asyncMessage -> {});
|
reply(messageCreateData, false, components, asyncMessage -> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +90,7 @@ public interface Context {
|
|||||||
reply(MessageCreateData.fromEmbeds(embeds));
|
reply(MessageCreateData.fromEmbeds(embeds));
|
||||||
}
|
}
|
||||||
|
|
||||||
default void edit(MessageEditData messageEditData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
default void edit(MessageEditData messageEditData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (isAcknowledged() && getInteractionHook() != null) {
|
if (isAcknowledged() && getInteractionHook() != null) {
|
||||||
getInteractionHook().editOriginal(messageEditData).setComponents(components).queue(message1 -> consumer.accept(new AsyncContext(message1)));
|
getInteractionHook().editOriginal(messageEditData).setComponents(components).queue(message1 -> consumer.accept(new AsyncContext(message1)));
|
||||||
return;
|
return;
|
||||||
@@ -102,7 +99,7 @@ public interface Context {
|
|||||||
reply(MessageCreateData.fromEditData(messageEditData), ephemeral, components, consumer);
|
reply(MessageCreateData.fromEditData(messageEditData), ephemeral, components, consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void edit(MessageEditData messageEditData, List<MessageTopLevelComponent> components) {
|
default void edit(MessageEditData messageEditData, List<LayoutComponent> components) {
|
||||||
edit(messageEditData, false, components, asyncMessage -> {});
|
edit(messageEditData, false, components, asyncMessage -> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +115,11 @@ public interface Context {
|
|||||||
edit(List.of(embed), List.of());
|
edit(List.of(embed), List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
default void edit(List<MessageEmbed> embeds, List<MessageTopLevelComponent> components) {
|
default void edit(List<MessageEmbed> embeds, List<LayoutComponent> components) {
|
||||||
edit(MessageEditData.fromEmbeds(embeds), components);
|
edit(MessageEditData.fromEmbeds(embeds), components);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void edit(MessageEmbed embed, List<MessageTopLevelComponent> components) {
|
default void edit(MessageEmbed embed, List<LayoutComponent> components) {
|
||||||
edit(List.of(embed), components);
|
edit(List.of(embed), components);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +135,7 @@ public interface Context {
|
|||||||
defer(false, consumer);
|
defer(false, consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void replyEphemeral(MessageCreateData messageCreateData, List<MessageTopLevelComponent> components) {
|
default void replyEphemeral(MessageCreateData messageCreateData, List<LayoutComponent> components) {
|
||||||
reply(messageCreateData, true, components, asyncMessage -> {});
|
reply(messageCreateData, true, components, asyncMessage -> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +147,7 @@ public interface Context {
|
|||||||
replyEphemeralWithLimit(message);
|
replyEphemeralWithLimit(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void replyEphemeral(String message, MessageTopLevelComponent component) {
|
default void replyEphemeral(String message, List<LayoutComponent> components) {
|
||||||
replyEphemeral(message, List.of(component));
|
|
||||||
}
|
|
||||||
|
|
||||||
default void replyEphemeral(String message, List<MessageTopLevelComponent> components) {
|
|
||||||
replyEphemeral(MessageCreateData.fromContent(message), components);
|
replyEphemeral(MessageCreateData.fromContent(message), components);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +155,7 @@ public interface Context {
|
|||||||
replyEphemeral(MessageCreateData.fromEmbeds(embeds));
|
replyEphemeral(MessageCreateData.fromEmbeds(embeds));
|
||||||
}
|
}
|
||||||
|
|
||||||
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
User user = getDiscordUser();
|
User user = getDiscordUser();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.openPrivateChannel()
|
user.openPrivateChannel()
|
||||||
@@ -171,11 +164,11 @@ public interface Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default void sendMessageInPrivate(String message, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
default void sendMessageInPrivate(String message, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
sendMessageInPrivate(MessageCreateData.fromContent(message), components, consumer);
|
sendMessageInPrivate(MessageCreateData.fromContent(message), components, consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<MessageTopLevelComponent> components) {
|
default void sendMessageInPrivate(MessageCreateData messageCreateData, List<LayoutComponent> components) {
|
||||||
sendMessageInPrivate(messageCreateData, components, asyncContext -> {});
|
sendMessageInPrivate(messageCreateData, components, asyncContext -> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ package me.bobert.netneutrality.core.context;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.components.MessageTopLevelComponent;
|
import me.bobert.netneutrality.context.AsyncContext;
|
||||||
import net.dv8tion.jda.api.interactions.Interaction;
|
import net.dv8tion.jda.api.interactions.Interaction;
|
||||||
import net.dv8tion.jda.api.interactions.InteractionHook;
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
import net.dv8tion.jda.api.interactions.callbacks.IModalCallback;
|
import net.dv8tion.jda.api.interactions.callbacks.IModalCallback;
|
||||||
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
|
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
|
||||||
import net.dv8tion.jda.api.modals.Modal;
|
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
|
||||||
|
import net.dv8tion.jda.api.interactions.modals.Modal;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -57,7 +58,7 @@ public interface InteractionContext extends Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void reply(MessageCreateData messageCreateData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
default void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (isAcknowledged()) {
|
if (isAcknowledged()) {
|
||||||
getInteractionHook().sendMessage(messageCreateData).setEphemeral(ephemeral).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
|
getInteractionHook().sendMessage(messageCreateData).setEphemeral(ephemeral).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ package me.bobert.netneutrality.core.context;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net.dv8tion.jda.api.components.MessageTopLevelComponent;
|
import me.bobert.netneutrality.context.AsyncContext;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
|
import net.dv8tion.jda.api.interactions.components.LayoutComponent;
|
||||||
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -31,7 +32,7 @@ public interface MessageContext extends NoMessageContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void reply(MessageCreateData messageCreateData, boolean ephemeral, List<MessageTopLevelComponent> components, Consumer<AsyncContext> consumer) {
|
default void reply(MessageCreateData messageCreateData, boolean ephemeral, List<LayoutComponent> components, Consumer<AsyncContext> consumer) {
|
||||||
if (ephemeral && getType() != ChannelType.PRIVATE) {
|
if (ephemeral && getType() != ChannelType.PRIVATE) {
|
||||||
sendMessageInPrivate(messageCreateData, components);
|
sendMessageInPrivate(messageCreateData, components);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ package me.bobert.netneutrality.core.context;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.context.AsyncContext;
|
||||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
import net.dv8tion.jda.api.interactions.InteractionHook;
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
import net.dv8tion.jda.api.modals.Modal;
|
import net.dv8tion.jda.api.interactions.modals.Modal;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 314 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user