done i think

This commit is contained in:
minster586
2025-08-05 23:52:08 -04:00
parent f137e61d3c
commit 4c2db6f87e
11 changed files with 683 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

97
pom.xml
View File

@@ -0,0 +1,97 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.minster586</groupId>
<artifactId>tiktok-stream-plugin</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>TikTokStreamPlugin</name>
<description>Integrates TikTok Live events with Minecraft 1.19</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<!-- Spigot API for Minecraft 1.19 -->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- Java-WebSocket for TikTok stream events -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<!-- Gson for JSON parsing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- SnakeYAML for YAML parsing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
</dependencies>
<repositories>
<!-- Spigot repo for 1.19 snapshot -->
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
<build>
<plugins>
<!-- Compiler plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Shade plugin to bundle dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<relocations>
<relocation>
<pattern>org.java_websocket</pattern>
<shadedPattern>com.minster586.shaded.websocket</shadedPattern>
</relocation>
</relocations>
<minimizeJar>true</minimizeJar>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,102 @@
package com.minster586.tiktokstream.events;
import com.minster586.tiktokstream.TikTokStreamPlugin;
import com.minster586.tiktokstream.config.ConfigManager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.ChatMessageType;
import com.minster586.ui.NotificationManager;
import java.util.List;
public class TikTokEventDispatcher {
private final ConfigManager config;
public TikTokEventDispatcher() {
this.config = TikTokStreamPlugin.getInstance().getConfigManager();
}
public void handleGift(String username, String giftName, int amount) {
if (!config.isGiftEnabled()) return;
List<String> locations = config.getGiftLocations();
String prefix = config.getPrefix();
String chatMessage = config.getGiftChatMessage()
.replace("%prefix%", prefix)
.replace("%username%", username)
.replace("%giftName%", giftName)
.replace("%amount%", String.valueOf(amount));
String actionBarMessage = config.getGiftActionBarMessage()
.replace("%prefix%", prefix)
.replace("%username%", username)
.replace("%giftName%", giftName)
.replace("%amount%", String.valueOf(amount));
if (locations.contains("chat")) {
Bukkit.broadcastMessage(chatMessage);
}
if (locations.contains("action-bar")) {
for (Player player : Bukkit.getOnlinePlayers()) {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(actionBarMessage));
}
}
// Toast notifications removed. Use NotificationManager for chat/action-bar only.
}
public void handleJoin(String username) {
if (!config.isJoinEnabled()) return;
String location = config.getJoinLocation();
String message = config.getJoinActionBarMessage().replace("%username%", username);
switch (location) {
case "chat" -> Bukkit.broadcastMessage(config.getJoinChatMessage().replace("%username%", username));
case "action-bar" -> Bukkit.getOnlinePlayers().forEach(p -> p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(message)));
// Toast notifications removed
}
}
public void handleChat(String username, String messageText) {
if (!config.isChatEnabled()) return;
String location = config.getChatLocation();
String formatted = config.getChatMessage()
.replace("%prefix%", config.getPrefix())
.replace("%username%", username)
.replace("%message%", messageText);
switch (location) {
case "chat" -> Bukkit.broadcastMessage(formatted);
case "action-bar" -> Bukkit.getOnlinePlayers().forEach(p -> p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(
config.getChatActionBarMessage()
.replace("%username%", username)
.replace("%message%", messageText)
)));
// Toast notifications removed
}
}
public void handleFollow(String username) {
if (!config.isFollowEnabled()) return;
List<String> locations = config.getFollowLocations();
if (locations.contains("chat")) {
Bukkit.broadcastMessage(config.getFollowChatMessage().replace("%username%", username));
}
if (locations.contains("action-bar")) {
Bukkit.getOnlinePlayers().forEach(p -> p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(
config.getFollowActionBarMessage().replace("%username%", username)
)));
}
// Toast notifications removed. Use NotificationManager for chat/action-bar only.
}
}

View File

@@ -0,0 +1,44 @@
package com.minster586.tiktokstream;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class TikTokLiveCommand implements CommandExecutor, TabCompleter {
private final TikTokStreamPlugin plugin;
private static final String PERMISSION = "tiktok.live";
public TikTokLiveCommand(TikTokStreamPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission(PERMISSION)) {
sender.sendMessage(ChatColor.RED + "You do not have permission to use this command.");
return true;
}
if (args.length != 1 || !(args[0].equalsIgnoreCase("enable") || args[0].equalsIgnoreCase("disable"))) {
sender.sendMessage(ChatColor.YELLOW + "Usage: /tiktok live <enable|disable>");
return true;
}
boolean enable = args[0].equalsIgnoreCase("enable");
plugin.setTiktokLiveEnabled(enable);
sender.sendMessage(ChatColor.GREEN + "TikTok live notifications " + (enable ? "enabled" : "disabled") + ".");
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
return Arrays.asList("enable", "disable");
}
return Collections.emptyList();
}
}

View File

@@ -2,15 +2,18 @@ package com.minster586.tiktokstream;
import com.minster586.tiktokstream.config.ConfigManager; import com.minster586.tiktokstream.config.ConfigManager;
import com.minster586.tiktokstream.websocket.StreamerBotWebSocketClient; import com.minster586.tiktokstream.websocket.StreamerBotWebSocketClient;
import com.minster586.tiktokstream.util.GiftMappingManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.net.URI; import java.net.URI;
public class TikTokStreamPlugin extends JavaPlugin {
public class TikTokStreamPlugin extends JavaPlugin {
private static TikTokStreamPlugin instance; private static TikTokStreamPlugin instance;
private ConfigManager configManager; private ConfigManager configManager;
private StreamerBotWebSocketClient webSocketClient; private StreamerBotWebSocketClient webSocketClient;
private GiftMappingManager giftMappingManager;
private boolean tiktokLiveEnabled = true;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -20,12 +23,31 @@ public class TikTokStreamPlugin extends JavaPlugin {
configManager = new ConfigManager(this); configManager = new ConfigManager(this);
configManager.load(); configManager.load();
// Initialize GiftMappingManager
String giftMappingUrl = getConfig().getString("config.giftMappingUrl");
if (giftMappingUrl != null && !giftMappingUrl.isEmpty()) {
giftMappingManager = new GiftMappingManager(this, giftMappingUrl);
} else {
getLogger().warning("No giftMappingUrl found in config! Gift names will not be available.");
}
String websocketUrl = configManager.getWebSocketUrl(); String websocketUrl = configManager.getWebSocketUrl();
webSocketClient = new StreamerBotWebSocketClient(URI.create(websocketUrl)); webSocketClient = new StreamerBotWebSocketClient(URI.create(websocketUrl));
webSocketClient.connect(); webSocketClient.connect();
// Register /tiktok live command
getCommand("tiktok").setExecutor(new TikTokLiveCommand(this));
getCommand("tiktok").setTabCompleter(new TikTokLiveCommand(this));
getLogger().info("TikTokStreamPlugin enabled and connected to Streamer.bot."); getLogger().info("TikTokStreamPlugin enabled and connected to Streamer.bot.");
} }
public boolean isTiktokLiveEnabled() {
return tiktokLiveEnabled;
}
public void setTiktokLiveEnabled(boolean enabled) {
this.tiktokLiveEnabled = enabled;
}
@Override @Override
public void onDisable() { public void onDisable() {
@@ -42,4 +64,8 @@ public class TikTokStreamPlugin extends JavaPlugin {
public ConfigManager getConfigManager() { public ConfigManager getConfigManager() {
return configManager; return configManager;
} }
public GiftMappingManager getGiftMappingManager() {
return giftMappingManager;
}
} }

View File

@@ -0,0 +1,150 @@
package com.minster586.tiktokstream.config;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
public class ConfigManager {
private final JavaPlugin plugin;
private FileConfiguration config;
public ConfigManager(JavaPlugin plugin) {
this.plugin = plugin;
}
public void load() {
File configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) {
plugin.saveResource("config.yml", false);
}
config = YamlConfiguration.loadConfiguration(configFile);
// Load defaults from resources/config.yml
InputStream defaultStream = plugin.getResource("config.yml");
if (defaultStream != null) {
YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defaultStream));
config.setDefaults(defaultConfig);
}
}
// 🔧 Accessors (no hardcoded defaults — rely on config.yml)
public String getPrefix() {
return config.getString("config.prefix");
}
public String getTikTokUsername() {
return config.getString("config.tiktok_username");
}
public String getGiftMappingUrl() {
return config.getString("config.giftMappingUrl");
}
public String getWebSocketUrl() {
return config.getString("config.websocket.url");
}
public int getWebSocketRefreshDelay() {
return config.getInt("config.websocket.refresh-delay");
}
public boolean isJoinEnabled() {
return config.getBoolean("config.settings.join.joined-enabled");
}
public String getJoinLocation() {
return config.getString("config.settings.join.loctions");
}
public String getJoinChatMessage() {
return config.getString("config.settings.join.chat-message");
}
public String getJoinActionBarMessage() {
return config.getString("config.settings.join.action-bar-message");
}
public boolean isGiftEnabled() {
return config.getBoolean("config.settings.gift.gifts-enabled");
}
public List<String> getGiftLocations() {
return parseLocationList("config.settings.gift.loctions");
}
public String getGiftChatMessage() {
return config.getString("config.settings.gift.chat-message");
}
public String getGiftActionBarMessage() {
return config.getString("config.settings.gift.action-bar-message");
}
public boolean isChatEnabled() {
return config.getBoolean("config.settings.chat.chat-enabled");
}
public String getChatLocation() {
return config.getString("config.settings.chat.loctions");
}
public String getChatMessage() {
return config.getString("config.settings.chat.chat-message");
}
public String getChatActionBarMessage() {
return config.getString("config.settings.chat.action-bar-message");
}
public boolean isFollowEnabled() {
return config.getBoolean("config.settings.follows.follow-enabled");
}
public List<String> getFollowLocations() {
return parseLocationList("config.settings.follows.loctions");
}
public String getFollowChatMessage() {
return config.getString("config.settings.follows.chat-message");
}
public String getFollowActionBarMessage() {
return config.getString("config.settings.follows.action-bar-message");
}
// 🔧 Helpers
private List<String> parseLocationList(String path) {
String raw = config.getString(path);
if (raw == null) return Collections.emptyList();
return Arrays.stream(raw.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}
private Integer getNullableInt(String path) {
if (!config.contains(path) || config.get(path) == null) return null;
return config.getInt(path);
}
// Generic config accessors for plugin internals
public int getIntOrDefault(String path, int def) {
if (!config.contains(path) || config.get(path) == null) return def;
return config.getInt(path);
}
public String getStringOrDefault(String path, String def) {
if (!config.contains(path) || config.get(path) == null) return def;
return config.getString(path);
}
}

View File

@@ -0,0 +1,94 @@
package com.minster586.tiktokstream.util;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.yaml.snakeyaml.Yaml;
public class GiftMappingManager {
private final JavaPlugin plugin;
private final File mappingFile;
private Map<Integer, String> giftMap = new HashMap<>();
public GiftMappingManager(JavaPlugin plugin, String mappingUrl) {
this.plugin = plugin;
this.mappingFile = new File(plugin.getDataFolder(), "gift-mapping.yaml");
checkAndUpdateMapping(mappingUrl);
loadMapping();
}
private void checkAndUpdateMapping(String urlStr) {
try {
// Download remote file to temp
File tempFile = File.createTempFile("gift-mapping", ".yaml");
try (InputStream in = new URL(urlStr).openStream();
FileOutputStream out = new FileOutputStream(tempFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
// Compare contents
boolean shouldReplace = !mappingFile.exists() || !filesEqual(mappingFile, tempFile);
if (shouldReplace) {
try (InputStream in = new FileInputStream(tempFile);
OutputStream out = new FileOutputStream(mappingFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
plugin.getLogger().info("Gift mapping updated from remote.");
} else {
plugin.getLogger().info("Gift mapping is up to date.");
}
tempFile.delete();
} catch (IOException e) {
plugin.getLogger().warning("Failed to check/update gift mapping: " + e.getMessage());
}
}
private boolean filesEqual(File f1, File f2) throws IOException {
if (f1.length() != f2.length()) return false;
try (InputStream in1 = new FileInputStream(f1); InputStream in2 = new FileInputStream(f2)) {
int b1, b2;
do {
b1 = in1.read();
b2 = in2.read();
if (b1 != b2) return false;
} while (b1 != -1);
}
return true;
}
// downloadMapping is now handled by checkAndUpdateMapping
public void loadMapping() {
if (!mappingFile.exists()) return;
try (InputStream input = new FileInputStream(mappingFile)) {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(input);
giftMap.clear();
if (data != null) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
try {
int id = Integer.parseInt(entry.getKey());
giftMap.put(id, String.valueOf(entry.getValue()));
} catch (NumberFormatException ignored) {}
}
}
} catch (IOException e) {
plugin.getLogger().warning("Failed to load gift mapping: " + e.getMessage());
}
}
public String getGiftName(int id) {
return giftMap.getOrDefault(id, "Unknown Gift");
}
}

View File

@@ -0,0 +1,65 @@
package com.minster586.tiktokstream.websocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import com.minster586.tiktokstream.TikTokStreamPlugin;
import com.minster586.tiktokstream.config.ConfigManager;
public class StreamerBotWebSocketClient extends WebSocketClient {
private int retryCount = 0;
private final ConfigManager configManager;
private final int maxRetries;
private final String notifyPermission;
private final String notifyMessage;
public StreamerBotWebSocketClient(URI serverUri) {
super(serverUri);
this.configManager = TikTokStreamPlugin.getInstance().getConfigManager();
this.maxRetries = configManager != null ? configManager.getIntOrDefault("config.websocket.retry-count", 3) : 3;
this.notifyPermission = configManager != null ? configManager.getStringOrDefault("config.websocket.notify-permission", "tiktok.live") : "tiktok.live";
this.notifyMessage = configManager != null ? configManager.getStringOrDefault("config.websocket.notify-message", "§c[StreamerBot] Could not connect after %retries% attempts. Check your config or Streamer.bot status.") : "§c[StreamerBot] Could not connect after %retries% attempts. Check your config or Streamer.bot status.";
}
@Override
public void onOpen(ServerHandshake handshakedata) {
retryCount = 0; // Reset on successful connection
}
@Override
public void onMessage(String message) {
// Handle incoming messages from Streamer.bot here
}
@Override
public void onClose(int code, String reason, boolean remote) {
if (retryCount < maxRetries) {
retryCount++;
try {
Thread.sleep(2000L * retryCount); // Exponential backoff
} catch (InterruptedException ignored) {}
this.reconnect();
} else {
String msg = notifyMessage.replace("%retries%", String.valueOf(maxRetries));
notifyOps(msg);
}
}
@Override
public void onError(Exception ex) {
// Optionally log errors
}
private void notifyOps(String message) {
Bukkit.getScheduler().runTask(Bukkit.getPluginManager().getPlugin("TikTokStreamPlugin"), () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.isOp() || player.hasPermission(notifyPermission)) {
player.sendMessage(message);
}
}
});
}
}

View File

@@ -0,0 +1,54 @@
package com.minster586.ui;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.ChatMessageType;
import java.util.Map;
public class NotificationManager {
private static final JavaPlugin plugin = JavaPlugin.getProvidingPlugin(NotificationManager.class);
public static void sendNotification(Player player, ConfigurationSection section, Map<String, String> placeholders) {
if (section == null) return;
String locationsRaw = section.getString("locations", "").toLowerCase();
String[] locations = locationsRaw.split(",");
for (String loc : locations) {
switch (loc.trim()) {
case "chat":
sendChat(player, section.getString("chat-message", ""), placeholders);
break;
case "action-bar":
sendActionBar(player, section.getString("action-bar-message", ""), placeholders);
break;
default:
Bukkit.getLogger().warning("Unknown notification location: " + loc);
}
}
}
private static void sendChat(Player player, String message, Map<String, String> placeholders) {
if (message == null || message.isEmpty()) return;
player.sendMessage(ChatColor.translateAlternateColorCodes('&', replacePlaceholders(message, placeholders)));
}
private static void sendActionBar(Player player, String message, Map<String, String> placeholders) {
if (message == null || message.isEmpty()) return;
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(replacePlaceholders(message, placeholders)));
}
private static String replacePlaceholders(String input, Map<String, String> placeholders) {
if (input == null) return "";
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
input = input.replace("%" + entry.getKey() + "%", entry.getValue());
}
return input;
}
}

View File

@@ -0,0 +1,33 @@
config:
prefix: [TikTok]
tiktok_username: Change-me #this is the username of your tiktok account with out the "@" symbol
giftMappingUrl: "https://yourdomain.com/gift-mapping.yaml" #you can change if need but might want to make sure it follows same scheam
websocket:
url: "ws://localhost:8080" # change IP if need
refresh-delay: 6 # this is in seconds
retry-count: 3 # Number of times to retry connecting to Streamer.bot
notify-permission: "tiktok.live" # Permission required to receive connection failure notifications
notify-message: "§c[StreamerBot] Could not connect after %retries% attempts. Check your config or Streamer.bot status."
settings:
join:
joined-enabled: true
loctions: action-bar #this can be chat, action-bar
chat-message: ""
action-bar-message: "%username% joined the stream!"
gift:
gifts-enabled: true
loctions: chat #this can be chat, action-bar
chat-message: "%prefix% - %username% sent a %giftName%!"
action-bar-message: ""
chat:
chat-enabled: true
loctions: chat #this can be chat, action-bar
chat-message: "%prefix% - [%username%] > %message%"
action-bar-message: ""
follows:
follow-enabled: true
loctions: chat #this can be chat, action-bar
chat-message: "%prefix% - [%username%] Followed"
action-bar-message: ""

View File

@@ -0,0 +1,16 @@
name: TikTokStreamPlugin
main: com.minster586.tiktokstream.TikTokStreamPlugin
version: 1.0.0
author: minster586
description: Integrates TikTok Live events with Minecraft 1.19
api-version: 1.19
commands:
tiktok:
description: TikTok live control command
usage: /tiktok live <enable|disable>
permission: tiktok.live
permissions:
tiktok.live:
description: Allows control of TikTok live notifications
default: op