commit b0ec15d9c486f81070d2426529bda1c7f20e0116 Author: asminme Date: Thu Oct 8 08:49:36 2026 +0200 Initial plugin setup diff --git a/.gitea/workflows/build-plugin.yml b/.gitea/workflows/build-plugin.yml new file mode 100644 index 0000000..a1ee1c9 --- /dev/null +++ b/.gitea/workflows/build-plugin.yml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb12c00 --- /dev/null +++ b/README.md @@ -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) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..1774653 --- /dev/null +++ b/build.gradle @@ -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 +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..2075c89 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'AsminDiscordBridge' diff --git a/src/main/java/me/asmin/discordbridge/AsminDiscordBridge.java b/src/main/java/me/asmin/discordbridge/AsminDiscordBridge.java new file mode 100644 index 0000000..8d034e8 --- /dev/null +++ b/src/main/java/me/asmin/discordbridge/AsminDiscordBridge.java @@ -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 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()); + } + }); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..9fbd219 --- /dev/null +++ b/src/main/resources/config.yml @@ -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 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..03daa38 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -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]