initial commit

This commit is contained in:
2026-04-24 04:09:22 +03:00
commit baa0a2ae3b
17 changed files with 852 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
package dev.mrakells.gtnhtelegram;
import betterquesting.api.api.QuestingAPI;
import betterquesting.api.events.QuestEvent;
import betterquesting.api.properties.NativeProps;
import betterquesting.api.questing.IQuest;
import betterquesting.api.questing.IQuestLine;
import betterquesting.questing.QuestDatabase;
import betterquesting.questing.QuestLineDatabase;
import com.google.common.io.BaseEncoding;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.entity.player.EntityPlayerMP;
public class BetterQuestingListener {
@SubscribeEvent
public void onQuestCompleted(QuestEvent event) {
if (!ModConfig.enabled
|| !ModConfig.sendQuestNotifications
|| event == null
|| event.getType() != QuestEvent.Type.COMPLETED) {
return;
}
Set<UUID> questIds = event.getQuestIDs();
if (questIds == null || questIds.isEmpty()) {
return;
}
EntityPlayerMP player = QuestingAPI.getPlayer(event.getPlayerID());
String playerName = escapeHtml(player == null ? String.valueOf(event.getPlayerID()) : player.getCommandSenderName());
String playerUuid = event.getPlayerID() == null ? "" : escapeHtml(event.getPlayerID().toString());
for (UUID questId : questIds) {
IQuest quest = QuestDatabase.INSTANCE.get(questId);
if (quest == null) {
continue;
}
if (!ModConfig.sendRepeatableQuestNotifications && quest.getProperty(NativeProps.REPEAT_TIME) >= 0) {
continue;
}
String questNameRaw = sanitize(quest.getProperty(NativeProps.NAME), questId.toString());
if (isHiddenTriggerQuest(questNameRaw)) {
continue;
}
String questName = escapeHtml(questNameRaw);
String chapterName = escapeHtml(findChapterName(questId));
String questUrl = escapeHtml(buildQuestUrl(questId));
String questLink = "<a href=\"" + questUrl + "\">" + questName + "</a>";
String message = ModConfig.questMessageFormat
.replace("{player}", playerName)
.replace("{playerUuid}", playerUuid)
.replace("{quest}", questLink)
.replace("{questName}", questName)
.replace("{questId}", escapeHtml(questId.toString()))
.replace("{chapter}", chapterName);
message = message.replace("{questUrl}", questUrl);
TelegramSender.send(message);
}
}
private static String buildQuestUrl(UUID questId) {
String baseUrl = ModConfig.questBaseUrl == null ? "" : ModConfig.questBaseUrl.trim();
return baseUrl + encodeQuestId(questId);
}
private static String encodeQuestId(UUID questId) {
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.putLong(questId.getMostSignificantBits());
buffer.putLong(questId.getLeastSignificantBits());
return BaseEncoding.base64().encode(buffer.array());
}
private static String findChapterName(UUID questId) {
for (Map.Entry<UUID, IQuestLine> entry : QuestLineDatabase.INSTANCE.entrySet()) {
IQuestLine questLine = entry.getValue();
if (questLine != null && questLine.containsKey(questId)) {
return sanitize(questLine.getProperty(NativeProps.NAME), "");
}
}
return "";
}
private static String sanitize(String value, String fallback) {
if (value == null) {
return fallback;
}
String trimmed = stripFormattingCodes(value).trim();
return trimmed.isEmpty() ? fallback : trimmed;
}
private static boolean isHiddenTriggerQuest(String questName) {
return questName != null && questName.startsWith("Trigger:");
}
private static String stripFormattingCodes(String value) {
return value.replaceAll("\u00A7.", "");
}
private static String escapeHtml(String value) {
if (value == null) {
return "";
}
return value
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
}

View File

@@ -0,0 +1,38 @@
package dev.mrakells.gtnhtelegram;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.common.MinecraftForge;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Mod(
modid = GTNHTelegramMod.MOD_ID,
name = GTNHTelegramMod.MOD_NAME,
version = GTNHTelegramMod.VERSION,
acceptableRemoteVersions = "*"
)
public class GTNHTelegramMod {
public static final String MOD_ID = "gtnhtelegram";
public static final String MOD_NAME = "GTNH Telegram Achievements";
public static final String VERSION = "1.0.0";
public static final Logger LOGGER = LogManager.getLogger(MOD_ID);
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
ModConfig.load(event.getSuggestedConfigurationFile());
TelegramSender.configure(
ModConfig.botToken,
ModConfig.chatId,
ModConfig.connectTimeoutMs,
ModConfig.readTimeoutMs
);
if (Loader.isModLoaded("betterquesting")) {
MinecraftForge.EVENT_BUS.register(new BetterQuestingListener());
LOGGER.info("BetterQuesting integration enabled");
}
LOGGER.info("Loaded {} with Telegram notifications {}", MOD_NAME, ModConfig.enabled ? "enabled" : "disabled");
}
}

View File

@@ -0,0 +1,90 @@
package dev.mrakells.gtnhtelegram;
import java.io.File;
import java.io.File;
import net.minecraftforge.common.config.Configuration;
public final class ModConfig {
public static boolean enabled = true;
public static boolean sendQuestNotifications = true;
public static boolean sendRepeatableQuestNotifications = false;
public static String botToken = "";
public static String chatId = "";
public static String questMessageFormat = "[Minecraft] {player} завершил квест: {quest}";
public static String questBaseUrl = "https://gtnhquestsbook.top/?id=";
public static int connectTimeoutMs = 5000;
public static int readTimeoutMs = 5000;
private ModConfig() {
}
public static void load(File file) {
Configuration configuration = new Configuration(file);
try {
configuration.load();
enabled = configuration.getBoolean(
"enabled",
Configuration.CATEGORY_GENERAL,
true,
"Master switch for Telegram notifications."
);
sendQuestNotifications = configuration.getBoolean(
"sendQuestNotifications",
Configuration.CATEGORY_GENERAL,
true,
"Send Telegram notifications when a BetterQuesting quest is completed."
);
sendRepeatableQuestNotifications = configuration.getBoolean(
"sendRepeatableQuestNotifications",
Configuration.CATEGORY_GENERAL,
false,
"Send Telegram notifications for repeatable BetterQuesting quests too."
);
botToken = configuration.getString(
"botToken",
Configuration.CATEGORY_GENERAL,
"",
"Telegram bot token from BotFather."
).trim();
chatId = configuration.getString(
"chatId",
Configuration.CATEGORY_GENERAL,
"",
"Telegram chat id that will receive notifications."
).trim();
questMessageFormat = configuration.getString(
"questMessageFormat",
Configuration.CATEGORY_GENERAL,
"[Minecraft] {player} завершил квест: {quest}",
"Quest message template. Supported placeholders: {player}, {quest}, {questName}, {questId}, {chapter}, {playerUuid}, {questUrl}."
);
questBaseUrl = configuration.getString(
"questBaseUrl",
Configuration.CATEGORY_GENERAL,
"https://gtnhquestsbook.top/?id=",
"Base URL for online quest links. The encoded BetterQuesting id will be appended automatically."
);
connectTimeoutMs = configuration.getInt(
"connectTimeoutMs",
Configuration.CATEGORY_GENERAL,
5000,
1000,
30000,
"Telegram API connection timeout in milliseconds."
);
readTimeoutMs = configuration.getInt(
"readTimeoutMs",
Configuration.CATEGORY_GENERAL,
5000,
1000,
30000,
"Telegram API read timeout in milliseconds."
);
} finally {
if (configuration.hasChanged()) {
configuration.save();
}
}
}
}

View File

@@ -0,0 +1,134 @@
package dev.mrakells.gtnhtelegram;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
public final class TelegramSender {
private static final Charset UTF_8 = StandardCharsets.UTF_8;
private static final AtomicBoolean MISSING_CONFIG_WARNING_SHOWN = new AtomicBoolean(false);
private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "gtnhtelegram-sender");
thread.setDaemon(true);
return thread;
}
});
private static volatile String botToken = "";
private static volatile String chatId = "";
private static volatile int connectTimeoutMs = 5000;
private static volatile int readTimeoutMs = 5000;
private TelegramSender() {
}
public static void configure(String token, String targetChatId, int connectTimeout, int readTimeout) {
botToken = token == null ? "" : token.trim();
chatId = targetChatId == null ? "" : targetChatId.trim();
connectTimeoutMs = connectTimeout;
readTimeoutMs = readTimeout;
MISSING_CONFIG_WARNING_SHOWN.set(false);
}
public static void send(final String text) {
if (isBlank(botToken) || isBlank(chatId)) {
if (MISSING_CONFIG_WARNING_SHOWN.compareAndSet(false, true)) {
GTNHTelegramMod.LOGGER.warn("Telegram notifications skipped because botToken or chatId is not configured");
}
return;
}
EXECUTOR.submit(new Runnable() {
@Override
public void run() {
postMessage(text);
}
});
}
private static void postMessage(String text) {
HttpURLConnection connection = null;
try {
URL url = new URL("https://api.telegram.org/bot" + botToken + "/sendMessage");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(connectTimeoutMs);
connection.setReadTimeout(readTimeoutMs);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
String body = "chat_id=" + encode(chatId)
+ "&text=" + encode(text)
+ "&parse_mode=" + encode("HTML")
+ "&disable_web_page_preview=true";
byte[] payload = body.getBytes(UTF_8);
OutputStream outputStream = connection.getOutputStream();
try {
outputStream.write(payload);
outputStream.flush();
} finally {
outputStream.close();
}
int responseCode = connection.getResponseCode();
if (responseCode >= 400) {
GTNHTelegramMod.LOGGER.error(
"Telegram API returned HTTP {}: {}",
Integer.valueOf(responseCode),
readResponse(connection.getErrorStream())
);
} else {
readResponse(connection.getInputStream());
}
} catch (Exception e) {
GTNHTelegramMod.LOGGER.error("Failed to send Telegram notification", e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private static String encode(String value) throws IOException {
return URLEncoder.encode(value, UTF_8.name());
}
private static String readResponse(InputStream stream) throws IOException {
if (stream == null) {
return "";
}
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8));
StringBuilder builder = new StringBuilder();
try {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} finally {
reader.close();
}
return builder.toString();
}
private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}

View File

@@ -0,0 +1,16 @@
[
{
"modid": "${modid}",
"name": "${modname}",
"description": "Sends Minecraft 1.7.10 achievement notifications to a Telegram chat.",
"version": "${version}",
"mcversion": "${mcversion}",
"url": "",
"updateUrl": "",
"authorList": ["mrakells"],
"credits": "",
"logoFile": "",
"screenshots": [],
"dependencies": []
}
]