Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1e0706d
chore: update config.example.yml to clarify xpApiUrl usage
brentspine Jan 15, 2026
bd33feb
docs: add setup instructions for Discord Ticket Bot
brentspine Jan 15, 2026
bef3d28
fix: set SQLite temp directory to avoid permission issues
brentspine Jan 15, 2026
0223eeb
chore: clean-up sonar issues
brentspine Jan 15, 2026
17c8eee
refactor(duplicates): streamline transcript generation and notificati…
brentspine Jan 15, 2026
707fef0
chore: revert bogus sonar warning
brentspine Jan 15, 2026
dd1c3cf
fix: Send Close Embed again
brentspine Jan 15, 2026
a8a3549
doc: update readme
brentspine Jan 15, 2026
11640eb
fix: improve ticket creation error handling and add info message logging
brentspine Jan 15, 2026
7832a8f
fix: add info message logging for ticket category and additional details
brentspine Jan 15, 2026
f7fa3b4
feat: add privileged supporter roles and config dump command. 2 Sonar…
brentspine Jan 15, 2026
b866615
chore(warnings): Reduce Warnings and Sonar Issues
brentspine Jan 15, 2026
86e04c2
doc: remove IntelliJ IDEA setup instructions from HOST_IT.md
brentspine Jan 15, 2026
9c4fc68
feat: refactor supporter rating stats handling with a new helper clas…
brentspine Jan 15, 2026
192028e
fix: handle potential null member in GetTranscript class for ticket o…
brentspine Jan 16, 2026
ee233b0
fix: Make config-dump command dev-only
brentspine Jan 16, 2026
1367fe1
refactor: Use TimeUnit instead of multiplying seconds
brentspine Jan 20, 2026
c627d56
refactor: remove config-dump
brentspine Jan 20, 2026
83acf0d
refactor: Remove usage of var in old and new code
brentspine Jan 20, 2026
9abe249
refactor: Replace TimeUnit.SECONDS multiplication with TimeUnit const…
brentspine Jan 20, 2026
26e97ff
Merge branch 'master' into master
brentspine Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions HOST_IT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# How to Run the Discord Ticket Bot

## Prerequisites
- **Java 21 or higher** installed
- A **Discord Bot Token** (from Discord Developer Portal)
- Your **Discord Server (Guild) ID**

---

## Method 1: Run Locally with Gradle (Recommended for Development)

### Step 1: Build the Project
Open PowerShell in the project directory and run:
```powershell
.\gradlew.bat shadowJar
```

This will compile the code and create a JAR file at:
`build\libs\discord-ticketbot.jar`

### Step 2: Create Configuration Directory
```powershell
mkdir Tickets
```

### Step 3: Create Configuration File
Copy the example config and edit it:
```powershell
Copy-Item config.example.yml Tickets\config.yml
```

Now open `Tickets\config.yml` in a text editor and **at minimum** set:
- `token: "YOUR_BOT_TOKEN_HERE"` - Replace with your Discord bot token

Optional settings you may want to configure:
- `staffId` - Role ID for staff members who can manage tickets
- `logChannel` - Channel ID for ticket transcripts
- `maxTicketsPerUser` - Maximum open tickets per user (default: 3)
- Other settings as needed

### Step 4: Run the Bot
```powershell
java -jar build\libs\discord-ticketbot.jar
```

### Step 5: Set Up in Discord
1. Invite your bot to your Discord server (needs permissions: Manage Channels, Manage Threads, Send Messages, Embed Links, Attach Files, Manage Roles)
2. In Discord, run the command: `/ticket setup`
3. This will automatically configure your `serverId` and create necessary channels/categories

---

## Method 2: Run with Docker

### Using Docker Compose (Easiest)
```powershell
docker-compose up -d
```

Before running, make sure to:
1. Create the `Tickets` directory: `mkdir Tickets`
2. Copy and edit the config: `Copy-Item config.example.yml Tickets\config.yml`
3. Set your bot token in `Tickets\config.yml`

### Using Docker Directly
```powershell
# Build the image
docker build -t discord-ticketbot .

# Run the container
docker run -d --name ticketbot -v ${PWD}/Tickets:/app/Tickets discord-ticketbot
```

---

## Getting Your Bot Token

1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Create a new application or select an existing one
3. Go to the "Bot" section
4. Click "Reset Token" to get your bot token (keep it secret!)
5. Enable these Privileged Gateway Intents:
- SERVER MEMBERS INTENT
- MESSAGE CONTENT INTENT
6. Go to OAuth2 > URL Generator:
- Select scope: `bot`, `applications.commands`
- Select permissions: Administrator (or specific permissions as needed)
- Copy the generated URL and use it to invite the bot to your server

---

## Configuration Overview

Key settings in `Tickets/config.yml`:

| Setting | Required | Description |
|---------|----------|-------------|
| `token` | ✓ | Your Discord bot token |
| `serverId` | Auto | Set automatically by `/ticket setup` |
| `staffId` | - | Staff role ID for ticket management |
| `logChannel` | - | Where transcripts are posted |
| `ratingStatsChannel` | - | Daily rating statistics |
| `pendingRatingCategory` | - | Category for tickets awaiting rating |
| `maxTicketsPerUser` | - | Max open tickets per user (default: 3) |
| `xpApiUrl` / `xpApiKey` | - | Optional XP system integration |

---

## Troubleshooting

**"Invalid Token" error**:
- Check that your bot token is correct in `Tickets/config.yml`
- Make sure there are no extra spaces or quotes

**"Missing Permissions" error**:
- Ensure your bot has Administrator permission or at least:
- Manage Channels, Manage Threads, Send Messages, Embed Links, Attach Files, Manage Roles

**"Database error"**:
- The bot creates a SQLite database automatically in the `Tickets` folder
- Make sure the bot has write permissions to this directory

**Commands not showing**:
- Make sure you've enabled `applications.commands` scope when inviting the bot
- Wait a few minutes for Discord to sync commands
- Try running `/ticket setup` to register commands

---

## Default Ticket Categories

The bot supports these ticket types:
- General Support
- Bug Report
- Crash Report
- Payment Issue
- Report User
- Security Issue

You can customize categories by modifying the category classes in `src/main/java/eu/greev/dcbot/ticketsystem/categories/`

---

## Next Steps After Running

1. Run `/ticket setup` in your Discord server
2. Configure staff roles with `staffId` in config
3. Set up log channels for transcripts
4. Test creating a ticket
5. Explore other commands like `/stats`, `/cleanup`, etc.

Enjoy your new ticket system! 🎫
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Greev Ticket Bot

The self coded ticket bot of Greev.eu with many features.\
[Join the Greev.eu Discord](https://grv.sh/discord)
[Join the Greev.eu Discord](https://grv.sh/discord)\
Fork by NoriskClient\
[Join the NoriskClient Discord](https://discord.norisk.gg)

## How to use?

Copy the built Jar file from the Actions tab of this repository, run it the first time and paste afterwards your bot token into `./Tickets/config.yml`\
Then run the bot again, use `/ticket setup` in Discord and enjoy your new ticket bot :)
See the [self host guide](HOST_IT.md) for instructions on how to host the bot yourself.
6 changes: 5 additions & 1 deletion config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ addToTicketThread:
ratingNotificationChannels:
- 123456789012345678

# Can see sensitive ticket info
privilegedSupporterRoles:
- 123456789012345678

# Custom claim emojis per user (userId: emoji)
claimEmojis:
123456789012345678: "🎫"
Expand All @@ -72,7 +76,7 @@ categoryRoles:
- 123456789012345678

# XP System Integration (optional - leave empty to disable)
# Backend API URL for awarding XP to helpers
# Backend API URL for awarding XP to helpers (will append /tickets/award-xp)
xpApiUrl: ""
# API key for authentication
xpApiKey: ""
21 changes: 18 additions & 3 deletions src/main/java/eu/greev/dcbot/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
import net.dv8tion.jda.api.requests.GatewayIntent;
Expand All @@ -49,6 +50,7 @@
import java.util.List;
import java.util.stream.Collectors;

@SuppressWarnings({"squid:S1192", "squid:S2386"})
@Slf4j
public class Main {
public static final Map<String, Interaction> INTERACTIONS = new HashMap<>();
Expand Down Expand Up @@ -117,7 +119,8 @@ public static void main(String[] args) throws InterruptedException, IOException

ticketService.loadOverflowCategories();

jda.updateCommands().addCommands(Commands.slash("ticket", "Manage the ticket system")
SlashCommandData ticketCommand = Commands.slash("ticket", "Manage the ticket system");
jda.updateCommands().addCommands(ticketCommand
.addSubcommands(new SubcommandData("add", "Add a User to this ticket")
.addOption(OptionType.USER, "member", "The user adding to the current ticket", true))
.addSubcommands(new SubcommandData("remove", "Remove a User from this ticket")
Expand Down Expand Up @@ -153,7 +156,7 @@ public static void main(String[] args) throws InterruptedException, IOException
.addOption(OptionType.STRING, "type", "Report type: daily, weekly, monthly", true))
.addSubcommands(new SubcommandData("set-privacy", "Toggle ob deine XP/Ratings öffentlich angezeigt werden")
.addOption(OptionType.STRING, "mode", "visible oder hidden", true))
).queue(s -> s.get(0).getSubcommands().forEach(c -> {
).queue(s -> s.getFirst().getSubcommands().forEach(c -> {
if (c.getName().equals("get-tickets")) {
getTicketCommandId = c.getId();
} else if (c.getName().equals("create")) {
Expand Down Expand Up @@ -212,11 +215,22 @@ public static void main(String[] args) throws InterruptedException, IOException
registerInteraction("debug-stats", new DebugStats(config, ticketService, missingPerm, jda));
registerInteraction("set-privacy", new SetPrivacy(config, ticketService, missingPerm, jda, supporterSettingsData));

if (config.isDevMode()) {
log.warn("Dev mode enabled, registering dev commands");
ticketCommand.addSubcommands(new SubcommandData("config-dump", "Dump the current ticket configuration"));
registerInteraction("config-dump", new ConfigDump(config, ticketService, missingPerm, jda));
}

log.info("Started: {}", OffsetDateTime.now(ZoneId.systemDefault()));

}

private static void initDatasource() {
// Set SQLite temp directory to avoid permission issues with C:\Windows\TEMP
String sqliteTempDir = new File("./Tickets/temp").getAbsolutePath();
new File(sqliteTempDir).mkdirs();
System.setProperty("org.sqlite.tmpdir", sqliteTempDir);

SQLiteDataSource ds = new SQLiteDataSource();
ds.setUrl("jdbc:sqlite:./Tickets/tickets.db");
jdbi = Jdbi.create(ds);
Expand Down Expand Up @@ -269,4 +283,5 @@ private static void registerCategory(ICategory category, Config config, TicketSe
OVERFLOW_CHANNEL_CATEGORIES.put(category, new ArrayList<>());
CATEGORIES.add(category);
}
}
}

2 changes: 1 addition & 1 deletion src/main/java/eu/greev/dcbot/scheduler/DailyScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class DailyScheduler {
private TicketService ticketService;

public void start() {
scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 24 * 60 * 60, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 24 * 60 * 60L, TimeUnit.SECONDS);
}

private int getInitialDelay() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class HourlyScheduler {
private XpService xpService;

public void start() {
scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 60 * 60, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 60L * 60L, TimeUnit.SECONDS);
}

private int getInitialDelay() {
Expand Down
57 changes: 35 additions & 22 deletions src/main/java/eu/greev/dcbot/scheduler/RatingStatsScheduler.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.greev.dcbot.scheduler;

import eu.greev.dcbot.ticketsystem.service.RatingData;
import eu.greev.dcbot.ticketsystem.service.SupporterRatingStatsHelper;
import eu.greev.dcbot.ticketsystem.service.SupporterSettingsData;
import eu.greev.dcbot.ticketsystem.service.TicketData;
import eu.greev.dcbot.utils.Config;
Expand Down Expand Up @@ -42,11 +43,11 @@ public RatingStatsScheduler(Config config, RatingData ratingData, TicketData tic
}

public void start() {
scheduler.scheduleAtFixedRate(this::sendDailyReport, getInitialDelayForHour(9), 24 * 60 * 60, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(this::sendDailyReport, getInitialDelayForHour(9), 24 * 60 * 60L, TimeUnit.SECONDS);

scheduler.scheduleAtFixedRate(this::sendWeeklyReport, getInitialDelayForWeekly(), 7 * 24 * 60 * 60, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(this::sendWeeklyReport, getInitialDelayForWeekly(), 7 * 24 * 60 * 60L, TimeUnit.SECONDS);

scheduler.scheduleAtFixedRate(this::sendMonthlyReport, getInitialDelayForMonthly(), 30 * 24 * 60 * 60, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(this::sendMonthlyReport, getInitialDelayForMonthly(), 30 * 24 * 60 * 60L, TimeUnit.SECONDS);

log.info("RatingStatsScheduler started - Daily reports at 9:00, Weekly reports on Monday 9:00, Monthly reports on 1st at 9:00");
}
Expand Down Expand Up @@ -327,42 +328,56 @@ private String formatTicketStatsRanked(Map<String, Integer> ticketsBySupporter,
}

private String formatRatingStats(Map<String, Double> avgRatings, Map<String, Integer> countRatings, int limit) {
var topSupporters = SupporterRatingStatsHelper.topSupporters(avgRatings, countRatings, limit);
StringBuilder sb = new StringBuilder();
int count = 0;
for (var entry : avgRatings.entrySet()) {
if (count >= limit) break;
String userId = entry.getKey();
for (var entry : topSupporters) {
String userId = entry.supporterId();
boolean hideStats = supporterSettingsData.isHideStats(userId);
String mention = hideStats ? "Anonym" : getUserMention(userId);
if (hideStats) {
sb.append(mention).append(": ★★★★★ (Ø ???, ???x)\n");
} else {
double avg = entry.getValue();
int ratings = countRatings.getOrDefault(userId, 0);
sb.append(mention).append(": ").append(getStarDisplay(avg)).append(" (Ø ").append(String.format("%.2f", avg)).append(", ").append(ratings).append("x)\n");
double avg = entry.avgRating();
int ratings = entry.ratingCount();
sb.append(mention)
.append(": ")
.append(SupporterRatingStatsHelper.starDisplay(avg))
.append(" (Ø ")
.append(String.format("%.2f", avg))
.append(", ")
.append(ratings)
.append("x)\n");
}
count++;
}
return sb.toString().trim();
}

private String formatRatingStatsRanked(Map<String, Double> avgRatings, Map<String, Integer> countRatings, int limit) {
var topSupporters = SupporterRatingStatsHelper.topSupporters(avgRatings, countRatings, limit);
StringBuilder sb = new StringBuilder();
int rank = 1;
for (var entry : avgRatings.entrySet()) {
if (rank > limit) break;
String userId = entry.getKey();
for (int i = 0; i < topSupporters.size(); i++) {
var entry = topSupporters.get(i);
int rank = i + 1;
String userId = entry.supporterId();
boolean hideStats = supporterSettingsData.isHideStats(userId);
String mention = hideStats ? "Anonym" : getUserMention(userId);
String medal = rank == 1 ? "🥇" : rank == 2 ? "🥈" : rank == 3 ? "🥉" : rank + ".";
if (hideStats) {
sb.append(medal).append(" ").append(mention).append(": ★★★★★ (Ø ???, ???x)\n");
} else {
double avg = entry.getValue();
int ratings = countRatings.getOrDefault(userId, 0);
sb.append(medal).append(" ").append(mention).append(": ").append(getStarDisplay(avg)).append(" (Ø ").append(String.format("%.2f", avg)).append(", ").append(ratings).append("x)\n");
double avg = entry.avgRating();
int ratings = entry.ratingCount();
sb.append(medal)
.append(" ")
.append(mention)
.append(": ")
.append(SupporterRatingStatsHelper.starDisplay(avg))
.append(" (Ø ")
.append(String.format("%.2f", avg))
.append(", ")
.append(ratings)
.append("x)\n");
}
rank++;
}
return sb.toString().trim();
}
Expand All @@ -378,8 +393,6 @@ private String getUserMention(String id) {
}

private String getStarDisplay(double avg) {
int fullStars = (int) Math.round(avg);
fullStars = Math.max(0, Math.min(5, fullStars));
return "★".repeat(fullStars) + "☆".repeat(5 - fullStars);
return SupporterRatingStatsHelper.starDisplay(avg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.AllArgsConstructor;
import net.dv8tion.jda.api.events.Event;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;

@AllArgsConstructor
Expand All @@ -25,7 +26,7 @@ public void execute(Event evt) {
}

event.getMessage().editMessageComponents(
net.dv8tion.jda.api.interactions.components.ActionRow.of(selectionBuilder.build())
ActionRow.of(selectionBuilder.build())
).queue();
}
}
Loading