This commit is contained in:
commit
b0ec15d9c4
7 files changed
+393
No files matched your search
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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]
|
||||
Reference in new issue
Block a user