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