i forgot to make small contributions
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# intellij
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# eclipse
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# macos
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# logs
|
||||||
|
*.log
|
||||||
58
build.gradle.kts
Normal file
58
build.gradle.kts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
plugins {
|
||||||
|
java
|
||||||
|
id("com.gradleup.shadow") version "8.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "party.cybsec"
|
||||||
|
version = "1.0.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://repo.papermc.io/repository/maven-public/")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// paper api
|
||||||
|
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
|
||||||
|
|
||||||
|
// sqlite
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.47.1.0")
|
||||||
|
|
||||||
|
// jda discord bot
|
||||||
|
implementation("net.dv8tion:JDA:6.3.0") {
|
||||||
|
exclude(module = "opus-java")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
shadowJar {
|
||||||
|
archiveClassifier.set("")
|
||||||
|
|
||||||
|
// relocate dependencies to avoid conflicts
|
||||||
|
relocate("org.xerial", "party.cybsec.oyetickets.libs.xerial")
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
archiveClassifier.set("slim")
|
||||||
|
}
|
||||||
|
|
||||||
|
build {
|
||||||
|
dependsOn(shadowJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
val props = mapOf("version" to version)
|
||||||
|
inputs.properties(props)
|
||||||
|
filteringCharset = "UTF-8"
|
||||||
|
filesMatching("plugin.yml") {
|
||||||
|
expand(props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
gradle.properties
Normal file
3
gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2G
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
248
gradlew
vendored
Executable file
248
gradlew
vendored
Executable file
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
93
gradlew.bat
vendored
Normal file
93
gradlew.bat
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
1
settings.gradle.kts
Normal file
1
settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "oyetickets"
|
||||||
98
src/main/java/party/cybsec/oyetickets/OyeTicketsPlugin.java
Normal file
98
src/main/java/party/cybsec/oyetickets/OyeTicketsPlugin.java
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package party.cybsec.oyetickets;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.command.AdminCommand;
|
||||||
|
import party.cybsec.oyetickets.command.TicketCommand;
|
||||||
|
import party.cybsec.oyetickets.command.TicketsCommand;
|
||||||
|
import party.cybsec.oyetickets.config.Config;
|
||||||
|
import party.cybsec.oyetickets.database.Database;
|
||||||
|
import party.cybsec.oyetickets.discord.DiscordBot;
|
||||||
|
import party.cybsec.oyetickets.session.SessionManager;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
public final class OyeTicketsPlugin extends JavaPlugin {
|
||||||
|
|
||||||
|
private static OyeTicketsPlugin instance;
|
||||||
|
private Database database;
|
||||||
|
private Config pluginConfig;
|
||||||
|
private DiscordBot discordBot;
|
||||||
|
private SessionManager sessionManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
instance = this;
|
||||||
|
|
||||||
|
// save default config
|
||||||
|
saveDefaultConfig();
|
||||||
|
|
||||||
|
// load config
|
||||||
|
this.pluginConfig = new Config(this);
|
||||||
|
|
||||||
|
// initialize database
|
||||||
|
this.database = new Database(this);
|
||||||
|
if (!database.initialize()) {
|
||||||
|
getLogger().severe("failed to initialize database, disabling plugin");
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// init session manager
|
||||||
|
this.sessionManager = new SessionManager();
|
||||||
|
|
||||||
|
// init discord bot
|
||||||
|
if (pluginConfig.isDiscordEnabled()) {
|
||||||
|
this.discordBot = new DiscordBot(this);
|
||||||
|
if (!discordBot.start()) {
|
||||||
|
getLogger().warning("failed to start discord bot, continuing without discord integration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register commands
|
||||||
|
getCommand("ticket").setExecutor(new TicketCommand(this));
|
||||||
|
getCommand("tickets").setExecutor(new TicketsCommand(this));
|
||||||
|
AdminCommand adminCommand = new AdminCommand(this);
|
||||||
|
getCommand("oyetickets").setExecutor(adminCommand);
|
||||||
|
getCommand("oyetickets").setTabCompleter(adminCommand);
|
||||||
|
|
||||||
|
getLogger().info("oyetickets enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
// cleanup sessions
|
||||||
|
if (sessionManager != null) {
|
||||||
|
sessionManager.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown discord bot
|
||||||
|
if (discordBot != null) {
|
||||||
|
discordBot.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// close database
|
||||||
|
if (database != null) {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogger().info("oyetickets disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OyeTicketsPlugin getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Database getDatabase() {
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Config getPluginConfig() {
|
||||||
|
return pluginConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscordBot getDiscordBot() {
|
||||||
|
return discordBot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionManager getSessionManager() {
|
||||||
|
return sessionManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package party.cybsec.oyetickets.command;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AdminCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
|
||||||
|
public AdminCommand(OyeTicketsPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
|
||||||
|
@NotNull String[] args) {
|
||||||
|
if (!sender.hasPermission("tickets.admin")) {
|
||||||
|
sender.sendMessage(Component.text("no permission").color(NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 0) {
|
||||||
|
sendHelp(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "reload" -> {
|
||||||
|
plugin.getPluginConfig().reload();
|
||||||
|
plugin.getDiscordBot().shutdown();
|
||||||
|
plugin.getDiscordBot().start();
|
||||||
|
sender.sendMessage(
|
||||||
|
Component.text("oyetickets config and discord bot reloaded").color(NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
case "status" -> {
|
||||||
|
sender.sendMessage(Component.text("--- oyetickets status ---").color(NamedTextColor.GOLD));
|
||||||
|
sender.sendMessage(Component
|
||||||
|
.text("discord: " + (plugin.getDiscordBot().getJda() != null ? "connected" : "disconnected"))
|
||||||
|
.color(NamedTextColor.GRAY));
|
||||||
|
sender.sendMessage(Component.text("database: connected").color(NamedTextColor.GRAY));
|
||||||
|
}
|
||||||
|
default -> sendHelp(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHelp(CommandSender sender) {
|
||||||
|
sender.sendMessage(Component.text("--- oyetickets admin ---").color(NamedTextColor.GOLD));
|
||||||
|
sender.sendMessage(Component.text("/oyetickets reload - reload configuration").color(NamedTextColor.GRAY));
|
||||||
|
sender.sendMessage(Component.text("/oyetickets status - show plugin status").color(NamedTextColor.GRAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||||
|
@NotNull String alias, @NotNull String[] args) {
|
||||||
|
if (!sender.hasPermission("tickets.admin"))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (args.length == 1) {
|
||||||
|
return List.of("reload", "status");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package party.cybsec.oyetickets.command;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.session.TicketSession;
|
||||||
|
import party.cybsec.oyetickets.ui.player.NavigationDialog;
|
||||||
|
import party.cybsec.oyetickets.util.PermissionUtil;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public class TicketCommand implements CommandExecutor {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
|
||||||
|
public TicketCommand(OyeTicketsPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||||
|
@NotNull String label, @NotNull String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage(Component.text("only players can create tickets")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permission
|
||||||
|
if (!PermissionUtil.checkAndNotify(player, "tickets.create")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check cooldown
|
||||||
|
if (plugin.getPluginConfig().isCooldownEnabled()) {
|
||||||
|
try {
|
||||||
|
int cooldownSeconds = plugin.getPluginConfig().getCooldownDuration();
|
||||||
|
if (plugin.getDatabase().isOnCooldown(player.getUniqueId(), cooldownSeconds)) {
|
||||||
|
player.sendMessage(Component.text("you're creating tickets too quickly, please wait")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("failed to check cooldown: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create or get session
|
||||||
|
TicketSession session = plugin.getSessionManager().getOrCreateSession(player);
|
||||||
|
|
||||||
|
// capture location immediately
|
||||||
|
session.setLocation(player.getLocation());
|
||||||
|
|
||||||
|
// open navigation dialog
|
||||||
|
new NavigationDialog(plugin, player, session).open();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package party.cybsec.oyetickets.command;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.ui.staff.TicketBrowserDialog;
|
||||||
|
import party.cybsec.oyetickets.util.PermissionUtil;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public class TicketsCommand implements CommandExecutor {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
|
||||||
|
public TicketsCommand(OyeTicketsPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||||
|
@NotNull String label, @NotNull String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage(Component.text("only players can view tickets")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permission
|
||||||
|
if (!PermissionUtil.checkAndNotify(player, "tickets.view")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// open ticket browser
|
||||||
|
new TicketBrowserDialog(plugin, player).open();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/main/java/party/cybsec/oyetickets/config/Config.java
Normal file
56
src/main/java/party/cybsec/oyetickets/config/Config.java
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package party.cybsec.oyetickets.config;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
public class Config {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private FileConfiguration config;
|
||||||
|
|
||||||
|
public Config(OyeTicketsPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.config = plugin.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
plugin.reloadConfig();
|
||||||
|
this.config = plugin.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// discord
|
||||||
|
public boolean isDiscordEnabled() {
|
||||||
|
return config.getBoolean("discord.enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDiscordBotToken() {
|
||||||
|
return config.getString("discord.bot-token", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDiscordChannelId() {
|
||||||
|
return config.getString("discord.channel-id", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// cooldown
|
||||||
|
public boolean isCooldownEnabled() {
|
||||||
|
return config.getBoolean("cooldown.enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCooldownDuration() {
|
||||||
|
return config.getInt("cooldown.duration-seconds", 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate detection
|
||||||
|
public boolean isDuplicateDetectionEnabled() {
|
||||||
|
return config.getBoolean("duplicate-detection.enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getSimilarityThreshold() {
|
||||||
|
return config.getDouble("duplicate-detection.similarity-threshold", 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// guidelines
|
||||||
|
public String getGuidelines() {
|
||||||
|
return config.getString("guidelines", "no guidelines configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
351
src/main/java/party/cybsec/oyetickets/database/Database.java
Normal file
351
src/main/java/party/cybsec/oyetickets/database/Database.java
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
package party.cybsec.oyetickets.database;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.Ticket;
|
||||||
|
import party.cybsec.oyetickets.model.TicketCategory;
|
||||||
|
import party.cybsec.oyetickets.model.TicketStatus;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Database {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private Connection connection;
|
||||||
|
|
||||||
|
public Database(OyeTicketsPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean initialize() {
|
||||||
|
try {
|
||||||
|
File dataFolder = plugin.getDataFolder();
|
||||||
|
if (!dataFolder.exists()) {
|
||||||
|
dataFolder.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
File dbFile = new File(dataFolder, "tickets.db");
|
||||||
|
String url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
|
||||||
|
|
||||||
|
connection = DriverManager.getConnection(url);
|
||||||
|
|
||||||
|
// enable foreign keys
|
||||||
|
try (Statement stmt = connection.createStatement()) {
|
||||||
|
stmt.execute("PRAGMA foreign_keys = ON");
|
||||||
|
}
|
||||||
|
|
||||||
|
// run migrations
|
||||||
|
runMigrations();
|
||||||
|
|
||||||
|
plugin.getLogger().info("database initialized");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("failed to initialize database: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runMigrations() throws SQLException {
|
||||||
|
// create schema version table
|
||||||
|
try (Statement stmt = connection.createStatement()) {
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
|
version INTEGER PRIMARY KEY,
|
||||||
|
applied_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentVersion = getCurrentVersion();
|
||||||
|
|
||||||
|
// migration 1: initial schema
|
||||||
|
if (currentVersion < 1) {
|
||||||
|
try (Statement stmt = connection.createStatement()) {
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE tickets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
uuid TEXT NOT NULL,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
short_desc TEXT NOT NULL,
|
||||||
|
long_desc TEXT,
|
||||||
|
world TEXT NOT NULL,
|
||||||
|
x REAL NOT NULL,
|
||||||
|
y REAL NOT NULL,
|
||||||
|
z REAL NOT NULL,
|
||||||
|
yaw REAL NOT NULL,
|
||||||
|
pitch REAL NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'NONE',
|
||||||
|
assigned_to TEXT,
|
||||||
|
discord_thread_id TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE staff_filters (
|
||||||
|
uuid TEXT PRIMARY KEY,
|
||||||
|
filter_unseen BOOLEAN DEFAULT 1,
|
||||||
|
filter_active BOOLEAN DEFAULT 1,
|
||||||
|
filter_waiting BOOLEAN DEFAULT 1,
|
||||||
|
filter_solved BOOLEAN DEFAULT 0,
|
||||||
|
filter_closed BOOLEAN DEFAULT 0
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE cooldowns (
|
||||||
|
uuid TEXT PRIMARY KEY,
|
||||||
|
last_ticket INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE audit_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ticket_id INTEGER NOT NULL,
|
||||||
|
staff_uuid TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (ticket_id) REFERENCES tickets(id)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
|
||||||
|
setVersion(1);
|
||||||
|
plugin.getLogger().info("applied migration 1: initial schema");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getCurrentVersion() throws SQLException {
|
||||||
|
try (Statement stmt = connection.createStatement();
|
||||||
|
ResultSet rs = stmt.executeQuery("SELECT MAX(version) FROM schema_version")) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVersion(int version) throws SQLException {
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(
|
||||||
|
"INSERT INTO schema_version (version, applied_at) VALUES (?, ?)")) {
|
||||||
|
stmt.setInt(1, version);
|
||||||
|
stmt.setLong(2, System.currentTimeMillis());
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ticket operations
|
||||||
|
public int saveTicket(Ticket ticket) throws SQLException {
|
||||||
|
return createTicket(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int createTicket(Ticket ticket) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO tickets (uuid, player_name, category, short_desc, long_desc,
|
||||||
|
world, x, y, z, yaw, pitch, status, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
stmt.setString(1, ticket.getPlayerUuid().toString());
|
||||||
|
stmt.setString(2, ticket.getPlayerName());
|
||||||
|
stmt.setString(3, ticket.getCategory().name());
|
||||||
|
stmt.setString(4, ticket.getShortDesc());
|
||||||
|
stmt.setString(5, ticket.getLongDesc());
|
||||||
|
stmt.setString(6, ticket.getWorld());
|
||||||
|
stmt.setDouble(7, ticket.getX());
|
||||||
|
stmt.setDouble(8, ticket.getY());
|
||||||
|
stmt.setDouble(9, ticket.getZ());
|
||||||
|
stmt.setFloat(10, ticket.getYaw());
|
||||||
|
stmt.setFloat(11, ticket.getPitch());
|
||||||
|
stmt.setString(12, ticket.getStatus().name());
|
||||||
|
stmt.setLong(13, ticket.getCreatedAt());
|
||||||
|
stmt.setLong(14, ticket.getUpdatedAt());
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SQLException("failed to get generated ticket id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Ticket getTicket(int id) throws SQLException {
|
||||||
|
String sql = "SELECT * FROM tickets WHERE id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return mapResultSetToTicket(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Ticket> getTickets() throws SQLException {
|
||||||
|
List<Ticket> tickets = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM tickets ORDER BY created_at DESC";
|
||||||
|
try (PreparedStatement pstmt = connection.prepareStatement(sql);
|
||||||
|
ResultSet rs = pstmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
tickets.add(mapResultSetToTicket(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tickets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTicketStatus(int ticketId, TicketStatus status) throws SQLException {
|
||||||
|
String sql = "UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, status.name());
|
||||||
|
stmt.setLong(2, System.currentTimeMillis());
|
||||||
|
stmt.setInt(3, ticketId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTicketAssignment(int ticketId, UUID staffUuid) throws SQLException {
|
||||||
|
String sql = "UPDATE tickets SET assigned_to = ?, updated_at = ? WHERE id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, staffUuid != null ? staffUuid.toString() : null);
|
||||||
|
stmt.setLong(2, System.currentTimeMillis());
|
||||||
|
stmt.setInt(3, ticketId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateDiscordThreadId(int ticketId, String threadId) throws SQLException {
|
||||||
|
String sql = "UPDATE tickets SET discord_thread_id = ? WHERE id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, threadId);
|
||||||
|
stmt.setInt(2, ticketId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteTicket(int ticketId) throws SQLException {
|
||||||
|
// delete from audit log first due to foreign keys if they exist (though
|
||||||
|
// audit_log has it)
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM audit_log WHERE ticket_id = ?")) {
|
||||||
|
stmt.setInt(1, ticketId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
String sql = "DELETE FROM tickets WHERE id = ?";
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setInt(1, ticketId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cooldown operations
|
||||||
|
public boolean isOnCooldown(UUID playerUuid, int cooldownSeconds) throws SQLException {
|
||||||
|
String sql = "SELECT last_ticket FROM cooldowns WHERE uuid = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long lastTicket = rs.getLong("last_ticket");
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
return (now - lastTicket) < (cooldownSeconds * 1000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCooldown(UUID playerUuid) throws SQLException {
|
||||||
|
String sql = "INSERT OR REPLACE INTO cooldowns (uuid, last_ticket) VALUES (?, ?)";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
stmt.setLong(2, System.currentTimeMillis());
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// audit log
|
||||||
|
public void logAction(int ticketId, UUID staffUuid, String action) throws SQLException {
|
||||||
|
String sql = "INSERT INTO audit_log (ticket_id, staff_uuid, action, timestamp) VALUES (?, ?, ?, ?)";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setInt(1, ticketId);
|
||||||
|
stmt.setString(2, staffUuid.toString());
|
||||||
|
stmt.setString(3, action);
|
||||||
|
stmt.setLong(4, System.currentTimeMillis());
|
||||||
|
stmt.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Ticket getTicketByThreadId(String threadId) throws SQLException {
|
||||||
|
String sql = "SELECT * FROM tickets WHERE discord_thread_id = ?";
|
||||||
|
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
|
||||||
|
pstmt.setString(1, threadId);
|
||||||
|
try (ResultSet rs = pstmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return mapResultSetToTicket(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ticket mapResultSetToTicket(ResultSet rs) throws SQLException {
|
||||||
|
return new Ticket.Builder()
|
||||||
|
.id(rs.getInt("id"))
|
||||||
|
.playerUuid(UUID.fromString(rs.getString("uuid")))
|
||||||
|
.playerName(rs.getString("player_name"))
|
||||||
|
.category(TicketCategory.valueOf(rs.getString("category")))
|
||||||
|
.shortDesc(rs.getString("short_desc"))
|
||||||
|
.longDesc(rs.getString("long_desc"))
|
||||||
|
.world(rs.getString("world"))
|
||||||
|
.x(rs.getDouble("x"))
|
||||||
|
.y(rs.getDouble("y"))
|
||||||
|
.z(rs.getDouble("z"))
|
||||||
|
.yaw(rs.getFloat("yaw"))
|
||||||
|
.pitch(rs.getFloat("pitch"))
|
||||||
|
.status(TicketStatus.valueOf(rs.getString("status")))
|
||||||
|
.assignedTo(rs.getString("assigned_to") != null ? UUID.fromString(rs.getString("assigned_to")) : null)
|
||||||
|
.discordThreadId(rs.getString("discord_thread_id"))
|
||||||
|
.createdAt(rs.getLong("created_at"))
|
||||||
|
.updatedAt(rs.getLong("updated_at"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (connection != null) {
|
||||||
|
try {
|
||||||
|
connection.close();
|
||||||
|
plugin.getLogger().info("database connection closed");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("error closing database: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/main/java/party/cybsec/oyetickets/discord/DiscordBot.java
Normal file
196
src/main/java/party/cybsec/oyetickets/discord/DiscordBot.java
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package party.cybsec.oyetickets.discord;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.Ticket;
|
||||||
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
|
||||||
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
|
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public class DiscordBot extends ListenerAdapter {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private JDA jda;
|
||||||
|
private boolean ready = false;
|
||||||
|
|
||||||
|
public DiscordBot(OyeTicketsPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean start() {
|
||||||
|
String token = plugin.getPluginConfig().getDiscordBotToken();
|
||||||
|
|
||||||
|
if (token == null || token.isEmpty() || token.equals("your-bot-token-here")) {
|
||||||
|
plugin.getLogger().warning("discord bot token not configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jda = JDABuilder.createDefault(token)
|
||||||
|
.enableIntents(GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT)
|
||||||
|
.addEventListeners(this)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// wait for ready
|
||||||
|
jda.awaitReady();
|
||||||
|
ready = true;
|
||||||
|
|
||||||
|
plugin.getLogger().info("discord bot connected");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("failed to start discord bot: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (jda != null) {
|
||||||
|
jda.shutdown();
|
||||||
|
plugin.getLogger().info("discord bot disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JDA getJda() {
|
||||||
|
return jda;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady() {
|
||||||
|
return ready && jda != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createTicketThread(Ticket ticket) {
|
||||||
|
if (!isReady())
|
||||||
|
return;
|
||||||
|
|
||||||
|
String channelId = plugin.getPluginConfig().getDiscordChannelId();
|
||||||
|
if (channelId == null || channelId.isEmpty()) {
|
||||||
|
plugin.getLogger().warning("discord channel id not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
net.dv8tion.jda.api.entities.channel.middleman.GuildChannel guildChannel = jda
|
||||||
|
.getGuildChannelById(channelId);
|
||||||
|
|
||||||
|
if (guildChannel == null) {
|
||||||
|
plugin.getLogger().warning("discord channel not found: " + channelId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create thread
|
||||||
|
String threadTitle = ticket.getStatus().getEmoji() + " " + ticket.getTitle();
|
||||||
|
|
||||||
|
if (guildChannel instanceof net.dv8tion.jda.api.entities.channel.concrete.ForumChannel forumChannel) {
|
||||||
|
EmbedBuilder embed = buildTicketEmbed(ticket);
|
||||||
|
MessageCreateBuilder message = new MessageCreateBuilder()
|
||||||
|
.setEmbeds(embed.build())
|
||||||
|
.setContent("**new support ticket**\n\n" +
|
||||||
|
plugin.getPluginConfig().getGuidelines() +
|
||||||
|
"\n\n**reminder:** upload screenshots in this thread if applicable");
|
||||||
|
|
||||||
|
forumChannel.createForumPost(threadTitle, message.build())
|
||||||
|
.queue(post -> {
|
||||||
|
long threadId = post.getThreadChannel().getIdLong();
|
||||||
|
String threadIdStr = String.valueOf(threadId);
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().updateDiscordThreadId(ticket.getId(), threadIdStr);
|
||||||
|
ticket.setDiscordThreadId(threadIdStr);
|
||||||
|
plugin.getLogger().info("created discord forum post for ticket #" + ticket.getId()
|
||||||
|
+ " (Thread ID: " + threadIdStr + ")");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("failed to save discord thread id: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (guildChannel instanceof net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer threadContainer) {
|
||||||
|
threadContainer.createThreadChannel(threadTitle).queue(thread -> {
|
||||||
|
EmbedBuilder embed = buildTicketEmbed(ticket);
|
||||||
|
MessageCreateBuilder message = new MessageCreateBuilder()
|
||||||
|
.setEmbeds(embed.build())
|
||||||
|
.setContent("**new support ticket**\n\n" +
|
||||||
|
plugin.getPluginConfig().getGuidelines() +
|
||||||
|
"\n\n**reminder:** upload screenshots in this thread if applicable");
|
||||||
|
|
||||||
|
thread.sendMessage(message.build()).queue(msg -> {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().updateDiscordThreadId(ticket.getId(), thread.getId());
|
||||||
|
ticket.setDiscordThreadId(thread.getId());
|
||||||
|
plugin.getLogger().info("created discord thread for ticket #" + ticket.getId() + " (ID: "
|
||||||
|
+ thread.getId() + ")");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("failed to save discord thread id: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("error creating discord thread: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTicketStatus(Ticket ticket) {
|
||||||
|
if (!isReady())
|
||||||
|
return;
|
||||||
|
|
||||||
|
String threadId = ticket.getDiscordThreadId();
|
||||||
|
if (threadId == null || threadId.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ThreadChannel thread = jda.getThreadChannelById(threadId);
|
||||||
|
if (thread == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
String newTitle = ticket.getStatus().getEmoji() + " " + ticket.getTitle();
|
||||||
|
thread.getManager().setName(newTitle).queue();
|
||||||
|
|
||||||
|
EmbedBuilder embed = buildTicketEmbed(ticket);
|
||||||
|
thread.sendMessageEmbeds(embed.build()).queue();
|
||||||
|
|
||||||
|
if (ticket.getStatus().isClosed()) {
|
||||||
|
thread.getManager().setArchived(true).setLocked(true).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("error updating discord thread: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EmbedBuilder buildTicketEmbed(Ticket ticket) {
|
||||||
|
EmbedBuilder embed = new EmbedBuilder();
|
||||||
|
embed.setTitle(ticket.getTitle());
|
||||||
|
embed.setColor(getColorForStatus(ticket.getStatus()));
|
||||||
|
embed.addField("player", ticket.getPlayerName(), true);
|
||||||
|
embed.addField("category", ticket.getCategory().getDisplayName(), true);
|
||||||
|
embed.addField("status", ticket.getStatus().getEmoji() + " " + ticket.getStatus().getDisplayName(), true);
|
||||||
|
embed.addField("location", "```\n" + ticket.getCoordinates() + "\n```", false);
|
||||||
|
embed.addField("description", ticket.getFullDescription(), false);
|
||||||
|
if (ticket.getAssignedTo() != null) {
|
||||||
|
String assignedName = plugin.getServer().getOfflinePlayer(ticket.getAssignedTo()).getName();
|
||||||
|
embed.addField("assigned to", assignedName, true);
|
||||||
|
}
|
||||||
|
embed.setFooter("ticket #" + ticket.getId());
|
||||||
|
embed.setTimestamp(java.time.Instant.ofEpochMilli(ticket.getUpdatedAt()));
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color getColorForStatus(party.cybsec.oyetickets.model.TicketStatus status) {
|
||||||
|
return switch (status) {
|
||||||
|
case NONE -> Color.GRAY;
|
||||||
|
case ACTIVE -> Color.YELLOW;
|
||||||
|
case WAITING -> Color.ORANGE;
|
||||||
|
case SOLVED -> Color.GREEN;
|
||||||
|
case CLOSED -> Color.RED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
276
src/main/java/party/cybsec/oyetickets/model/Ticket.java
Normal file
276
src/main/java/party/cybsec/oyetickets/model/Ticket.java
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
package party.cybsec.oyetickets.model;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Ticket {
|
||||||
|
|
||||||
|
private final int id;
|
||||||
|
private final UUID playerUuid;
|
||||||
|
private final String playerName;
|
||||||
|
private final TicketCategory category;
|
||||||
|
private final String shortDesc;
|
||||||
|
private final String longDesc;
|
||||||
|
private final String world;
|
||||||
|
private final double x;
|
||||||
|
private final double y;
|
||||||
|
private final double z;
|
||||||
|
private final float yaw;
|
||||||
|
private final float pitch;
|
||||||
|
private TicketStatus status;
|
||||||
|
private UUID assignedTo;
|
||||||
|
private String discordThreadId;
|
||||||
|
private final long createdAt;
|
||||||
|
private long updatedAt;
|
||||||
|
|
||||||
|
private Ticket(Builder builder) {
|
||||||
|
this.id = builder.id;
|
||||||
|
this.playerUuid = builder.playerUuid;
|
||||||
|
this.playerName = builder.playerName;
|
||||||
|
this.category = builder.category;
|
||||||
|
this.shortDesc = builder.shortDesc;
|
||||||
|
this.longDesc = builder.longDesc;
|
||||||
|
this.world = builder.world;
|
||||||
|
this.x = builder.x;
|
||||||
|
this.y = builder.y;
|
||||||
|
this.z = builder.z;
|
||||||
|
this.yaw = builder.yaw;
|
||||||
|
this.pitch = builder.pitch;
|
||||||
|
this.status = builder.status;
|
||||||
|
this.assignedTo = builder.assignedTo;
|
||||||
|
this.discordThreadId = builder.discordThreadId;
|
||||||
|
this.createdAt = builder.createdAt;
|
||||||
|
this.updatedAt = builder.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return category.getTag() + " - " + shortDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCoordinates() {
|
||||||
|
return String.format("%s %.0f %.0f %.0f", world, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullDescription() {
|
||||||
|
if (longDesc == null || longDesc.isEmpty()) {
|
||||||
|
return shortDesc;
|
||||||
|
}
|
||||||
|
return shortDesc + "\n\n" + longDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlayerName() {
|
||||||
|
return playerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TicketCategory getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getShortDesc() {
|
||||||
|
return shortDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLongDesc() {
|
||||||
|
return longDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorld() {
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getZ() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getYaw() {
|
||||||
|
return yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPitch() {
|
||||||
|
return pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TicketStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getAssignedTo() {
|
||||||
|
return assignedTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDiscordThreadId() {
|
||||||
|
return discordThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setters for mutable fields
|
||||||
|
public void setStatus(TicketStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
this.updatedAt = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssignedTo(UUID assignedTo) {
|
||||||
|
this.assignedTo = assignedTo;
|
||||||
|
this.updatedAt = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDiscordThreadId(String discordThreadId) {
|
||||||
|
this.discordThreadId = discordThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private int id;
|
||||||
|
private UUID playerUuid;
|
||||||
|
private String playerName;
|
||||||
|
private TicketCategory category;
|
||||||
|
private String shortDesc;
|
||||||
|
private String longDesc;
|
||||||
|
private String world;
|
||||||
|
private double x;
|
||||||
|
private double y;
|
||||||
|
private double z;
|
||||||
|
private float yaw;
|
||||||
|
private float pitch;
|
||||||
|
private TicketStatus status = TicketStatus.NONE;
|
||||||
|
private UUID assignedTo;
|
||||||
|
private String discordThreadId;
|
||||||
|
private long createdAt = System.currentTimeMillis();
|
||||||
|
private long updatedAt = System.currentTimeMillis();
|
||||||
|
|
||||||
|
public Builder id(int id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder playerUuid(UUID uuid) {
|
||||||
|
this.playerUuid = uuid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder playerName(String name) {
|
||||||
|
this.playerName = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder category(TicketCategory category) {
|
||||||
|
this.category = category;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder shortDesc(String desc) {
|
||||||
|
this.shortDesc = desc;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder longDesc(String desc) {
|
||||||
|
this.longDesc = desc;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder location(Location loc) {
|
||||||
|
this.world = loc.getWorld().getName();
|
||||||
|
this.x = loc.getX();
|
||||||
|
this.y = loc.getY();
|
||||||
|
this.z = loc.getZ();
|
||||||
|
this.yaw = loc.getYaw();
|
||||||
|
this.pitch = loc.getPitch();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder world(String world) {
|
||||||
|
this.world = world;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder x(double x) {
|
||||||
|
this.x = x;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder y(double y) {
|
||||||
|
this.y = y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder z(double z) {
|
||||||
|
this.z = z;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder yaw(float yaw) {
|
||||||
|
this.yaw = yaw;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder pitch(float pitch) {
|
||||||
|
this.pitch = pitch;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder status(TicketStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder assignedTo(UUID uuid) {
|
||||||
|
this.assignedTo = uuid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder discordThreadId(String id) {
|
||||||
|
this.discordThreadId = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder createdAt(long timestamp) {
|
||||||
|
this.createdAt = timestamp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder updatedAt(long timestamp) {
|
||||||
|
this.updatedAt = timestamp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder player(UUID uuid, String name) {
|
||||||
|
this.playerUuid = uuid;
|
||||||
|
this.playerName = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder description(String shortDesc, String longDesc) {
|
||||||
|
this.shortDesc = shortDesc;
|
||||||
|
this.longDesc = longDesc;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Ticket build() {
|
||||||
|
return new Ticket(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package party.cybsec.oyetickets.model;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
public enum TicketCategory {
|
||||||
|
CRIME("crime", Material.IRON_SWORD),
|
||||||
|
LOST_ITEM("lost item", Material.CHEST),
|
||||||
|
SOCIAL_CASE("social case", Material.PLAYER_HEAD),
|
||||||
|
BUG("bug", Material.SPIDER_EYE);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final Material icon;
|
||||||
|
|
||||||
|
TicketCategory(String displayName, Material icon) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Material getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return name();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package party.cybsec.oyetickets.model;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
public enum TicketStatus {
|
||||||
|
NONE("none", Material.GRAY_WOOL, "⚪", NamedTextColor.GRAY),
|
||||||
|
ACTIVE("active", Material.YELLOW_WOOL, "🟡", NamedTextColor.YELLOW),
|
||||||
|
WAITING("waiting", Material.ORANGE_WOOL, "🟠", NamedTextColor.GOLD),
|
||||||
|
SOLVED("solved", Material.GREEN_WOOL, "🟢", NamedTextColor.GREEN),
|
||||||
|
CLOSED("closed", Material.RED_WOOL, "🔴", NamedTextColor.RED);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final Material icon;
|
||||||
|
private final String emoji;
|
||||||
|
private final NamedTextColor color;
|
||||||
|
|
||||||
|
TicketStatus(String displayName, Material icon, String emoji, NamedTextColor color) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.icon = icon;
|
||||||
|
this.emoji = emoji;
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Material getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmoji() {
|
||||||
|
return emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NamedTextColor getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClosed() {
|
||||||
|
return this == SOLVED || this == CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package party.cybsec.oyetickets.session;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SessionManager {
|
||||||
|
|
||||||
|
private final Map<UUID, TicketSession> sessions;
|
||||||
|
private static final long SESSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
public SessionManager() {
|
||||||
|
this.sessions = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TicketSession getOrCreateSession(Player player) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
sessions.computeIfAbsent(uuid, k -> new TicketSession());
|
||||||
|
return sessions.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TicketSession getSession(Player player) {
|
||||||
|
return sessions.get(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSession(Player player) {
|
||||||
|
sessions.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endSession(UUID uuid) {
|
||||||
|
sessions.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanup() {
|
||||||
|
sessions.entrySet().removeIf(entry -> entry.getValue().isExpired(SESSION_TIMEOUT_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package party.cybsec.oyetickets.session;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.model.TicketCategory;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
public class TicketSession {
|
||||||
|
|
||||||
|
private TicketCategory category;
|
||||||
|
private String shortDesc;
|
||||||
|
private String longDesc;
|
||||||
|
private Location location;
|
||||||
|
private boolean bookOpened;
|
||||||
|
private final long createdAt;
|
||||||
|
|
||||||
|
public TicketSession() {
|
||||||
|
this.createdAt = System.currentTimeMillis();
|
||||||
|
this.bookOpened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired(long timeoutMs) {
|
||||||
|
return (System.currentTimeMillis() - createdAt) > timeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isComplete() {
|
||||||
|
return category != null && shortDesc != null && location != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters and setters
|
||||||
|
public TicketCategory getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCategory(TicketCategory category) {
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getShortDesc() {
|
||||||
|
return shortDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShortDesc(String shortDesc) {
|
||||||
|
this.shortDesc = shortDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLongDesc() {
|
||||||
|
return longDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLongDesc(String longDesc) {
|
||||||
|
this.longDesc = longDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocation(Location location) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBookOpened() {
|
||||||
|
return bookOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBookOpened(boolean bookOpened) {
|
||||||
|
this.bookOpened = bookOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorldName() {
|
||||||
|
return location != null ? location.getWorld().getName() : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getX() {
|
||||||
|
return location != null ? location.getX() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getY() {
|
||||||
|
return location != null ? location.getY() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getZ() {
|
||||||
|
return location != null ? location.getZ() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.player;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.TicketCategory;
|
||||||
|
import party.cybsec.oyetickets.session.TicketSession;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CategoryDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player player;
|
||||||
|
private final TicketSession session;
|
||||||
|
|
||||||
|
public CategoryDialog(OyeTicketsPlugin plugin, Player player, TicketSession session) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.player = player;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
|
if (!player.isOnline())
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<ActionButton> buttons = new ArrayList<>();
|
||||||
|
for (TicketCategory category : TicketCategory.values()) {
|
||||||
|
buttons.add(ActionButton.builder(Component.text(category.getDisplayName(), NamedTextColor.YELLOW))
|
||||||
|
.tooltip(Component.text("Select this category for your ticket."))
|
||||||
|
.width(150)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
session.setCategory(category);
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
new TicketDetailsDialog(plugin, p, session).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add back button
|
||||||
|
buttons.add(ActionButton.builder(Component.text("Back", NamedTextColor.RED))
|
||||||
|
.width(100)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
new NavigationDialog(plugin, p, session).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Select Category", NamedTextColor.GOLD))
|
||||||
|
.body(List.of(
|
||||||
|
DialogBody.plainMessage(Component.text("What is this ticket regarding?"))))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.multiAction(buttons).build()));
|
||||||
|
|
||||||
|
player.showDialog(dialog);
|
||||||
|
}, 1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.player;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.session.TicketSession;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.BookMeta;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class NavigationDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player player;
|
||||||
|
private final TicketSession session;
|
||||||
|
|
||||||
|
public NavigationDialog(OyeTicketsPlugin plugin, Player player, TicketSession session) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.player = player;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
|
if (!player.isOnline())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Ticket Guidelines", NamedTextColor.GOLD))
|
||||||
|
.body(List.of(
|
||||||
|
DialogBody.plainMessage(Component.text("rules checklist:", NamedTextColor.YELLOW)),
|
||||||
|
DialogBody.plainMessage(Component.text("- location captured automatically")),
|
||||||
|
DialogBody.plainMessage(Component.text("- screenshots must go to discord")),
|
||||||
|
DialogBody.plainMessage(Component.text("- no spam / respectful behavior"))))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.multiAction(List.of(
|
||||||
|
ActionButton.builder(Component.text("Accept & Continue", TextColor.color(0xAEFFC1)))
|
||||||
|
.tooltip(Component.text("I agree to these guidelines."))
|
||||||
|
.width(150)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
new CategoryDialog(plugin, p, session).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Read Rules", NamedTextColor.GOLD))
|
||||||
|
.tooltip(Component.text("Open the full server rulebook."))
|
||||||
|
.width(100)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
openGuidelinesBook(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Exit", NamedTextColor.RED))
|
||||||
|
.width(80)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
p.sendMessage(Component.text("Ticket creation abandoned.")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
plugin.getSessionManager().endSession(p.getUniqueId());
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build()))
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
player.showDialog(dialog);
|
||||||
|
}, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openGuidelinesBook(Player p) {
|
||||||
|
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
|
||||||
|
BookMeta meta = (BookMeta) book.getItemMeta();
|
||||||
|
meta.setTitle("server guidelines");
|
||||||
|
meta.setAuthor("server staff");
|
||||||
|
meta.addPages(Component.text(plugin.getPluginConfig().getGuidelines()));
|
||||||
|
book.setItemMeta(meta);
|
||||||
|
p.openBook(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.player;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.Ticket;
|
||||||
|
import party.cybsec.oyetickets.session.TicketSession;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ReviewDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player player;
|
||||||
|
private final TicketSession session;
|
||||||
|
|
||||||
|
public ReviewDialog(OyeTicketsPlugin plugin, Player player, TicketSession session) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.player = player;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
|
if (!player.isOnline())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Final Review", NamedTextColor.GOLD))
|
||||||
|
.body(List.of(
|
||||||
|
DialogBody.plainMessage(Component.text("Summary:", NamedTextColor.YELLOW)),
|
||||||
|
DialogBody.plainMessage(
|
||||||
|
Component.text("Category: " + session.getCategory().getDisplayName())),
|
||||||
|
DialogBody.plainMessage(Component.text("Title: " + session.getShortDesc())),
|
||||||
|
DialogBody.plainMessage(Component
|
||||||
|
.text("Location: " + session.getWorldName() + " (" + (int) session.getX()
|
||||||
|
+ ", " + (int) session.getY() + ", " + (int) session.getZ() + ")")),
|
||||||
|
DialogBody.plainMessage(Component.text("Description: "
|
||||||
|
+ (session.getLongDesc() != null && !session.getLongDesc().isEmpty()
|
||||||
|
? "Provided"
|
||||||
|
: "None"))),
|
||||||
|
DialogBody.plainMessage(Component.empty()),
|
||||||
|
DialogBody.plainMessage(Component.text(
|
||||||
|
"Click submit to create your ticket. A Discord thread will be created for screenshots.",
|
||||||
|
NamedTextColor.GRAY))))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.confirmation(
|
||||||
|
ActionButton.builder(Component.text("Submit", TextColor.color(0xAEFFC1)))
|
||||||
|
.tooltip(Component.text("Create your ticket now."))
|
||||||
|
.width(100)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
submitAndFinish(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Back", NamedTextColor.RED))
|
||||||
|
.tooltip(Component.text("Go back to edit details."))
|
||||||
|
.width(100)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
new TicketDetailsDialog(plugin, p, session).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build())));
|
||||||
|
|
||||||
|
player.showDialog(dialog);
|
||||||
|
}, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitAndFinish(Player p) {
|
||||||
|
Ticket ticket = new Ticket.Builder()
|
||||||
|
.player(p.getUniqueId(), p.getName())
|
||||||
|
.category(session.getCategory())
|
||||||
|
.description(session.getShortDesc(), session.getLongDesc())
|
||||||
|
.location(p.getLocation())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().saveTicket(ticket);
|
||||||
|
p.sendMessage(Component.text("Ticket #" + ticket.getId() + " submitted successfully!")
|
||||||
|
.color(NamedTextColor.GREEN));
|
||||||
|
|
||||||
|
// trigger discord integration
|
||||||
|
plugin.getDiscordBot().createTicketThread(ticket);
|
||||||
|
|
||||||
|
// cleanup session
|
||||||
|
plugin.getSessionManager().endSession(p.getUniqueId());
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("failed to save ticket: " + e.getMessage());
|
||||||
|
p.sendMessage(Component.text("failed to submit ticket. please contact staff.")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.player;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.session.TicketSession;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.input.DialogInput;
|
||||||
|
import io.papermc.paper.registry.data.dialog.input.TextDialogInput;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TicketDetailsDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player player;
|
||||||
|
private final TicketSession session;
|
||||||
|
|
||||||
|
public TicketDetailsDialog(OyeTicketsPlugin plugin, Player player, TicketSession session) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.player = player;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
|
if (!player.isOnline())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Ticket Details", NamedTextColor.GOLD))
|
||||||
|
.body(List.of(
|
||||||
|
DialogBody.plainMessage(
|
||||||
|
Component.text("Please provide a title and detailed description."))))
|
||||||
|
.inputs(List.of(
|
||||||
|
DialogInput.text("title", Component.text("Brief Title", NamedTextColor.YELLOW))
|
||||||
|
.initial(session.getShortDesc() != null ? session.getShortDesc() : "")
|
||||||
|
.width(300)
|
||||||
|
.maxLength(100)
|
||||||
|
.build(),
|
||||||
|
DialogInput
|
||||||
|
.text("description",
|
||||||
|
Component.text("Full Description", NamedTextColor.YELLOW))
|
||||||
|
.initial(session.getLongDesc() != null ? session.getLongDesc() : "")
|
||||||
|
.width(300)
|
||||||
|
.maxLength(4000)
|
||||||
|
.multiline(TextDialogInput.MultilineOptions.create(10, 200))
|
||||||
|
.build()))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.confirmation(
|
||||||
|
ActionButton.builder(Component.text("Confirm", TextColor.color(0xAEFFC1)))
|
||||||
|
.tooltip(Component.text("Click to save and proceed to review."))
|
||||||
|
.width(100)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
String title = view.getText("title");
|
||||||
|
String description = view.getText("description");
|
||||||
|
|
||||||
|
if (title == null || title.trim().length() < 5) {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
p.sendMessage(Component.text("Title is too short! (min 5 characters)")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setShortDesc(title.trim());
|
||||||
|
session.setLongDesc(description != null ? description.trim() : "");
|
||||||
|
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
new ReviewDialog(plugin, p, session).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Cancel", TextColor.color(0xFFA0B1)))
|
||||||
|
.tooltip(Component.text("Discard changes and exit."))
|
||||||
|
.width(100)
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
p.sendMessage(Component.text("Ticket creation abandoned.")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build())));
|
||||||
|
|
||||||
|
player.showDialog(dialog);
|
||||||
|
}, 1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.staff;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.Ticket;
|
||||||
|
import party.cybsec.oyetickets.model.TicketStatus;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StatusDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player staff;
|
||||||
|
private final Ticket ticket;
|
||||||
|
|
||||||
|
public StatusDialog(OyeTicketsPlugin plugin, Player staff, Ticket ticket) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.staff = staff;
|
||||||
|
this.ticket = ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
List<ActionButton> buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
for (TicketStatus status : TicketStatus.values()) {
|
||||||
|
buttons.add(ActionButton.builder(Component.text(status.getDisplayName()))
|
||||||
|
.tooltip(Component.text("Set status to " + status.name().toLowerCase()))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
updateStatus(p, status);
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.add(ActionButton.builder(Component.text("Back", NamedTextColor.GRAY))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
new TicketActionDialog(plugin, p, ticket).open();
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Set Ticket Status", NamedTextColor.GOLD))
|
||||||
|
.body(List.of(DialogBody
|
||||||
|
.plainMessage(Component.text("Select a new status for ticket #" + ticket.getId()))))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.multiAction(buttons).build()));
|
||||||
|
|
||||||
|
staff.showDialog(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus(Player p, TicketStatus status) {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().updateTicketStatus(ticket.getId(), status);
|
||||||
|
ticket.setStatus(status);
|
||||||
|
p.sendMessage(Component.text("Status updated to " + status.getDisplayName(), NamedTextColor.GREEN));
|
||||||
|
if (plugin.getDiscordBot() != null) {
|
||||||
|
plugin.getDiscordBot().updateTicketStatus(ticket);
|
||||||
|
}
|
||||||
|
new TicketActionDialog(plugin, p, ticket).open();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
p.sendMessage(Component.text("Failed to update status.", NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.staff;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.Ticket;
|
||||||
|
import party.cybsec.oyetickets.model.TicketStatus;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TicketActionDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player staff;
|
||||||
|
private final Ticket ticket;
|
||||||
|
|
||||||
|
public TicketActionDialog(OyeTicketsPlugin plugin, Player staff, Ticket ticket) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.staff = staff;
|
||||||
|
this.ticket = ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Ticket #" + ticket.getId(), NamedTextColor.GOLD))
|
||||||
|
.body(List.of(
|
||||||
|
DialogBody.plainMessage(Component.text("Creator: ", NamedTextColor.GRAY)
|
||||||
|
.append(Component.text(ticket.getPlayerName(), NamedTextColor.WHITE))),
|
||||||
|
DialogBody.plainMessage(Component.text("Category: ", NamedTextColor.GRAY)
|
||||||
|
.append(Component.text(ticket.getCategory().getDisplayName(),
|
||||||
|
NamedTextColor.YELLOW))),
|
||||||
|
DialogBody.plainMessage(Component.text("Description: ", NamedTextColor.GRAY)
|
||||||
|
.append(Component.text(ticket.getShortDesc(), NamedTextColor.WHITE))),
|
||||||
|
DialogBody.plainMessage(Component.text("Status: ", NamedTextColor.GRAY)
|
||||||
|
.append(Component.text(ticket.getStatus().getDisplayName(),
|
||||||
|
ticket.getStatus().getColor())))))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.multiAction(List.of(
|
||||||
|
ActionButton.builder(Component.text("Read Full Info"))
|
||||||
|
.tooltip(Component.text("Open the ticket as a book."))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
openDescriptionBook(p);
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Teleport"))
|
||||||
|
.tooltip(Component.text("Teleport to the ticket location."))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
Location loc = new Location(Bukkit.getWorld(ticket.getWorld()), ticket.getX(),
|
||||||
|
ticket.getY(), ticket.getZ(), ticket.getYaw(), ticket.getPitch());
|
||||||
|
p.teleport(loc);
|
||||||
|
p.sendMessage(Component.text("Teleported!", NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Update Status"))
|
||||||
|
.tooltip(Component.text("Change the ticket status."))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
new StatusDialog(plugin, p, ticket).open();
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Claim Ticket"))
|
||||||
|
.tooltip(Component.text("Assign this ticket to yourself."))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().updateTicketAssignment(ticket.getId(),
|
||||||
|
p.getUniqueId());
|
||||||
|
ticket.setAssignedTo(p.getUniqueId());
|
||||||
|
p.sendMessage(
|
||||||
|
Component.text("Ticket assigned to you.", NamedTextColor.GREEN));
|
||||||
|
open(); // Refresh
|
||||||
|
} catch (SQLException e) {
|
||||||
|
p.sendMessage(
|
||||||
|
Component.text("Failed to assign ticket.", NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Delete Ticket", NamedTextColor.RED))
|
||||||
|
.tooltip(Component.text("Permanently delete this ticket."))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
showDeleteConfirmation(p);
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Back", NamedTextColor.GRAY))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
new TicketBrowserDialog(plugin, p).open();
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build()))
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
staff.showDialog(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDeleteConfirmation(Player p) {
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Confirm Deletion", NamedTextColor.RED))
|
||||||
|
.body(List.of(DialogBody.plainMessage(
|
||||||
|
Component.text("Are you sure you want to delete ticket #" + ticket.getId() + "?"))))
|
||||||
|
.build())
|
||||||
|
.type(DialogType.confirmation(
|
||||||
|
ActionButton.builder(Component.text("Delete", NamedTextColor.RED))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().deleteTicket(ticket.getId());
|
||||||
|
p.sendMessage(Component.text("Ticket deleted.", NamedTextColor.GREEN));
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
new TicketBrowserDialog(plugin, p).open();
|
||||||
|
});
|
||||||
|
} catch (SQLException e) {
|
||||||
|
p.sendMessage(
|
||||||
|
Component.text("Failed to delete ticket.", NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Cancel", NamedTextColor.GRAY))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
open();
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build())));
|
||||||
|
p.showDialog(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openDescriptionBook(Player p) {
|
||||||
|
org.bukkit.inventory.ItemStack book = new org.bukkit.inventory.ItemStack(org.bukkit.Material.WRITTEN_BOOK);
|
||||||
|
org.bukkit.inventory.meta.BookMeta meta = (org.bukkit.inventory.meta.BookMeta) book.getItemMeta();
|
||||||
|
meta.setTitle(ticket.getTitle());
|
||||||
|
meta.setAuthor(ticket.getPlayerName());
|
||||||
|
meta.addPages(Component.text(ticket.getFullDescription()));
|
||||||
|
book.setItemMeta(meta);
|
||||||
|
p.openBook(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package party.cybsec.oyetickets.ui.staff;
|
||||||
|
|
||||||
|
import party.cybsec.oyetickets.OyeTicketsPlugin;
|
||||||
|
import party.cybsec.oyetickets.model.Ticket;
|
||||||
|
import party.cybsec.oyetickets.model.TicketStatus;
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickCallback;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.SkullMeta;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TicketBrowserDialog {
|
||||||
|
|
||||||
|
private final OyeTicketsPlugin plugin;
|
||||||
|
private final Player staff;
|
||||||
|
|
||||||
|
public TicketBrowserDialog(OyeTicketsPlugin plugin, Player staff) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.staff = staff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
List<Ticket> tickets = plugin.getDatabase().getTickets();
|
||||||
|
// Filter for open tickets
|
||||||
|
List<Ticket> openTickets = tickets.stream()
|
||||||
|
.filter(t -> t.getStatus() != TicketStatus.CLOSED)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
if (!staff.isOnline())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (openTickets.isEmpty()) {
|
||||||
|
staff.sendMessage(Component.text("No open tickets found.", NamedTextColor.YELLOW));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showListDialog(openTickets);
|
||||||
|
});
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().severe("Failed to fetch tickets: " + e.getMessage());
|
||||||
|
staff.sendMessage(Component.text("Error accessing database.", NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showListDialog(List<Ticket> tickets) {
|
||||||
|
List<ActionButton> buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Ticket ticket : tickets) {
|
||||||
|
buttons.add(ActionButton.builder(Component.text(ticket.getTitle()))
|
||||||
|
.tooltip(Component
|
||||||
|
.text("Creator: " + ticket.getPlayerName() + "\nDescription: " + ticket.getShortDesc()))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
if (audience instanceof Player p) {
|
||||||
|
new TicketActionDialog(plugin, p, ticket).open();
|
||||||
|
}
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use MultiActionType with 1 column for a vertical list
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Staff Ticket Browser", NamedTextColor.GOLD))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.build())
|
||||||
|
.type(DialogType.multiAction(buttons)
|
||||||
|
.columns(1)
|
||||||
|
.exitAction(ActionButton.builder(Component.text("Exit Browser", NamedTextColor.GRAY))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build())
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
staff.showDialog(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String text, int maxLength) {
|
||||||
|
if (text == null)
|
||||||
|
return "";
|
||||||
|
if (text.length() <= maxLength)
|
||||||
|
return text;
|
||||||
|
return text.substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack getPlayerHead(Ticket ticket) {
|
||||||
|
ItemStack head = new ItemStack(Material.PLAYER_HEAD);
|
||||||
|
SkullMeta meta = (SkullMeta) head.getItemMeta();
|
||||||
|
meta.setOwningPlayer(plugin.getServer().getOfflinePlayer(ticket.getPlayerUuid()));
|
||||||
|
head.setItemMeta(meta);
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDeleteConfirmation(Player p, Ticket ticket) {
|
||||||
|
Dialog dialog = Dialog.create(builder -> builder.empty()
|
||||||
|
.base(DialogBase.builder(Component.text("Confirm Deletion", NamedTextColor.RED))
|
||||||
|
.body(List.of(DialogBody.plainMessage(
|
||||||
|
Component.text("Are you sure you want to delete ticket #" + ticket.getId() + "?"))))
|
||||||
|
.build())
|
||||||
|
.type(DialogType.confirmation(
|
||||||
|
ActionButton.builder(Component.text("Delete", NamedTextColor.RED))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().deleteTicket(ticket.getId());
|
||||||
|
p.sendMessage(Component.text("Ticket deleted.", NamedTextColor.GREEN));
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, this::open);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
p.sendMessage(
|
||||||
|
Component.text("Failed to delete ticket.", NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build(),
|
||||||
|
ActionButton.builder(Component.text("Cancel", NamedTextColor.GRAY))
|
||||||
|
.action(DialogAction.customClick((view, audience) -> {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, this::open);
|
||||||
|
}, ClickCallback.Options.builder().build()))
|
||||||
|
.build())));
|
||||||
|
p.showDialog(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/main/java/party/cybsec/oyetickets/util/ItemBuilder.java
Normal file
79
src/main/java/party/cybsec/oyetickets/util/ItemBuilder.java
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package party.cybsec.oyetickets.util;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.inventory.ItemFlag;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ItemBuilder {
|
||||||
|
|
||||||
|
private final ItemStack item;
|
||||||
|
private final ItemMeta meta;
|
||||||
|
|
||||||
|
public ItemBuilder(Material material) {
|
||||||
|
this.item = new ItemStack(material);
|
||||||
|
this.meta = item.getItemMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder(Material material, int amount) {
|
||||||
|
this.item = new ItemStack(material, amount);
|
||||||
|
this.meta = item.getItemMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder name(String name) {
|
||||||
|
meta.displayName(Component.text(name).decoration(TextDecoration.ITALIC, false));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder name(Component component) {
|
||||||
|
meta.displayName(component.decoration(TextDecoration.ITALIC, false));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder lore(String... lines) {
|
||||||
|
List<Component> lore = new ArrayList<>();
|
||||||
|
for (String line : lines) {
|
||||||
|
lore.add(Component.text(line).decoration(TextDecoration.ITALIC, false));
|
||||||
|
}
|
||||||
|
meta.lore(lore);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder lore(Component... lines) {
|
||||||
|
List<Component> lore = new ArrayList<>();
|
||||||
|
for (Component line : lines) {
|
||||||
|
lore.add(line.decoration(TextDecoration.ITALIC, false));
|
||||||
|
}
|
||||||
|
meta.lore(lore);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder lore(List<Component> lore) {
|
||||||
|
List<Component> noItalic = new ArrayList<>();
|
||||||
|
for (Component line : lore) {
|
||||||
|
noItalic.add(line.decoration(TextDecoration.ITALIC, false));
|
||||||
|
}
|
||||||
|
meta.lore(noItalic);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder flags(ItemFlag... flags) {
|
||||||
|
meta.addItemFlags(flags);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemBuilder hideAll() {
|
||||||
|
meta.addItemFlags(ItemFlag.values());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack build() {
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/party/cybsec/oyetickets/util/LocationUtil.java
Normal file
24
src/main/java/party/cybsec/oyetickets/util/LocationUtil.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package party.cybsec.oyetickets.util;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
public class LocationUtil {
|
||||||
|
|
||||||
|
public static String format(Location loc) {
|
||||||
|
return String.format("%s %.0f %.0f %.0f",
|
||||||
|
loc.getWorld().getName(),
|
||||||
|
loc.getX(),
|
||||||
|
loc.getY(),
|
||||||
|
loc.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatDetailed(Location loc) {
|
||||||
|
return String.format("%s x:%.2f y:%.2f z:%.2f yaw:%.1f pitch:%.1f",
|
||||||
|
loc.getWorld().getName(),
|
||||||
|
loc.getX(),
|
||||||
|
loc.getY(),
|
||||||
|
loc.getZ(),
|
||||||
|
loc.getYaw(),
|
||||||
|
loc.getPitch());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package party.cybsec.oyetickets.util;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
public class PermissionUtil {
|
||||||
|
|
||||||
|
public static boolean has(Player player, String permission) {
|
||||||
|
return player.hasPermission(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean checkAndNotify(Player player, String permission) {
|
||||||
|
if (player.hasPermission(permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("you don't have permission to do that")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean checkAndNotify(Player player, String permission, String message) {
|
||||||
|
if (player.hasPermission(permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage(Component.text(message).color(NamedTextColor.RED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/resources/config.yml
Normal file
27
src/main/resources/config.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# discord integration
|
||||||
|
discord:
|
||||||
|
enabled: true
|
||||||
|
bot-token: "your-bot-token-here"
|
||||||
|
channel-id: "1234567890"
|
||||||
|
|
||||||
|
# abuse controls
|
||||||
|
cooldown:
|
||||||
|
enabled: true
|
||||||
|
duration-seconds: 300
|
||||||
|
|
||||||
|
duplicate-detection:
|
||||||
|
enabled: true
|
||||||
|
similarity-threshold: 0.8
|
||||||
|
|
||||||
|
# guidelines (shown in orientation book)
|
||||||
|
guidelines: |
|
||||||
|
welcome to the staff support ticket system.
|
||||||
|
|
||||||
|
this system helps staff track and resolve player issues.
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- be specific in your description
|
||||||
|
- upload screenshots to discord
|
||||||
|
- do not spam tickets
|
||||||
|
|
||||||
|
your location will be automatically captured.
|
||||||
38
src/main/resources/plugin.yml
Normal file
38
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: OyeTickets
|
||||||
|
version: ${version}
|
||||||
|
main: party.cybsec.oyetickets.OyeTicketsPlugin
|
||||||
|
api-version: "1.21"
|
||||||
|
author: jawn
|
||||||
|
description: staff support ticket system with gui-driven workflow
|
||||||
|
|
||||||
|
|
||||||
|
commands:
|
||||||
|
ticket:
|
||||||
|
description: create a new support ticket
|
||||||
|
permission: tickets.create
|
||||||
|
usage: /ticket
|
||||||
|
tickets:
|
||||||
|
description: view and manage support tickets
|
||||||
|
permission: tickets.view
|
||||||
|
usage: /tickets
|
||||||
|
oyetickets:
|
||||||
|
description: admin commands for oyetickets
|
||||||
|
permission: tickets.admin
|
||||||
|
usage: /oyetickets <reload|status>
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
tickets.create:
|
||||||
|
description: allows creating support tickets
|
||||||
|
default: true
|
||||||
|
tickets.view:
|
||||||
|
description: allows viewing support tickets
|
||||||
|
default: op
|
||||||
|
tickets.manage:
|
||||||
|
description: allows managing support tickets
|
||||||
|
default: op
|
||||||
|
tickets.teleport:
|
||||||
|
description: allows teleporting to ticket locations
|
||||||
|
default: op
|
||||||
|
tickets.admin:
|
||||||
|
description: allows reloading and managing the plugin
|
||||||
|
default: op
|
||||||
Reference in New Issue
Block a user