Initial plugin setup
Build Plugin / build-and-release (push) Canceled after 0s

This commit is contained in:
asminme committed 2026-10-08 08:49:36 +02:00
commit b0ec15d9c4
7 files changed
+393

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
name: Build Plugin
run-name: Build AsminDiscordBridge
on:
push:
branches:
- main
- master
workflow_dispatch:
concurrency:
group: asmin-discord-bridge-release
cancel-in-progress: false
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Install Gradle 8.7
run: |
curl -fsSLo /tmp/gradle.zip https://services.gradle.org/distributions/gradle-8.7-bin.zip
unzip -q /tmp/gradle.zip -d /tmp
echo "/tmp/gradle-8.7/bin" >> "$GITHUB_PATH"
- name: Calculate next version
id: version
shell: bash
run: |
latest="$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)"
if [[ -z "$latest" ]]; then
version="1.0.0"
else
current="${latest#v}"
IFS='.' read -r major minor patch <<< "$current"
if (( patch >= 9 )); then
version="$major.$((minor + 1)).0"
else
version="$major.$minor.$((patch + 1))"
fi
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
- name: Build plugin
run: gradle --no-daemon clean build -PbuildVersion=${{ steps.version.outputs.version }}
- name: Create Gitea release and upload plugin
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
if [[ -z "$GITEA_TOKEN" ]]; then
echo "Dodaj token z prawem zapisu do repo jako sekret GITEA_TOKEN."
exit 1
fi
jar="AsminDiscordBridge_${VERSION}.jar"
api="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY"
release="$(curl --fail-with-body -sS -X POST "$api/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$TAG\",\"name\":\"AsminDiscordBridge $VERSION\",\"body\":\"Automatyczny build AsminDiscordBridge $VERSION.\",\"draft\":false,\"prerelease\":false}")"
release_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<< "$release")"
curl --fail-with-body -sS -X POST "$api/releases/$release_id/assets?name=$jar" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@build/libs/$jar;filename=$jar"
+9
View File
@@ -0,0 +1,9 @@
# AsminDiscordBridge
Prosty plugin do Minecrafta, który pokazuje na Discordzie, czy serwer jest online oraz kto aktualnie gra. Lista graczy jest sortowana według rangi z LuckPerms. Wiadomość odświeża się co 180 sekund, a jej ID plugin zapamiętuje w `memory.yml`, żeby po restarcie edytować tę samą wiadomość.
Plugin jest napisany pod moje własne potrzeby. Kod jest „taki sobie” i nie wiem, czy będzie stabilny — **nie odpowiadam za jego działanie**. Jeśli chcesz go użyć albo poeksperymentować, zapraszam. Chętnie przyjmę feedback w Pull Requeście albo na adresie [hello@asmin.me](mailto:hello@asmin.me).
Możesz edytować kod. Jeśli jednak zamierzasz opublikować własną wersję, proszę o uznanie mojego autorstwa w creditsach.
**Autor:** [Asmin_](https://asmin.me)
+48
View File
@@ -0,0 +1,48 @@
plugins {
id 'java'
id 'com.gradleup.shadow' version '8.3.6'
}
group = 'me.asmin.discordbridge'
version = providers.gradleProperty('buildVersion').getOrElse('1.0.0')
repositories {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.lucko.me/' }
}
dependencies {
compileOnly 'io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT'
compileOnly 'net.luckperms:api:5.4'
implementation 'net.dv8tion:JDA:5.2.2'
implementation 'org.slf4j:slf4j-simple:2.0.16'
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(17)
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
processResources {
filesMatching('plugin.yml') {
expand(version: project.version)
}
}
shadowJar {
archiveFileName = "AsminDiscordBridge_${project.version}.jar"
relocate 'org.slf4j', 'me.asmin.discordbridge.libs.slf4j'
relocate 'org.jetbrains.annotations', 'me.asmin.discordbridge.libs.annotations'
}
tasks.build {
dependsOn tasks.shadowJar
}
tasks.jar {
enabled = false
}
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'AsminDiscordBridge'
@@ -0,0 +1,237 @@
package me.asmin.discordbridge;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.model.group.Group;
import net.luckperms.api.model.user.User;
import net.luckperms.api.query.QueryOptions;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
public final class AsminDiscordBridge extends JavaPlugin {
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)§[0-9A-FK-OR]");
private JDA jda;
private LuckPerms luckPerms;
private File memoryFile;
private YamlConfiguration memory;
private BukkitTask refreshTask;
private long channelId;
private int embedColor;
private volatile boolean shuttingDown;
@Override
public void onEnable() {
saveDefaultConfig();
loadMemory();
String token = getConfig().getString("discord.token", "").trim();
String configuredChannel = getConfig().getString("discord.channel-id", "").trim();
if (token.isEmpty() || token.equals("WSTAW_TOKEN_BOTA") || !configuredChannel.matches("\\d+")) {
getLogger().severe("Uzupełnij discord.token i discord.channel-id w config.yml. Plugin zostaje wyłączony.");
getServer().getPluginManager().disablePlugin(this);
return;
}
channelId = Long.parseLong(configuredChannel);
embedColor = parseColor(getConfig().getString("server.embed-color", "#90EE90"));
if (getServer().getPluginManager().getPlugin("LuckPerms") != null) {
try {
luckPerms = LuckPermsProvider.get();
} catch (IllegalStateException exception) {
getLogger().warning("Nie udało się podłączyć do LuckPerms. Lista wyświetli same nicki.");
}
} else {
getLogger().warning("LuckPerms nie jest zainstalowany. Lista wyświetli same nicki.");
}
try {
jda = JDABuilder.createDefault(token)
.enableIntents(GatewayIntent.GUILD_MESSAGES)
.addEventListeners(new ListenerAdapter() {
@Override
public void onReady(ReadyEvent event) {
getLogger().info("Połączono z Discordem jako " + event.getJDA().getSelfUser().getName());
getServer().getScheduler().runTask(AsminDiscordBridge.this, () -> {
publish(true);
startRefreshTask();
});
}
})
.build();
} catch (RuntimeException exception) {
getLogger().severe("Nie udało się uruchomić bota Discord: " + exception.getMessage());
getServer().getPluginManager().disablePlugin(this);
}
}
@Override
public void onDisable() {
shuttingDown = true;
if (refreshTask != null) {
refreshTask.cancel();
}
if (jda != null) {
try {
TextChannel channel = jda.getTextChannelById(channelId);
String messageId = memory.getString("message-id", "");
if (channel != null && !messageId.isBlank()) {
channel.editMessageEmbedsById(messageId, createEmbed(false)).complete();
}
} catch (Exception exception) {
getLogger().warning("Nie udało się ustawić statusu OFFLINE: " + exception.getMessage());
} finally {
jda.shutdownNow();
try {
jda.awaitShutdown(3, TimeUnit.SECONDS);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
}
}
}
}
private void startRefreshTask() {
if (shuttingDown || refreshTask != null) {
return;
}
long seconds = Math.max(30L, getConfig().getLong("server.refresh-seconds", 180L));
refreshTask = getServer().getScheduler().runTaskTimer(this, () -> publish(true), seconds * 20L, seconds * 20L);
}
private void publish(boolean online) {
if (jda == null || shuttingDown) {
return;
}
TextChannel channel = jda.getTextChannelById(channelId);
if (channel == null) {
getLogger().warning("Nie znaleziono kanału Discord " + channelId + ". Sprawdź ID i dostęp bota.");
return;
}
String messageId = memory.getString("message-id", "");
MessageEmbed embed = createEmbed(online);
if (messageId.isBlank()) {
channel.sendMessageEmbeds(embed).queue(message -> saveMessageId(message.getId()),
error -> getLogger().warning("Nie udało się wysłać wiadomości statusowej: " + error.getMessage()));
return;
}
channel.editMessageEmbedsById(messageId, embed).queue(
ignored -> { },
error -> {
getLogger().warning("Nie udało się edytować wiadomości statusowej; tworzę nową: " + error.getMessage());
channel.sendMessageEmbeds(embed).queue(message -> saveMessageId(message.getId()),
sendError -> getLogger().warning("Nie udało się wysłać wiadomości statusowej: " + sendError.getMessage()));
}
);
}
private MessageEmbed createEmbed(boolean online) {
String brand = getConfig().getString("server.brand", "ClassicMC.top");
String status = online ? "🟢 ONLINE" : "🔴 OFFLINE";
StringBuilder players = new StringBuilder();
if (online) {
getServer().getOnlinePlayers().stream()
.sorted(Comparator.comparingInt(this::getPlayerWeight).reversed()
.thenComparing(Player::getName, String.CASE_INSENSITIVE_ORDER))
.forEach(player -> {
if (players.length() > 0) players.append('\n');
players.append(formatPrefix(getPlayerPrefix(player)))
.append(ChatColor.stripColor(player.getName()));
});
}
if (players.length() == 0) {
players.append("Brak graczy online");
}
if (players.length() > 3800) {
players.setLength(3800);
players.append("\n… lista skrócona");
}
EmbedBuilder embed = new EmbedBuilder()
.setColor(new Color(embedColor))
.setTitle(brand)
.addField("Status Serwera", status, false)
.addField("Aktywni gracze / Online Players (" + (online ? getServer().getOnlinePlayers().size() : 0) + ")",
players.toString(), false)
.setFooter("AsminDiscordBridge • odświeżanie co "
+ Math.max(30L, getConfig().getLong("server.refresh-seconds", 180L)) + " s");
return embed.build();
}
private int getPlayerWeight(Player player) {
if (luckPerms == null) return Integer.MIN_VALUE;
User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) return Integer.MIN_VALUE;
Set<Group> groups = user.getInheritedGroups(QueryOptions.defaultContextualOptions());
return groups.stream().map(Group::getWeight).filter(weight -> weight != null)
.max(Integer::compareTo).orElse(Integer.MIN_VALUE);
}
private String getPlayerPrefix(Player player) {
if (luckPerms == null) return "";
User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) return "";
String prefix = user.getCachedData().getMetaData(QueryOptions.defaultContextualOptions()).getPrefix();
return prefix == null ? "" : prefix;
}
private String formatPrefix(String prefix) {
String translated = ChatColor.translateAlternateColorCodes('&', prefix);
String plain = ChatColor.stripColor(translated);
if (plain == null || plain.isEmpty()) return "";
// Discord nie obsługuje własnych kolorów tekstu w embedach; zachowujemy pogrubienie prefixu.
return LEGACY_COLOR.matcher(translated).find() && translated.matches("(?s).*§[lL].*")
? "**" + plain + "** " : plain + " ";
}
private int parseColor(String hex) {
try {
return Integer.parseInt(hex.trim().replace("#", ""), 16);
} catch (NumberFormatException exception) {
getLogger().warning("Nieprawidłowy server.embed-color; używam #90EE90.");
return 0x90EE90;
}
}
private void loadMemory() {
memoryFile = new File(getDataFolder(), "memory.yml");
if (!memoryFile.exists()) {
getDataFolder().mkdirs();
}
memory = YamlConfiguration.loadConfiguration(memoryFile);
}
private void saveMessageId(String messageId) {
if (shuttingDown) return;
getServer().getScheduler().runTask(this, () -> {
memory.set("message-id", messageId);
try {
memory.save(memoryFile);
} catch (IOException exception) {
getLogger().severe("Nie udało się zapisać memory.yml: " + exception.getMessage());
}
});
}
}
+10
View File
@@ -0,0 +1,10 @@
# Token utworzysz w Discord Developer Portal. Nie udostępniaj go publicznie.
discord:
token: "WSTAW_TOKEN_BOTA"
channel-id: "WSTAW_ID_KANALU"
server:
brand: "ClassicMC.top"
# Kolor paska embeda w formacie HEX (domyślnie jasna zieleń).
embed-color: "#90EE90"
refresh-seconds: 180
+8
View File
@@ -0,0 +1,8 @@
name: AsminDiscordBridge
version: '${version}'
main: me.asmin.discordbridge.AsminDiscordBridge
api-version: '1.20'
author: Asmin_
website: https://asmin.me
description: Prosty status serwera Minecraft na Discordzie.
softdepend: [LuckPerms]