2
0
mirror of https://github.com/proxiodev/RedisBungee.git synced 2026-04-07 23:50:27 +00:00

redo module system

This commit is contained in:
2024-05-12 14:59:50 +04:00
parent e8514b3e8b
commit 765e6fe122
95 changed files with 388 additions and 70 deletions

85
api/build.gradle.kts Normal file
View File

@@ -0,0 +1,85 @@
import java.time.Instant
import java.io.ByteArrayOutputStream
plugins {
`java-library`
`maven-publish`
id("net.kyori.blossom") version "1.2.0"
}
dependencies {
api(libs.guava)
api(libs.jedis)
api(libs.okhttp)
api(libs.configurate)
api(libs.caffeine)
api(libs.adventure.api)
api(libs.adventure.gson)
api(libs.adventure.legacy)
api(libs.adventure.plain)
api(libs.adventure.miniMessage)
}
description = "RedisBungee interfaces"
blossom {
replaceToken("@version@", "$version")
// GIT
val commit: String;
val commitStdout = ByteArrayOutputStream()
rootProject.exec {
standardOutput = commitStdout
commandLine("git", "rev-parse", "HEAD")
}
commit = "$commitStdout".replace("\n", "") // for some reason it adds new line so remove it.
commitStdout.close()
replaceToken("@git_commit@", commit)
}
java {
withJavadocJar()
withSourcesJar()
}
tasks {
// thanks again for paper too
withType<Javadoc> {
val options = options as StandardJavadocDocletOptions
options.use()
options.isDocFilesSubDirs = true
val jedisVersion = libs.jedis.get().version
val configurateVersion = libs.configurate.get().version
val guavaVersion = libs.guava.get().version
val adventureVersion = libs.adventure.plain.get().version
options.links(
"https://configurate.aoeu.xyz/$configurateVersion/apidocs/", // configurate
"https://javadoc.io/doc/redis.clients/jedis/$jedisVersion/", // jedis
"https://guava.dev/releases/$guavaVersion/api/docs/", // guava
"https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine",
"https://jd.advntr.dev/api/$adventureVersion"
)
}
compileJava {
options.encoding = Charsets.UTF_8.name()
options.release.set(17)
}
javadoc {
options.encoding = Charsets.UTF_8.name()
}
processResources {
filteringCharset = Charsets.UTF_8.name()
}
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}

View File

@@ -0,0 +1,491 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisClusterSummoner;
import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisPooledSummoner;
import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPooled;
import java.net.InetAddress;
import java.util.*;
/**
* This abstract class is extended by platform plugin to provide some platform specific methods.
* overall its general contains all methods needed by external usage.
*
* @author Ham1255
* @since 0.8.0
*/
@SuppressWarnings("unused")
public abstract class AbstractRedisBungeeAPI {
protected final RedisBungeePlugin<?> plugin;
private static AbstractRedisBungeeAPI abstractRedisBungeeAPI;
public AbstractRedisBungeeAPI(RedisBungeePlugin<?> plugin) {
// this does make sure that no one can replace first initiated API class.
if (abstractRedisBungeeAPI == null) {
abstractRedisBungeeAPI = this;
}
this.plugin = plugin;
}
/**
* Get a combined count of all players on this network.
*
* @return a count of all players found
*/
public final int getPlayerCount() {
return plugin.proxyDataManager().totalNetworkPlayers();
}
/**
* Get the last time a player was on. If the player is currently online, this will return 0. If the player has not been recorded,
* this will return -1. Otherwise it will return a value in milliseconds.
*
* @param player a player name
* @return the last time a player was on, if online returns a 0
*/
public final long getLastOnline(@NonNull UUID player) {
return plugin.playerDataManager().getLastOnline(player);
}
/**
* Get the server where the specified player is playing. This function also deals with the case of local players
* as well, and will return local information on them.
*
* @param player a player uuid
* @return a String name for the server the player is on. Can be Null if plugins is doing weird stuff to the proxy internals
*/
@Nullable
public final String getServerNameFor(@NonNull UUID player) {
return plugin.playerDataManager().getServerFor(player);
}
/**
* Get a combined list of players on this network.
* <p>
* <strong>Note that this function returns an instance of {@link com.google.common.collect.ImmutableSet}.</strong>
*
* @return a Set with all players found
*/
public final Set<UUID> getPlayersOnline() {
return plugin.proxyDataManager().networkPlayers();
}
/**
* Get a combined list of players on this network, as a collection of usernames.
*
* @return a Set with all players found
* @see #getNameFromUuid(java.util.UUID)
* @since 0.3
*/
public final Collection<String> getHumanPlayersOnline() {
Set<String> names = new HashSet<>();
for (UUID uuid : getPlayersOnline()) {
names.add(getNameFromUuid(uuid, false));
}
return names;
}
/**
* Get a full list of players on all servers.
*
* @return a immutable Multimap with all players found on this network
* @since 0.2.5
*/
public final Multimap<String, UUID> getServerToPlayers() {
return plugin.playerDataManager().serversToPlayers();
}
/**
* Get a list of players on the server with the given name.
*
* @param server a server name
* @return a Set with all players found on this server
*/
public final Set<UUID> getPlayersOnServer(@NonNull String server) {
return ImmutableSet.copyOf(getServerToPlayers().get(server));
}
/**
* Get a list of players on the specified proxy.
*
* @param proxyID proxy id
* @return a Set with all UUIDs found on this proxy
*/
public final Set<UUID> getPlayersOnProxy(@NonNull String proxyID) {
return plugin.proxyDataManager().getPlayersOn(proxyID);
}
/**
* Convenience method: Checks if the specified player is online.
*
* @param player a player name
* @return if the player is online
*/
public final boolean isPlayerOnline(@NonNull UUID player) {
return getLastOnline(player) == 0;
}
/**
* Get the {@link java.net.InetAddress} associated with this player.
*
* @param player the player to fetch the IP for
* @return an {@link java.net.InetAddress} if the player is online, null otherwise
* @since 0.2.4
*/
public final InetAddress getPlayerIp(@NonNull UUID player) {
return plugin.playerDataManager().getIpFor(player);
}
/**
* Get the RedisBungee proxy ID this player is connected to.
*
* @param player the player to fetch the IP for
* @return the proxy the player is connected to, or null if they are offline
* @since 0.3.3
*/
public final String getProxy(@NonNull UUID player) {
return plugin.playerDataManager().getProxyFor(player);
}
/**
* Sends a proxy command to all proxies.
*
* @param command the command to send and execute
* @see #sendProxyCommand(String, String)
* @since 0.2.5
*/
public final void sendProxyCommand(@NonNull String command) {
sendProxyCommand("allservers", command);
}
/**
* Sends a proxy command to the proxy with the given ID. "allservers" means all proxies.
*
* @param proxyId a proxy ID
* @param command the command to send and execute
* @see #getProxyId()
* @see #getAllProxies()
* @since 0.2.5
*/
public final void sendProxyCommand(@NonNull String proxyId, @NonNull String command) {
plugin.proxyDataManager().sendCommandTo(proxyId, command);
}
/**
* Sends a message to a PubSub channel which makes PubSubMessageEvent fire.
* <p>
* Note: Since 0.12.0 registering a channel api is no longer required
*
* @param channel The PubSub channel
* @param message the message body to send
* @since 0.3.3
*/
public final void sendChannelMessage(@NonNull String channel, @NonNull String message) {
plugin.proxyDataManager().sendChannelMessage(channel, message);
}
/**
* Get the current BungeeCord / Velocity proxy ID for this server.
*
* @return the current server ID
* @see #getAllProxies()
* @since 0.8.0
*/
public final String getProxyId() {
return plugin.proxyDataManager().proxyId();
}
/**
* Get the current BungeeCord / Velocity proxy ID for this server.
*
* @return the current server ID
* @see #getAllServers()
* @since 0.2.5
* @deprecated to avoid confusion between A server and A proxy see #getProxyId()
*/
@Deprecated
public final String getServerId() {
return getProxyId();
}
/**
* Get all the linked proxies in this network.
*
* @return the list of all proxies
* @see #getProxyId()
* @since 0.8.0
*/
public final List<String> getAllProxies() {
return plugin.proxyDataManager().proxiesIds();
}
/**
* Get all the linked proxies in this network.
*
* @return the list of all proxies
* @see #getServerId()
* @since 0.2.5
* @deprecated to avoid confusion between A server and A proxy see see {@link #getAllProxies()}
*/
@Deprecated
public final List<String> getAllServers() {
return getAllProxies();
}
/**
* Register (a) PubSub channel(s), so that you may handle PubSubMessageEvent for it.
*
* @param channels the channels to register
* @since 0.3
* @deprecated No longer required
*/
@Deprecated
public final void registerPubSubChannels(String... channels) {
}
/**
* Unregister (a) PubSub channel(s).
*
* @param channels the channels to unregister
* @since 0.3
* @deprecated No longer required
*/
@Deprecated
public final void unregisterPubSubChannels(String... channels) {
}
/**
* Fetch a name from the specified UUID. UUIDs are cached locally and in Redis. This function falls back to Mojang
* as a last resort, so calls <strong>may</strong> be blocking.
* <p>
* For the common use case of translating a list of UUIDs into names, use {@link #getHumanPlayersOnline()} instead.
* <p>
* If performance is a concern, use {@link #getNameFromUuid(java.util.UUID, boolean)} as this allows you to disable Mojang lookups.
*
* @param uuid the UUID to fetch the name for
* @return the name for the UUID
* @since 0.3
*/
public final String getNameFromUuid(@NonNull UUID uuid) {
return getNameFromUuid(uuid, true);
}
/**
* Fetch a name from the specified UUID. UUIDs are cached locally and in Redis. This function can fall back to Mojang
* as a last resort if {@code expensiveLookups} is true, so calls <strong>may</strong> be blocking.
* <p>
* For the common use case of translating the list of online players into names, use {@link #getHumanPlayersOnline()}.
* <p>
* If performance is a concern, set {@code expensiveLookups} to false as this will disable lookups via Mojang.
*
* @param uuid the UUID to fetch the name for
* @param expensiveLookups whether or not to perform potentially expensive lookups
* @return the name for the UUID
* @since 0.3.2
*/
public final String getNameFromUuid(@NonNull UUID uuid, boolean expensiveLookups) {
return plugin.getUuidTranslator().getNameFromUuid(uuid, expensiveLookups);
}
/**
* Fetch a UUID from the specified name. Names are cached locally and in Redis. This function falls back to Mojang
* as a last resort, so calls <strong>may</strong> be blocking.
* <p>
* If performance is a concern, see {@link #getUuidFromName(String, boolean)}, which disables the following functions:
* <ul>
* <li>Searching local entries case-insensitively</li>
* <li>Searching Mojang</li>
* </ul>
*
* @param name the UUID to fetch the name for
* @return the UUID for the name
* @since 0.3
*/
public final UUID getUuidFromName(@NonNull String name) {
return getUuidFromName(name, true);
}
/**
* Fetch a UUID from the specified name. Names are cached locally and in Redis. This function falls back to Mojang
* as a last resort if {@code expensiveLookups} is true, so calls <strong>may</strong> be blocking.
* <p>
* If performance is a concern, set {@code expensiveLookups} to false to disable searching Mojang and searching for usernames
* case-insensitively.
*
* @param name the UUID to fetch the name for
* @param expensiveLookups whether or not to perform potentially expensive lookups
* @return the {@link UUID} for the name
* @since 0.3.2
*/
public final UUID getUuidFromName(@NonNull String name, boolean expensiveLookups) {
return plugin.getUuidTranslator().getTranslatedUuid(name, expensiveLookups);
}
/**
* Kicks a player from the network
* calls {@link #getUuidFromName(String)} to get uuid
*
* @param playerName player name
* @param message kick message that player will see on kick
* @since 0.8.0
* @deprecated
*/
@Deprecated
public void kickPlayer(String playerName, String message) {
kickPlayer(getUuidFromName(playerName), message);
}
/**
* Kicks a player from the network
*
* @param playerUUID player name
* @param message kick message that player will see on kick
* @since 0.8.0
* @deprecated
*/
@Deprecated
public void kickPlayer(UUID playerUUID, String message) {
kickPlayer(playerUUID, Component.text(message));
}
/**
* Kicks a player from the network
* calls {@link #getUuidFromName(String)} to get uuid
*
* @param playerName player name
* @param message kick message that player will see on kick
* @since 0.12.0
*/
public void kickPlayer(String playerName, Component message) {
kickPlayer(getUuidFromName(playerName), message);
}
/**
* Kicks a player from the network
*
* @param playerUUID player name
* @param message kick message that player will see on kick
* @since 0.12.0
*/
public void kickPlayer(UUID playerUUID, Component message) {
this.plugin.playerDataManager().kickPlayer(playerUUID, message);
}
/**
* This gives you instance of Jedis
*
* @return {@link Jedis}
* @throws IllegalStateException if the {@link #getMode()} is not equal to {@link RedisBungeeMode#SINGLE}
* @see #getJedisPool()
* @since 0.7.0
*/
public Jedis requestJedis() {
if (getMode() == RedisBungeeMode.SINGLE) {
return getJedisPool().getResource();
} else {
throw new IllegalStateException("Mode is not " + RedisBungeeMode.SINGLE);
}
}
/**
* This gets Redis Bungee {@link JedisPool}
*
* @return {@link JedisPool}
* @throws IllegalStateException if the {@link #getMode()} is not equal to {@link RedisBungeeMode#SINGLE}
* @throws IllegalStateException if JedisPool compatibility mode is disabled in the config
* @since 0.6.5
*/
public JedisPool getJedisPool() {
if (getMode() == RedisBungeeMode.SINGLE) {
JedisPool jedisPool = ((JedisPooledSummoner) this.plugin.getSummoner()).getCompatibilityJedisPool();
if (jedisPool == null) {
throw new IllegalStateException("JedisPool compatibility mode is disabled, Please enable it in the RedisBungee config.yml");
}
return jedisPool;
} else {
throw new IllegalStateException("Mode is not " + RedisBungeeMode.SINGLE);
}
}
/**
* This gives you an instance of JedisCluster that can't be closed
* see {@link com.imaginarycode.minecraft.redisbungee.api.summoners.NotClosableJedisCluster}
*
* @return {@link redis.clients.jedis.JedisCluster}
* @throws IllegalStateException if the {@link #getMode()} is not equal to {@link RedisBungeeMode#CLUSTER}
* @since 0.8.0
*/
public JedisCluster requestClusterJedis() {
if (getMode() == RedisBungeeMode.CLUSTER) {
return ((JedisClusterSummoner) this.plugin.getSummoner()).obtainResource();
} else {
throw new IllegalStateException("Mode is not " + RedisBungeeMode.CLUSTER);
}
}
/**
* This gives you an instance of JedisPooled that can't be closed
* see {@link com.imaginarycode.minecraft.redisbungee.api.summoners.NotClosableJedisPooled}
*
* @return {@link redis.clients.jedis.JedisPooled}
* @throws IllegalStateException if the {@link #getMode()} is not equal to {@link RedisBungeeMode#SINGLE}
* @since 0.8.0
*/
public JedisPooled requestJedisPooled() {
if (getMode() == RedisBungeeMode.SINGLE) {
return ((JedisPooledSummoner) this.plugin.getSummoner()).obtainResource();
} else {
throw new IllegalStateException("Mode is not " + RedisBungeeMode.SINGLE);
}
}
/**
* returns Summoner class responsible for Single Jedis {@link redis.clients.jedis.JedisPooled} with {@link JedisPool}, Cluster Jedis {@link redis.clients.jedis.JedisCluster} handling
*
* @return {@link Summoner}
* @since 0.8.0
*/
public Summoner<?> getSummoner() {
return this.plugin.getSummoner();
}
/**
* shows what mode is RedisBungee is on
* Basically what every redis mode is used like cluster or single instance.
*
* @return {@link RedisBungeeMode}
* @since 0.8.0
*/
public RedisBungeeMode getMode() {
return this.plugin.getRedisBungeeMode();
}
public static AbstractRedisBungeeAPI getAbstractRedisBungeeAPI() {
return abstractRedisBungeeAPI;
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee;
public class Constants {
public final static String VERSION = "@version@";
public final static String GIT_COMMIT = "@git_commit@";
public static String getGithubCommitLink() {
return "https://github.com/ProxioDev/RedisBungee/commit/" + GIT_COMMIT;
}
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.google.common.net.InetAddresses;
import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerChangedServerNetworkEvent;
import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerLeftNetworkEvent;
import com.imaginarycode.minecraft.redisbungee.api.events.IPubSubMessageEvent;
import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisPipelineTask;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.json.JSONObject;
import redis.clients.jedis.ClusterPipeline;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
import redis.clients.jedis.UnifiedJedis;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public abstract class PlayerDataManager<P, LE, DE, PS extends IPubSubMessageEvent, SC extends IPlayerChangedServerNetworkEvent, NJE extends IPlayerLeftNetworkEvent, CE> {
protected final RedisBungeePlugin<P> plugin;
private final LoadingCache<UUID, String> serverCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(this::getServerFromRedis);
private final LoadingCache<UUID, String> lastServerCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(this::getLastServerFromRedis);
private final LoadingCache<UUID, String> proxyCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(this::getProxyFromRedis);
private final LoadingCache<UUID, InetAddress> ipCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(this::getIpAddressFromRedis);
private final Object SERVERS_TO_PLAYERS_KEY = new Object();
private final LoadingCache<Object, Multimap<String, UUID>> serverToPlayersCache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(this::serversToPlayersBuilder);
private final UnifiedJedis unifiedJedis;
private final String proxyId;
private final String networkId;
public PlayerDataManager(RedisBungeePlugin<P> plugin) {
this.plugin = plugin;
this.unifiedJedis = plugin.proxyDataManager().unifiedJedis();
this.proxyId = plugin.proxyDataManager().proxyId();
this.networkId = plugin.proxyDataManager().networkId();
}
// handle network wide
// server change
public abstract void onPlayerChangedServerNetworkEvent(SC event);
public abstract void onNetworkPlayerQuit(NJE event);
// local events
public abstract void onPubSubMessageEvent(PS event);
public abstract void onServerConnectedEvent(CE event);
public abstract void onLoginEvent(LE event);
public abstract void onDisconnectEvent(DE event);
protected void handleNetworkPlayerServerChange(IPlayerChangedServerNetworkEvent event) {
this.serverCache.invalidate(event.getUuid());
this.lastServerCache.invalidate(event.getUuid());
}
protected void handleNetworkPlayerQuit(IPlayerLeftNetworkEvent event) {
this.proxyCache.invalidate(event.getUuid());
this.serverCache.invalidate(event.getUuid());
this.ipCache.invalidate(event.getUuid());
}
protected void handlePubSubMessageEvent(IPubSubMessageEvent event) {
// kick api
if (event.getChannel().equals("redisbungee-kick")) {
JSONObject data = new JSONObject(event.getMessage());
String proxy = data.getString("proxy");
if (proxy.equals(this.proxyId)) {
return;
}
UUID uuid = UUID.fromString(data.getString("uuid"));
String message = data.getString("message");
plugin.handlePlatformKick(uuid, COMPONENT_SERIALIZER.deserialize(message));
return;
}
if (event.getChannel().equals("redisbungee-serverchange")) {
JSONObject data = new JSONObject(event.getMessage());
String proxy = data.getString("proxy");
if (proxy.equals(this.proxyId)) {
return;
}
UUID uuid = UUID.fromString(data.getString("uuid"));
String from = null;
if (data.has("from")) from = data.getString("from");
String to = data.getString("to");
plugin.fireEvent(plugin.createPlayerChangedServerNetworkEvent(uuid, from, to));
return;
}
if (event.getChannel().equals("redisbungee-player-join")) {
JSONObject data = new JSONObject(event.getMessage());
String proxy = data.getString("proxy");
if (proxy.equals(this.proxyId)) {
return;
}
UUID uuid = UUID.fromString(data.getString("uuid"));
plugin.fireEvent(plugin.createPlayerJoinedNetworkEvent(uuid));
return;
}
if (event.getChannel().equals("redisbungee-player-leave")) {
JSONObject data = new JSONObject(event.getMessage());
String proxy = data.getString("proxy");
if (proxy.equals(this.proxyId)) {
return;
}
UUID uuid = UUID.fromString(data.getString("uuid"));
plugin.fireEvent(plugin.createPlayerLeftNetworkEvent(uuid));
}
}
protected void playerChangedServer(UUID uuid, String from, String to) {
JSONObject data = new JSONObject();
data.put("proxy", this.proxyId);
data.put("uuid", uuid);
data.put("from", from);
data.put("to", to);
plugin.proxyDataManager().sendChannelMessage("redisbungee-serverchange", data.toString());
plugin.fireEvent(plugin.createPlayerChangedServerNetworkEvent(uuid, from, to));
handleServerChangeRedis(uuid, to);
}
private final JSONComponentSerializer COMPONENT_SERIALIZER =JSONComponentSerializer.json();
public void kickPlayer(UUID uuid, Component message) {
if (!plugin.handlePlatformKick(uuid, message)) { // handle locally before SENDING a message
JSONObject data = new JSONObject();
data.put("proxy", this.proxyId);
data.put("uuid", uuid);
data.put("message", COMPONENT_SERIALIZER.serialize(message));
plugin.proxyDataManager().sendChannelMessage("redisbungee-kick", data.toString());
}
}
private void handleServerChangeRedis(UUID uuid, String server) {
Map<String, String> data = new HashMap<>();
data.put("server", server);
data.put("last-server", server);
unifiedJedis.hset("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", data);
}
protected void addPlayer(final UUID uuid, final String name, final InetAddress inetAddress) {
Map<String, String> redisData = new HashMap<>();
redisData.put("last-online", String.valueOf(0));
redisData.put("proxy", this.proxyId);
redisData.put("ip", inetAddress.getHostAddress());
unifiedJedis.hset("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", redisData);
plugin.getUuidTranslator().persistInfo(name, uuid, this.unifiedJedis);
JSONObject data = new JSONObject();
data.put("proxy", this.proxyId);
data.put("uuid", uuid);
plugin.proxyDataManager().sendChannelMessage("redisbungee-player-join", data.toString());
plugin.fireEvent(plugin.createPlayerJoinedNetworkEvent(uuid));
this.plugin.proxyDataManager().addPlayer(uuid);
}
protected void removePlayer(UUID uuid) {
unifiedJedis.hset("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "last-online", String.valueOf(System.currentTimeMillis()));
unifiedJedis.hdel("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "server", "proxy", "ip");
JSONObject data = new JSONObject();
data.put("proxy", this.proxyId);
data.put("uuid", uuid);
plugin.proxyDataManager().sendChannelMessage("redisbungee-player-leave", data.toString());
plugin.fireEvent(plugin.createPlayerLeftNetworkEvent(uuid));
this.plugin.proxyDataManager().removePlayer(uuid);
}
protected String getProxyFromRedis(UUID uuid) {
return unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "proxy");
}
protected String getServerFromRedis(UUID uuid) {
return unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "server");
}
protected String getLastServerFromRedis(UUID uuid) {
return unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "last-server");
}
protected InetAddress getIpAddressFromRedis(UUID uuid) {
String ip = unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "ip");
if (ip == null) return null;
return InetAddresses.forString(ip);
}
protected long getLastOnlineFromRedis(UUID uuid) {
String unixString = unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "last-online");
if (unixString == null) return -1;
return Long.parseLong(unixString);
}
public String getLastServerFor(UUID uuid) {
return this.lastServerCache.get(uuid);
}
public String getServerFor(UUID uuid) {
return this.serverCache.get(uuid);
}
public String getProxyFor(UUID uuid) {
return this.proxyCache.get(uuid);
}
public InetAddress getIpFor(UUID uuid) {
return this.ipCache.get(uuid);
}
public long getLastOnline(UUID uuid) {
return getLastOnlineFromRedis(uuid);
}
public Multimap<String, UUID> serversToPlayers() {
return this.serverToPlayersCache.get(SERVERS_TO_PLAYERS_KEY);
}
protected Multimap<String, UUID> serversToPlayersBuilder(Object o) {
try {
return new RedisPipelineTask<Multimap<String, UUID>>(plugin) {
private final Set<UUID> uuids = plugin.proxyDataManager().networkPlayers();
private final ImmutableMultimap.Builder<String, UUID> builder = ImmutableMultimap.builder();
@Override
public Multimap<String, UUID> doPooledPipeline(Pipeline pipeline) {
HashMap<UUID, Response<String>> responses = new HashMap<>();
for (UUID uuid : uuids) {
responses.put(uuid, pipeline.hget("redis-bungee::" + networkId + "::player::" + uuid + "::data", "server"));
}
pipeline.sync();
responses.forEach((uuid, response) -> builder.put(response.get(), uuid));
return builder.build();
}
@Override
public Multimap<String, UUID> clusterPipeline(ClusterPipeline pipeline) {
HashMap<UUID, Response<String>> responses = new HashMap<>();
for (UUID uuid : uuids) {
responses.put(uuid, pipeline.hget("redis-bungee::" + networkId + "::player::" + uuid + "::data", "server"));
}
pipeline.sync();
responses.forEach((uuid, response) -> builder.put(response.get(), uuid));
return builder.build();
}
}.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,399 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload;
import com.imaginarycode.minecraft.redisbungee.api.payloads.gson.AbstractPayloadSerializer;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.DeathPayload;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.HeartbeatPayload;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.PubSubPayload;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.RunCommandPayload;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.DeathPayloadSerializer;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.HeartbeatPayloadSerializer;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.PubSubPayloadSerializer;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.RunCommandPayloadSerializer;
import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisPipelineTask;
import com.imaginarycode.minecraft.redisbungee.api.util.RedisUtil;
import redis.clients.jedis.*;
import redis.clients.jedis.params.XAddParams;
import redis.clients.jedis.params.XReadParams;
import redis.clients.jedis.resps.StreamEntry;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.common.base.Preconditions.checkArgument;
public abstract class ProxyDataManager implements Runnable {
private static final int MAX_ENTRIES = 10000;
private final AtomicBoolean closed = new AtomicBoolean(false);
private final UnifiedJedis unifiedJedis;
// data:
// Proxy id, heartbeat (unix epoch from instant), players as int
private final ConcurrentHashMap<String, HeartbeatPayload.HeartbeatData> heartbeats = new ConcurrentHashMap<>();
private final String networkId;
private final String proxyId;
private final String STREAM_ID;
// This different from proxy id, just to detect if there is duplicate proxy using same proxy id
private final UUID dataManagerUUID = UUID.randomUUID();
protected final RedisBungeePlugin<?> plugin;
private final Gson gson = new GsonBuilder().registerTypeAdapter(AbstractPayload.class, new AbstractPayloadSerializer()).registerTypeAdapter(HeartbeatPayload.class, new HeartbeatPayloadSerializer()).registerTypeAdapter(DeathPayload.class, new DeathPayloadSerializer()).registerTypeAdapter(PubSubPayload.class, new PubSubPayloadSerializer()).registerTypeAdapter(RunCommandPayload.class, new RunCommandPayloadSerializer()).create();
public ProxyDataManager(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
this.proxyId = this.plugin.configuration().getProxyId();
this.unifiedJedis = plugin.getSummoner().obtainResource();
this.destroyProxyMembers();
this.networkId = plugin.configuration().networkId();
this.STREAM_ID = "network-" + this.networkId + "-redisbungee-stream";
}
public abstract Set<UUID> getLocalOnlineUUIDs();
public Set<UUID> getPlayersOn(String proxyId) {
checkArgument(proxiesIds().contains(proxyId), proxyId + " is not a valid proxy ID");
if (proxyId.equals(this.proxyId)) return this.getLocalOnlineUUIDs();
if (!this.heartbeats.containsKey(proxyId)) {
return new HashSet<>(); // return empty hashset or null?
}
return getProxyMembers(proxyId);
}
public List<String> proxiesIds() {
return Collections.list(this.heartbeats.keys());
}
public synchronized void sendCommandTo(String proxyToRun, String command) {
if (isClosed()) return;
publishPayload(new RunCommandPayload(this.proxyId, proxyToRun, command));
}
public synchronized void sendChannelMessage(String channel, String message) {
if (isClosed()) return;
this.plugin.fireEvent(this.plugin.createPubSubEvent(channel, message));
publishPayload(new PubSubPayload(this.proxyId, channel, message));
}
// call every 1 second
public synchronized void publishHeartbeat() {
if (isClosed()) return;
HeartbeatPayload.HeartbeatData heartbeatData = new HeartbeatPayload.HeartbeatData(Instant.now().getEpochSecond(), this.getLocalOnlineUUIDs().size());
this.heartbeats.put(this.proxyId(), heartbeatData);
publishPayload(new HeartbeatPayload(this.proxyId, heartbeatData));
}
public Set<UUID> networkPlayers() {
try {
return new RedisPipelineTask<Set<UUID>>(this.plugin) {
@Override
public Set<UUID> doPooledPipeline(Pipeline pipeline) {
HashSet<Response<Set<String>>> responses = new HashSet<>();
for (String proxyId : proxiesIds()) {
responses.add(pipeline.smembers("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players"));
}
pipeline.sync();
HashSet<UUID> uuids = new HashSet<>();
for (Response<Set<String>> response : responses) {
for (String stringUUID : response.get()) {
uuids.add(UUID.fromString(stringUUID));
}
}
return uuids;
}
@Override
public Set<UUID> clusterPipeline(ClusterPipeline pipeline) {
HashSet<Response<Set<String>>> responses = new HashSet<>();
for (String proxyId : proxiesIds()) {
responses.add(pipeline.smembers("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players"));
}
pipeline.sync();
HashSet<UUID> uuids = new HashSet<>();
for (Response<Set<String>> response : responses) {
for (String stringUUID : response.get()) {
uuids.add(UUID.fromString(stringUUID));
}
}
return uuids;
}
}.call();
} catch (Exception e) {
throw new RuntimeException("unable to get network players", e);
}
}
public int totalNetworkPlayers() {
int players = 0;
for (HeartbeatPayload.HeartbeatData value : this.heartbeats.values()) {
players += value.players();
}
return players;
}
public Map<String, Integer> eachProxyCount() {
ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
heartbeats.forEach((proxy, data) -> builder.put(proxy, data.players()));
return builder.build();
}
// Call on close
private synchronized void publishDeath() {
publishPayload(new DeathPayload(this.proxyId));
}
private void publishPayload(AbstractPayload payload) {
Map<String, String> data = new HashMap<>();
data.put("payload", gson.toJson(payload));
data.put("data-manager-uuid", this.dataManagerUUID.toString());
data.put("class", payload.getClassName());
this.unifiedJedis.xadd(STREAM_ID, XAddParams.xAddParams().maxLen(MAX_ENTRIES).id(StreamEntryID.NEW_ENTRY), data);
}
private void handleHeartBeat(HeartbeatPayload payload) {
String id = payload.senderProxy();
if (!heartbeats.containsKey(id)) {
plugin.logInfo("Proxy {} has connected", id);
}
heartbeats.put(id, payload.data());
}
// call every 1 minutes
public void correctionTask() {
// let's check this proxy players
Set<UUID> localOnlineUUIDs = getLocalOnlineUUIDs();
Set<UUID> storedRedisUuids = getProxyMembers(this.proxyId);
if (!localOnlineUUIDs.equals(storedRedisUuids)) {
plugin.logWarn("De-synced playerS set detected correcting....");
Set<UUID> add = new HashSet<>(localOnlineUUIDs);
Set<UUID> remove = new HashSet<>(storedRedisUuids);
add.removeAll(storedRedisUuids);
remove.removeAll(localOnlineUUIDs);
for (UUID uuid : add) {
plugin.logWarn("found {} that isn't in the set, adding it to the Corrected set", uuid);
}
for (UUID uuid : remove) {
plugin.logWarn("found {} that does not belong to this proxy removing it from the corrected set", uuid);
}
try {
new RedisPipelineTask<Void>(plugin) {
@Override
public Void doPooledPipeline(Pipeline pipeline) {
Set<String> removeString = new HashSet<>();
for (UUID uuid : remove) {
removeString.add(uuid.toString());
}
Set<String> addString = new HashSet<>();
for (UUID uuid : add) {
addString.add(uuid.toString());
}
pipeline.srem("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", removeString.toArray(new String[]{}));
pipeline.sadd("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", addString.toArray(new String[]{}));
pipeline.sync();
return null;
}
@Override
public Void clusterPipeline(ClusterPipeline pipeline) {
Set<String> removeString = new HashSet<>();
for (UUID uuid : remove) {
removeString.add(uuid.toString());
}
Set<String> addString = new HashSet<>();
for (UUID uuid : add) {
addString.add(uuid.toString());
}
pipeline.srem("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", removeString.toArray(new String[]{}));
pipeline.sadd("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", addString.toArray(new String[]{}));
pipeline.sync();
return null;
}
}.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
plugin.logInfo("Player set has been corrected!");
}
// handle dead proxies "THAT" Didn't send death payload but considered dead due TIMEOUT ~30 seconds
final Set<String> deadProxies = new HashSet<>();
for (Map.Entry<String, HeartbeatPayload.HeartbeatData> stringHeartbeatDataEntry : this.heartbeats.entrySet()) {
String id = stringHeartbeatDataEntry.getKey();
long heartbeat = stringHeartbeatDataEntry.getValue().heartbeat();
if (Instant.now().getEpochSecond() - heartbeat > RedisUtil.PROXY_TIMEOUT) {
deadProxies.add(id);
cleanProxy(id);
}
}
try {
new RedisPipelineTask<Void>(plugin) {
@Override
public Void doPooledPipeline(Pipeline pipeline) {
for (String deadProxy : deadProxies) {
pipeline.del("redisbungee::" + networkId + "::proxies::" + deadProxy + "::online-players");
}
pipeline.sync();
return null;
}
@Override
public Void clusterPipeline(ClusterPipeline pipeline) {
for (String deadProxy : deadProxies) {
pipeline.del("redisbungee::" + networkId + "::proxies::" + deadProxy + "::online-players");
}
pipeline.sync();
return null;
}
}.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void handleProxyDeath(DeathPayload payload) {
cleanProxy(payload.senderProxy());
}
private void cleanProxy(String id) {
if (id.equals(this.proxyId())) {
return;
}
for (UUID uuid : getProxyMembers(id)) plugin.fireEvent(plugin.createPlayerLeftNetworkEvent(uuid));
this.heartbeats.remove(id);
plugin.logInfo("Proxy {} has disconnected", id);
}
private void handleChannelMessage(PubSubPayload payload) {
String channel = payload.channel();
String message = payload.message();
this.plugin.fireEvent(this.plugin.createPubSubEvent(channel, message));
}
protected abstract void handlePlatformCommandExecution(String command);
private void handleCommand(RunCommandPayload payload) {
String proxyToRun = payload.proxyToRun();
String command = payload.command();
if (proxyToRun.equals("allservers") || proxyToRun.equals(this.proxyId())) {
handlePlatformCommandExecution(command);
}
}
public void addPlayer(UUID uuid) {
this.unifiedJedis.sadd("redisbungee::" + this.networkId + "::proxies::" + this.proxyId + "::online-players", uuid.toString());
}
public void removePlayer(UUID uuid) {
this.unifiedJedis.srem("redisbungee::" + this.networkId + "::proxies::" + this.proxyId + "::online-players", uuid.toString());
}
private void destroyProxyMembers() {
unifiedJedis.del("redisbungee::" + this.networkId + "::proxies::" + this.proxyId + "::online-players");
}
private Set<UUID> getProxyMembers(String proxyId) {
Set<String> uuidsStrings = unifiedJedis.smembers("redisbungee::" + this.networkId + "::proxies::" + proxyId + "::online-players");
HashSet<UUID> uuids = new HashSet<>();
for (String proxyMember : uuidsStrings) {
uuids.add(UUID.fromString(proxyMember));
}
return uuids;
}
private StreamEntryID lastStreamEntryID;
// polling from stream
@Override
public void run() {
while (!isClosed()) {
try {
List<java.util.Map.Entry<String, List<StreamEntry>>> data = unifiedJedis.xread(XReadParams.xReadParams().block(0), Collections.singletonMap(STREAM_ID, lastStreamEntryID != null ? lastStreamEntryID : StreamEntryID.LAST_ENTRY));
for (Map.Entry<String, List<StreamEntry>> datum : data) {
for (StreamEntry streamEntry : datum.getValue()) {
this.lastStreamEntryID = streamEntry.getID();
String payloadData = streamEntry.getFields().get("payload");
String clazz = streamEntry.getFields().get("class");
UUID payloadDataManagerUUID = UUID.fromString(streamEntry.getFields().get("data-manager-uuid"));
AbstractPayload unknownPayload = (AbstractPayload) gson.fromJson(payloadData, Class.forName(clazz));
if (unknownPayload.senderProxy().equals(this.proxyId)) {
if (!payloadDataManagerUUID.equals(this.dataManagerUUID)) {
plugin.logWarn("detected other proxy is using same ID! {} this can cause issues, please shutdown this proxy and change the id!", this.proxyId);
}
continue;
}
if (unknownPayload instanceof HeartbeatPayload payload) {
handleHeartBeat(payload);
} else if (unknownPayload instanceof DeathPayload payload) {
handleProxyDeath(payload);
} else if (unknownPayload instanceof RunCommandPayload payload) {
handleCommand(payload);
} else if (unknownPayload instanceof PubSubPayload payload) {
handleChannelMessage(payload);
} else {
plugin.logWarn("got unknown data manager payload: {}", unknownPayload.getClassName());
}
}
}
} catch (Exception e) {
this.plugin.logFatal("an error has occurred in the stream", e);
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
}
}
}
public void close() {
closed.set(true);
this.publishDeath();
this.heartbeats.clear();
this.destroyProxyMembers();
}
public boolean isClosed() {
return closed.get();
}
public String proxyId() {
return proxyId;
}
public UnifiedJedis unifiedJedis() {
return unifiedJedis;
}
public String networkId() {
return networkId;
}
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api;
public enum RedisBungeeMode {
SINGLE, CLUSTER
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api;
import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI;
import com.imaginarycode.minecraft.redisbungee.api.config.LangConfiguration;
import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration;
import com.imaginarycode.minecraft.redisbungee.api.events.EventsPlatform;
import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner;
import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDTranslator;
import net.kyori.adventure.text.Component;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* This Class has all internal methods needed by every redis bungee plugin, and it can be used to implement another platforms than bungeecord or another forks of RedisBungee
* <p>
* Reason this is interface because some proxies implementations require the user to extend class for plugins for example bungeecord.
*
* @author Ham1255
* @since 0.7.0
*/
public interface RedisBungeePlugin<P> extends EventsPlatform {
default void initialize() {
}
default void stop() {
}
void logInfo(String msg);
void logInfo(String format, Object... object);
void logWarn(String msg);
void logWarn(String format, Object... object);
void logFatal(String msg);
void logFatal(String format, Throwable throwable);
RedisBungeeConfiguration configuration();
LangConfiguration langConfiguration();
Summoner<?> getSummoner();
RedisBungeeMode getRedisBungeeMode();
AbstractRedisBungeeAPI getAbstractRedisBungeeApi();
ProxyDataManager proxyDataManager();
PlayerDataManager<P, ?, ?, ?, ?, ?, ?> playerDataManager();
UUIDTranslator getUuidTranslator();
boolean isOnlineMode();
P getPlayer(UUID uuid);
P getPlayer(String name);
UUID getPlayerUUID(String player);
String getPlayerName(UUID player);
boolean handlePlatformKick(UUID uuid, Component message);
String getPlayerServerName(P player);
boolean isPlayerOnAServer(P player);
InetAddress getPlayerIp(P player);
void executeAsync(Runnable runnable);
void executeAsyncAfter(Runnable runnable, TimeUnit timeUnit, int time);
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.config;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* This language support implementation is temporarily
* until I come up with better system but for now we will use Maps instead :/
* Todo: possible usage of adventure api
*/
public class LangConfiguration {
private interface RegistrableMessages {
void register(String id, Locale locale, String miniMessage);
void test(Locale locale);
default void throwError(Locale locale, String where) {
throw new IllegalStateException("Language system in `" + where + "` found missing entries for " + locale.toString());
}
}
public static class Messages implements RegistrableMessages{
private final Map<Locale, Component> LOGGED_IN_FROM_OTHER_LOCATION;
private final Map<Locale, Component> ALREADY_LOGGED_IN;
private final Map<Locale, String> SERVER_CONNECTING;
private final Map<Locale, String> SERVER_NOT_FOUND;
private final Locale defaultLocale;
public Messages(Locale defaultLocale) {
LOGGED_IN_FROM_OTHER_LOCATION = new HashMap<>();
ALREADY_LOGGED_IN = new HashMap<>();
SERVER_CONNECTING = new HashMap<>();
SERVER_NOT_FOUND = new HashMap<>();
this.defaultLocale = defaultLocale;
}
public void register(String id, Locale locale, String miniMessage) {
switch (id) {
case "server-not-found" -> SERVER_NOT_FOUND.put(locale, miniMessage);
case "server-connecting" -> SERVER_CONNECTING.put(locale, miniMessage);
case "logged-in-other-location" -> LOGGED_IN_FROM_OTHER_LOCATION.put(locale, MiniMessage.miniMessage().deserialize(miniMessage));
case "already-logged-in" -> ALREADY_LOGGED_IN.put(locale, MiniMessage.miniMessage().deserialize(miniMessage));
}
}
public Component alreadyLoggedIn(Locale locale) {
if (ALREADY_LOGGED_IN.containsKey(locale)) return ALREADY_LOGGED_IN.get(locale);
return ALREADY_LOGGED_IN.get(defaultLocale);
}
// there is no way to know whats client locale during login so just default to use default locale MESSAGES.
public Component alreadyLoggedIn() {
return this.alreadyLoggedIn(this.defaultLocale);
}
public Component loggedInFromOtherLocation(Locale locale) {
if (LOGGED_IN_FROM_OTHER_LOCATION.containsKey(locale)) return LOGGED_IN_FROM_OTHER_LOCATION.get(locale);
return LOGGED_IN_FROM_OTHER_LOCATION.get(defaultLocale);
}
// there is no way to know what's client locale during login so just default to use default locale MESSAGES.
public Component loggedInFromOtherLocation() {
return this.loggedInFromOtherLocation(this.defaultLocale);
}
public Component serverConnecting(Locale locale, String server) {
String miniMessage;
if (SERVER_CONNECTING.containsKey(locale)) {
miniMessage = SERVER_CONNECTING.get(locale);
} else {
miniMessage = SERVER_CONNECTING.get(defaultLocale);
}
return MiniMessage.miniMessage().deserialize(miniMessage, Placeholder.parsed("server", server));
}
public Component serverConnecting(String server) {
return this.serverConnecting(this.defaultLocale, server);
}
public Component serverNotFound(Locale locale, String server) {
String miniMessage;
if (SERVER_NOT_FOUND.containsKey(locale)) {
miniMessage = SERVER_NOT_FOUND.get(locale);
} else {
miniMessage = SERVER_NOT_FOUND.get(defaultLocale);
}
return MiniMessage.miniMessage().deserialize(miniMessage, Placeholder.parsed("server", server));
}
public Component serverNotFound(String server) {
return this.serverNotFound(this.defaultLocale, server);
}
// tests locale if set CORRECTLY or just throw if not
public void test(Locale locale) {
if (!(LOGGED_IN_FROM_OTHER_LOCATION.containsKey(locale) && ALREADY_LOGGED_IN.containsKey(locale) && SERVER_CONNECTING.containsKey(locale) && SERVER_NOT_FOUND.containsKey(locale))) {
throwError(locale, "messages");
}
}
}
private final Component redisBungeePrefix;
private final Locale defaultLanguage;
private final boolean useClientLanguage;
private final Messages messages;
public LangConfiguration(Component redisBungeePrefix, Locale defaultLanguage, boolean useClientLanguage, Messages messages) {
this.redisBungeePrefix = redisBungeePrefix;
this.defaultLanguage = defaultLanguage;
this.useClientLanguage = useClientLanguage;
this.messages = messages;
}
public Component redisBungeePrefix() {
return redisBungeePrefix;
}
public Locale defaultLanguage() {
return defaultLanguage;
}
public boolean useClientLanguage() {
return useClientLanguage;
}
public Messages messages() {
return messages;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.config;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InetAddresses;
import javax.annotation.Nullable;
import java.net.InetAddress;
import java.util.List;
public class RedisBungeeConfiguration {
private final String proxyId;
private final List<InetAddress> exemptAddresses;
private final boolean kickWhenOnline;
private final boolean handleReconnectToLastServer;
private final boolean handleMotd;
private final CommandsConfiguration commandsConfiguration;
private final String networkId;
public RedisBungeeConfiguration(String networkId, String proxyId, List<String> exemptAddresses, boolean kickWhenOnline, boolean handleReconnectToLastServer, boolean handleMotd, CommandsConfiguration commandsConfiguration) {
this.proxyId = proxyId;
ImmutableList.Builder<InetAddress> addressBuilder = ImmutableList.builder();
for (String s : exemptAddresses) {
addressBuilder.add(InetAddresses.forString(s));
}
this.exemptAddresses = addressBuilder.build();
this.kickWhenOnline = kickWhenOnline;
this.handleReconnectToLastServer = handleReconnectToLastServer;
this.handleMotd = handleMotd;
this.commandsConfiguration = commandsConfiguration;
this.networkId = networkId;
}
public String getProxyId() {
return proxyId;
}
public List<InetAddress> getExemptAddresses() {
return exemptAddresses;
}
public boolean kickWhenOnline() {
return kickWhenOnline;
}
public boolean handleMotd() {
return this.handleMotd;
}
public boolean handleReconnectToLastServer() {
return this.handleReconnectToLastServer;
}
public record CommandsConfiguration(boolean redisbungeeEnabled, boolean redisbungeeLegacyEnabled,
@Nullable LegacySubCommandsConfiguration legacySubCommandsConfiguration) {
}
public record LegacySubCommandsConfiguration(boolean findEnabled, boolean glistEnabled, boolean ipEnabled,
boolean lastseenEnabled, boolean plistEnabled, boolean pproxyEnabled,
boolean sendtoallEnabled, boolean serveridEnabled,
boolean serveridsEnabled, boolean installFind, boolean installGlist, boolean installIp,
boolean installLastseen, boolean installPlist, boolean installPproxy,
boolean installSendtoall, boolean installServerid,
boolean installServerids) {
}
public CommandsConfiguration commandsConfiguration() {
return commandsConfiguration;
}
public String networkId() {
return networkId;
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.config.loaders;
import com.google.common.reflect.TypeToken;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration;
import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisClusterSummoner;
import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisPooledSummoner;
import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.objectmapping.ObjectMappingException;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.*;
import redis.clients.jedis.providers.ClusterConnectionProvider;
import redis.clients.jedis.providers.PooledConnectionProvider;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
public interface ConfigLoader extends GenericConfigLoader {
int CONFIG_VERSION = 2;
default void loadConfig(RedisBungeePlugin<?> plugin, Path dataFolder) throws IOException {
Path configFile = createConfigFile(dataFolder, "config.yml", "config.yml");
final YAMLConfigurationLoader yamlConfigurationFileLoader = YAMLConfigurationLoader.builder().setPath(configFile).build();
ConfigurationNode node = yamlConfigurationFileLoader.load();
if (node.getNode("config-version").getInt(0) != CONFIG_VERSION) {
handleOldConfig(dataFolder, "config.yml", "config.yml");
node = yamlConfigurationFileLoader.load();
}
final boolean useSSL = node.getNode("useSSL").getBoolean(false);
final boolean kickWhenOnline = node.getNode("kick-when-online").getBoolean(true);
String redisPassword = node.getNode("redis-password").getString("");
String redisUsername = node.getNode("redis-username").getString("");
String networkId = node.getNode("network-id").getString("main");
String proxyId = node.getNode("proxy-id").getString("proxy-1");
final int maxConnections = node.getNode("max-redis-connections").getInt(10);
List<String> exemptAddresses;
try {
exemptAddresses = node.getNode("exempt-ip-addresses").getList(TypeToken.of(String.class));
} catch (ObjectMappingException e) {
exemptAddresses = Collections.emptyList();
}
// check redis password
if ((redisPassword.isEmpty() || redisPassword.equals("none"))) {
redisPassword = null;
plugin.logWarn("password is empty");
}
if ((redisUsername.isEmpty() || redisUsername.equals("none"))) {
redisUsername = null;
}
// env var
String proxyIdFromEnv = System.getenv("REDISBUNGEE_PROXY_ID");
if (proxyIdFromEnv != null) {
plugin.logInfo("Overriding current configured proxy id {} and been set to {} by Environment variable REDISBUNGEE_PROXY_ID", proxyId, proxyIdFromEnv);
proxyId = proxyIdFromEnv;
}
String networkIdFromEnv = System.getenv("REDISBUNGEE_NETWORK_ID");
if (networkIdFromEnv != null) {
plugin.logInfo("Overriding current configured network id {} and been set to {} by Environment variable REDISBUNGEE_NETWORK_ID", networkId, networkIdFromEnv);
networkId = networkIdFromEnv;
}
// Configuration sanity checks.
if (proxyId == null || proxyId.isEmpty()) {
String genId = UUID.randomUUID().toString();
plugin.logInfo("Generated proxy id " + genId + " and saving it to config.");
node.getNode("proxy-id").setValue(genId);
yamlConfigurationFileLoader.save(node);
proxyId = genId;
plugin.logInfo("proxy id was generated: " + proxyId);
} else {
plugin.logInfo("Loaded proxy id " + proxyId);
}
if (networkId.isEmpty()) {
networkId = "main";
plugin.logWarn("network id was empty and replaced with 'main'");
}
plugin.logInfo("Loaded network id " + networkId);
boolean reconnectToLastServer = node.getNode("reconnect-to-last-server").getBoolean();
boolean handleMotd = node.getNode("handle-motd").getBoolean(true);
plugin.logInfo("handle reconnect to last server: {}", reconnectToLastServer);
plugin.logInfo("handle motd: {}", handleMotd);
// commands
boolean redisBungeeEnabled = node.getNode("commands", "redisbungee", "enabled").getBoolean(true);
boolean redisBungeeLegacyEnabled =node.getNode("commands", "redisbungee-legacy", "enabled").getBoolean(false);
boolean glistEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "glist", "enabled").getBoolean(false);
boolean findEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "find", "enabled").getBoolean(false);
boolean lastseenEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "lastseen", "enabled").getBoolean(false);
boolean ipEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "ip", "enabled").getBoolean(false);
boolean pproxyEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "pproxy", "enabled").getBoolean(false);
boolean sendToAllEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "sendtoall", "enabled").getBoolean(false);
boolean serverIdEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverid", "enabled").getBoolean(false);
boolean serverIdsEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverids", "enabled").getBoolean(false);
boolean pListEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "plist", "enabled").getBoolean(false);
boolean installGlist = node.getNode("commands", "redisbungee-legacy", "subcommands", "glist", "install").getBoolean(false);
boolean installFind = node.getNode("commands", "redisbungee-legacy", "subcommands", "find", "install").getBoolean(false);
boolean installLastseen = node.getNode("commands", "redisbungee-legacy", "subcommands", "lastseen", "install").getBoolean(false);
boolean installIp = node.getNode("commands", "redisbungee-legacy", "subcommands", "ip", "install").getBoolean(false);
boolean installPproxy = node.getNode("commands", "redisbungee-legacy", "subcommands", "pproxy", "install").getBoolean(false);
boolean installSendToAll = node.getNode("commands", "redisbungee-legacy", "subcommands", "sendtoall", "install").getBoolean(false);
boolean installServerid = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverid", "install").getBoolean(false);
boolean installServerIds = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverids", "install").getBoolean(false);
boolean installPlist = node.getNode("commands", "redisbungee-legacy", "subcommands", "plist", "install").getBoolean(false);
RedisBungeeConfiguration configuration = new RedisBungeeConfiguration(networkId, proxyId, exemptAddresses, kickWhenOnline, reconnectToLastServer, handleMotd, new RedisBungeeConfiguration.CommandsConfiguration(
redisBungeeEnabled, redisBungeeLegacyEnabled,
new RedisBungeeConfiguration.LegacySubCommandsConfiguration(
findEnabled, glistEnabled, ipEnabled,
lastseenEnabled, pListEnabled, pproxyEnabled,
sendToAllEnabled, serverIdEnabled, serverIdsEnabled,
installFind, installGlist, installIp,
installLastseen, installPlist, installPproxy,
installSendToAll, installServerid, installServerIds)
));
Summoner<?> summoner;
RedisBungeeMode redisBungeeMode;
if (useSSL) {
plugin.logInfo("Using ssl");
}
if (node.getNode("cluster-mode-enabled").getBoolean(false)) {
plugin.logInfo("RedisBungee MODE: CLUSTER");
Set<HostAndPort> hostAndPortSet = new HashSet<>();
GenericObjectPoolConfig<Connection> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(maxConnections);
poolConfig.setBlockWhenExhausted(true);
node.getNode("redis-cluster-servers").getChildrenList().forEach((childNode) -> {
Map<Object, ? extends ConfigurationNode> hostAndPort = childNode.getChildrenMap();
String host = hostAndPort.get("host").getString();
int port = hostAndPort.get("port").getInt();
hostAndPortSet.add(new HostAndPort(host, port));
});
plugin.logInfo(hostAndPortSet.size() + " cluster nodes were specified");
if (hostAndPortSet.isEmpty()) {
throw new RuntimeException("No redis cluster servers specified");
}
summoner = new JedisClusterSummoner(new ClusterConnectionProvider(hostAndPortSet, DefaultJedisClientConfig.builder().user(redisUsername).password(redisPassword).ssl(useSSL).socketTimeoutMillis(5000).timeoutMillis(10000).build(), poolConfig));
redisBungeeMode = RedisBungeeMode.CLUSTER;
} else {
plugin.logInfo("RedisBungee MODE: SINGLE");
final String redisServer = node.getNode("redis-server").getString("127.0.0.1");
final int redisPort = node.getNode("redis-port").getInt(6379);
if (redisServer != null && redisServer.isEmpty()) {
throw new RuntimeException("No redis server specified");
}
JedisPool jedisPool = null;
if (node.getNode("enable-jedis-pool-compatibility").getBoolean(false)) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(node.getNode("compatibility-max-connections").getInt(3));
config.setBlockWhenExhausted(true);
jedisPool = new JedisPool(config, redisServer, redisPort, 5000, redisUsername, redisPassword, useSSL);
plugin.logInfo("Compatibility JedisPool was created");
}
GenericObjectPoolConfig<Connection> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(maxConnections);
poolConfig.setBlockWhenExhausted(true);
summoner = new JedisPooledSummoner(new PooledConnectionProvider(new ConnectionFactory(new HostAndPort(redisServer, redisPort), DefaultJedisClientConfig.builder().user(redisUsername).timeoutMillis(5000).ssl(useSSL).password(redisPassword).build()), poolConfig), jedisPool);
redisBungeeMode = RedisBungeeMode.SINGLE;
}
plugin.logInfo("Successfully connected to Redis.");
onConfigLoad(configuration, summoner, redisBungeeMode);
}
void onConfigLoad(RedisBungeeConfiguration configuration, Summoner<?> summoner, RedisBungeeMode mode);
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.config.loaders;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
public interface GenericConfigLoader {
// CHANGES on every reboot
String RANDOM_OLD = "backup-" + Instant.now().getEpochSecond();
default Path createConfigFile(Path dataFolder, String configFile, @Nullable String defaultResourceID) throws IOException {
if (Files.notExists(dataFolder)) {
Files.createDirectory(dataFolder);
}
Path file = dataFolder.resolve(configFile);
if (Files.notExists(file) && defaultResourceID != null) {
try (InputStream in = getClass().getClassLoader().getResourceAsStream(defaultResourceID)) {
Files.createFile(file);
assert in != null;
Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
}
}
return file;
}
default void handleOldConfig(Path dataFolder, String configFile, @Nullable String defaultResourceID) throws IOException {
Path oldConfigFolder = dataFolder.resolve("old_config");
if (Files.notExists(oldConfigFolder)) {
Files.createDirectory(oldConfigFolder);
}
Path randomStoreConfigDirectory = oldConfigFolder.resolve(RANDOM_OLD);
if (Files.notExists(randomStoreConfigDirectory)) {
Files.createDirectory(randomStoreConfigDirectory);
}
Path oldConfigPath = dataFolder.resolve(configFile);
Files.move(oldConfigPath, randomStoreConfigDirectory.resolve(configFile));
createConfigFile(dataFolder, configFile, defaultResourceID);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.config.loaders;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.config.LangConfiguration;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Locale;
public interface LangConfigLoader extends GenericConfigLoader {
int CONFIG_VERSION = 1;
default void loadLangConfig(RedisBungeePlugin<?> plugin, Path dataFolder) throws IOException {
Path configFile = createConfigFile(dataFolder, "lang.yml", "lang.yml");
final YAMLConfigurationLoader yamlConfigurationFileLoader = YAMLConfigurationLoader.builder().setPath(configFile).build();
ConfigurationNode node = yamlConfigurationFileLoader.load();
if (node.getNode("config-version").getInt(0) != CONFIG_VERSION) {
handleOldConfig(dataFolder, "lang.yml", "lang.yml");
node = yamlConfigurationFileLoader.load();
}
// MINI message serializer
MiniMessage miniMessage = MiniMessage.miniMessage();
Component prefix = miniMessage.deserialize(node.getNode("prefix").getString("<color:red>[<color:yellow>Redis<color:red>Bungee]"));
Locale defaultLocale = Locale.forLanguageTag(node.getNode("default-locale").getString("en-us"));
boolean useClientLocale = node.getNode("use-client-locale").getBoolean(true);
LangConfiguration.Messages messages = new LangConfiguration.Messages(defaultLocale);
node.getNode("messages").getChildrenMap().forEach((key, childNode) -> childNode.getChildrenMap().forEach((childKey, childChildNode) -> {
messages.register(key.toString(), Locale.forLanguageTag(childKey.toString()), childChildNode.getString());
}));
messages.test(defaultLocale);
onLangConfigLoad(new LangConfiguration(prefix, defaultLocale, useClientLocale, messages));
}
void onLangConfigLoad(LangConfiguration langConfiguration);
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.events;
import java.util.UUID;
/**
* Since each platform have their own events' implementation for example Bungeecord events extends Event while velocity don't
*
* @author Ham1255
* @since 0.7.0
*/
public interface EventsPlatform {
IPlayerChangedServerNetworkEvent createPlayerChangedServerNetworkEvent(UUID uuid, String previousServer, String server);
IPlayerJoinedNetworkEvent createPlayerJoinedNetworkEvent(UUID uuid);
IPlayerLeftNetworkEvent createPlayerLeftNetworkEvent(UUID uuid);
IPubSubMessageEvent createPubSubEvent(String channel, String message);
void fireEvent(Object event);
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.events;
import java.util.UUID;
public interface IPlayerChangedServerNetworkEvent extends RedisBungeeEvent {
UUID getUuid();
String getServer();
String getPreviousServer();
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.events;
import java.util.UUID;
public interface IPlayerJoinedNetworkEvent extends RedisBungeeEvent {
UUID getUuid();
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.events;
import java.util.UUID;
public interface IPlayerLeftNetworkEvent extends RedisBungeeEvent {
UUID getUuid();
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.events;
public interface IPubSubMessageEvent extends RedisBungeeEvent {
String getChannel();
String getMessage();
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.events;
interface RedisBungeeEvent {
}

View File

@@ -0,0 +1,24 @@
package com.imaginarycode.minecraft.redisbungee.api.payloads;
public abstract class AbstractPayload {
private final String senderProxy;
public AbstractPayload(String proxyId) {
this.senderProxy = proxyId;
}
public AbstractPayload(String senderProxy, String className) {
this.senderProxy = senderProxy;
}
public String senderProxy() {
return senderProxy;
}
public String getClassName() {
return getClass().getName();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.gson;
import com.google.gson.*;
import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload;
import java.lang.reflect.Type;
public class AbstractPayloadSerializer implements JsonSerializer<AbstractPayload>, JsonDeserializer<AbstractPayload> {
@Override
public AbstractPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
return new AbstractPayload(jsonObject.get("proxy").getAsString(), jsonObject.get("class").getAsString()) {
};
}
@Override
public JsonElement serialize(AbstractPayload src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.add("proxy", new JsonPrimitive(src.senderProxy()));
return jsonObject;
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy;
import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload;
public class DeathPayload extends AbstractPayload {
public DeathPayload(String proxyId) {
super(proxyId);
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy;
import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload;
public class HeartbeatPayload extends AbstractPayload {
public record HeartbeatData(long heartbeat, int players) {
}
private final HeartbeatData data;
public HeartbeatPayload(String proxyId, HeartbeatData data) {
super(proxyId);
this.data = data;
}
public HeartbeatData data() {
return data;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy;
import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload;
public class PubSubPayload extends AbstractPayload {
private final String channel;
private final String message;
public PubSubPayload(String proxyId, String channel, String message) {
super(proxyId);
this.channel = channel;
this.message = message;
}
public String channel() {
return channel;
}
public String message() {
return message;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy;
import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload;
public class RunCommandPayload extends AbstractPayload {
private final String proxyToRun;
private final String command;
public RunCommandPayload(String proxyId, String proxyToRun, String command) {
super(proxyId);
this.proxyToRun = proxyToRun;
this.command = command;
}
public String proxyToRun() {
return proxyToRun;
}
public String command() {
return command;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson;
import com.google.gson.*;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.DeathPayload;
import java.lang.reflect.Type;
public class DeathPayloadSerializer implements JsonSerializer<DeathPayload>, JsonDeserializer<DeathPayload> {
private static final Gson gson = new Gson();
@Override
public DeathPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String senderProxy = jsonObject.get("proxy").getAsString();
return new DeathPayload(senderProxy);
}
@Override
public JsonElement serialize(DeathPayload src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.add("proxy", new JsonPrimitive(src.senderProxy()));
return jsonObject;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson;
import com.google.gson.*;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.HeartbeatPayload;
import java.lang.reflect.Type;
public class HeartbeatPayloadSerializer implements JsonSerializer<HeartbeatPayload>, JsonDeserializer<HeartbeatPayload> {
@Override
public HeartbeatPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String senderProxy = jsonObject.get("proxy").getAsString();
long heartbeat = jsonObject.get("heartbeat").getAsLong();
int players = jsonObject.get("players").getAsInt();
return new HeartbeatPayload(senderProxy, new HeartbeatPayload.HeartbeatData(heartbeat, players));
}
@Override
public JsonElement serialize(HeartbeatPayload src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.add("proxy", new JsonPrimitive(src.senderProxy()));
jsonObject.add("heartbeat", new JsonPrimitive(src.data().heartbeat()));
jsonObject.add("players", new JsonPrimitive(src.data().players()));
return jsonObject;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson;
import com.google.gson.*;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.PubSubPayload;
import java.lang.reflect.Type;
public class PubSubPayloadSerializer implements JsonSerializer<PubSubPayload>, JsonDeserializer<PubSubPayload> {
private static final Gson gson = new Gson();
@Override
public PubSubPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String senderProxy = jsonObject.get("proxy").getAsString();
String channel = jsonObject.get("channel").getAsString();
String message = jsonObject.get("message").getAsString();
return new PubSubPayload(senderProxy, channel, message);
}
@Override
public JsonElement serialize(PubSubPayload src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.add("proxy", new JsonPrimitive(src.senderProxy()));
jsonObject.add("channel", new JsonPrimitive(src.channel()));
jsonObject.add("message", context.serialize(src.message()));
return jsonObject;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson;
import com.google.gson.*;
import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.RunCommandPayload;
import java.lang.reflect.Type;
public class RunCommandPayloadSerializer implements JsonSerializer<RunCommandPayload>, JsonDeserializer<RunCommandPayload> {
@Override
public RunCommandPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String senderProxy = jsonObject.get("proxy").getAsString();
String proxyToRun = jsonObject.get("proxy-to-run").getAsString();
String command = jsonObject.get("command").getAsString();
return new RunCommandPayload(senderProxy, proxyToRun, command);
}
@Override
public JsonElement serialize(RunCommandPayload src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.add("proxy", new JsonPrimitive(src.senderProxy()));
jsonObject.add("proxy-to-run", new JsonPrimitive(src.proxyToRun()));
jsonObject.add("command", context.serialize(src.command()));
return jsonObject;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.summoners;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.providers.ClusterConnectionProvider;
import java.io.IOException;
import java.time.Duration;
public class JedisClusterSummoner implements Summoner<JedisCluster> {
private final ClusterConnectionProvider clusterConnectionProvider;
public JedisClusterSummoner(ClusterConnectionProvider clusterConnectionProvider) {
this.clusterConnectionProvider = clusterConnectionProvider;
// test the connection
JedisCluster jedisCluster = obtainResource();
jedisCluster.set("random_data", "0");
jedisCluster.del("random_data");
}
@Override
public void close() throws IOException {
this.clusterConnectionProvider.close();
}
@Override
public JedisCluster obtainResource() {
return new NotClosableJedisCluster(this.clusterConnectionProvider, 60, Duration.ofSeconds(10));
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.summoners;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.providers.PooledConnectionProvider;
import java.io.IOException;
public class JedisPooledSummoner implements Summoner<JedisPooled> {
private final PooledConnectionProvider connectionProvider;
private final JedisPool jedisPool;
public JedisPooledSummoner(PooledConnectionProvider connectionProvider, JedisPool jedisPool) {
this.connectionProvider = connectionProvider;
this.jedisPool = jedisPool;
// test connections
if (jedisPool != null) {
try (Jedis jedis = this.jedisPool.getResource()) {
// Test the connection to make sure configuration is right
jedis.ping();
}
}
final JedisPooled jedisPooled = this.obtainResource();
jedisPooled.set("random_data", "0");
jedisPooled.del("random_data");
}
@Override
public JedisPooled obtainResource() {
// create UnClosable JedisPool *disposable*
return new NotClosableJedisPooled(this.connectionProvider);
}
public JedisPool getCompatibilityJedisPool() {
return this.jedisPool;
}
@Override
public void close() throws IOException {
if (this.jedisPool != null) {
this.jedisPool.close();
}
this.connectionProvider.close();
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.summoners;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.providers.ClusterConnectionProvider;
import java.time.Duration;
public class NotClosableJedisCluster extends JedisCluster {
NotClosableJedisCluster(ClusterConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration) {
super(provider, maxAttempts, maxTotalRetriesDuration);
}
@Override
public void close() {
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.summoners;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.providers.PooledConnectionProvider;
public class NotClosableJedisPooled extends JedisPooled {
NotClosableJedisPooled(PooledConnectionProvider provider) {
super(provider);
}
@Override
public void close() {
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.summoners;
import redis.clients.jedis.UnifiedJedis;
import java.io.Closeable;
/**
* This class intended for future release to support redis sentinel or redis clusters
*
* @author Ham1255
* @since 0.7.0
*/
public interface Summoner<P extends UnifiedJedis> extends Closeable {
P obtainResource();
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.tasks;
import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import redis.clients.jedis.*;
public abstract class RedisPipelineTask<T> extends RedisTask<T> {
public RedisPipelineTask(AbstractRedisBungeeAPI api) {
super(api);
}
public RedisPipelineTask(RedisBungeePlugin<?> plugin) {
super(plugin);
}
@Override
public T unifiedJedisTask(UnifiedJedis unifiedJedis) {
if (unifiedJedis instanceof JedisPooled pooled) {
try (Pipeline pipeline = pooled.pipelined()) {
return doPooledPipeline(pipeline);
}
} else if (unifiedJedis instanceof JedisCluster jedisCluster) {
try (ClusterPipeline pipeline = jedisCluster.pipelined()) {
return clusterPipeline(pipeline);
}
}
return null;
}
public abstract T doPooledPipeline(Pipeline pipeline);
public abstract T clusterPipeline(ClusterPipeline pipeline);
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.tasks;
import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisClusterSummoner;
import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisPooledSummoner;
import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner;
import redis.clients.jedis.UnifiedJedis;
import java.util.concurrent.Callable;
/**
* Since Jedis now have UnifiedJedis which basically extended by cluster / single connections classes
* can help us to have shared code.
*/
public abstract class RedisTask<V> implements Runnable, Callable<V> {
protected final Summoner<?> summoner;
protected final RedisBungeeMode mode;
@Override
public V call() throws Exception {
return this.execute();
}
public RedisTask(AbstractRedisBungeeAPI api) {
this.summoner = api.getSummoner();
this.mode = api.getMode();
}
public RedisTask(RedisBungeePlugin<?> plugin) {
this.summoner = plugin.getSummoner();
this.mode = plugin.getRedisBungeeMode();
}
public abstract V unifiedJedisTask(UnifiedJedis unifiedJedis);
@Override
public void run() {
this.execute();
}
public V execute() {
// JedisCluster, JedisPooled in fact is just UnifiedJedis does not need new instance since its single instance anyway.
if (mode == RedisBungeeMode.SINGLE) {
JedisPooledSummoner jedisSummoner = (JedisPooledSummoner) summoner;
return this.unifiedJedisTask(jedisSummoner.obtainResource());
} else if (mode == RedisBungeeMode.CLUSTER) {
JedisClusterSummoner jedisClusterSummoner = (JedisClusterSummoner) summoner;
return this.unifiedJedisTask(jedisClusterSummoner.obtainResource());
}
return null;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.tasks;
import com.google.gson.Gson;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.util.uuid.CachedUUIDEntry;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.exceptions.JedisException;
import java.util.ArrayList;
public class UUIDCleanupTask extends RedisTask<Void>{
private final Gson gson = new Gson();
private final RedisBungeePlugin<?> plugin;
public UUIDCleanupTask(RedisBungeePlugin<?> plugin) {
super(plugin);
this.plugin = plugin;
}
// this code is inspired from https://github.com/minecrafter/redisbungeeclean
@Override
public Void unifiedJedisTask(UnifiedJedis unifiedJedis) {
try {
final long number = unifiedJedis.hlen("uuid-cache");
plugin.logInfo("Found {} entries", number);
ArrayList<String> fieldsToRemove = new ArrayList<>();
unifiedJedis.hgetAll("uuid-cache").forEach((field, data) -> {
CachedUUIDEntry cachedUUIDEntry = gson.fromJson(data, CachedUUIDEntry.class);
if (cachedUUIDEntry.expired()) {
fieldsToRemove.add(field);
}
});
if (!fieldsToRemove.isEmpty()) {
unifiedJedis.hdel("uuid-cache", fieldsToRemove.toArray(new String[0]));
}
plugin.logInfo("deleted {} entries", fieldsToRemove.size());
} catch (JedisException e) {
plugin.logFatal("There was an error fetching information", e);
}
return null;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.util;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.UnifiedJedis;
public class InitialUtils {
public static void checkRedisVersion(RedisBungeePlugin<?> plugin) {
new RedisTask<Void>(plugin) {
@Override
public Void unifiedJedisTask(UnifiedJedis unifiedJedis) {
// This is more portable than INFO <section>
String info = new String((byte[]) unifiedJedis.sendCommand(Protocol.Command.INFO));
for (String s : info.split("\r\n")) {
if (s.startsWith("redis_version:")) {
String version = s.split(":")[1];
plugin.logInfo("Redis server version: " + version);
if (!RedisUtil.isRedisVersionRight(version)) {
plugin.logFatal("Your version of Redis (" + version + ") is not at least version " + RedisUtil.MAJOR_VERSION + "." + RedisUtil.MINOR_VERSION + " RedisBungee requires a newer version of Redis.");
throw new RuntimeException("Unsupported Redis version detected");
}
long uuidCacheSize = unifiedJedis.hlen("uuid-cache");
if (uuidCacheSize > 750000) {
plugin.logInfo("Looks like you have a really big UUID cache! Run '/rb clean' to remove expired cache entries");
}
break;
}
}
return null;
}
}.execute();
}
}

View File

@@ -0,0 +1,31 @@
package com.imaginarycode.minecraft.redisbungee.api.util;
import com.google.common.annotations.VisibleForTesting;
@VisibleForTesting
public class RedisUtil {
public final static int PROXY_TIMEOUT = 30;
public static final int MAJOR_VERSION = 6;
public static final int MINOR_VERSION = 2;
public static boolean isRedisVersionRight(String redisVersion) {
String[] args = redisVersion.split("\\.");
if (args.length < 2) {
return false;
}
int major = Integer.parseInt(args[0]);
int minor = Integer.parseInt(args[1]);
if (major > MAJOR_VERSION) return true;
return major == MAJOR_VERSION && minor >= MINOR_VERSION;
}
// Ham1255: i am keeping this if some plugin uses this *IF*
@Deprecated
public static boolean canUseLua(String redisVersion) {
// Need to use >=3 to use Lua optimizations.
return isRedisVersionRight(redisVersion);
}
}

View File

@@ -0,0 +1,40 @@
package com.imaginarycode.minecraft.redisbungee.api.util.serialize;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.io.ByteArrayDataOutput;
import java.util.Collection;
import java.util.Map;
public class MultiMapSerialization {
public static void serializeMultiset(Multiset<String> collection, ByteArrayDataOutput output) {
output.writeInt(collection.elementSet().size());
for (Multiset.Entry<String> entry : collection.entrySet()) {
output.writeUTF(entry.getElement());
output.writeInt(entry.getCount());
}
}
@SuppressWarnings("SameParameterValue")
public static void serializeMultimap(Multimap<String, String> collection, boolean includeNames, ByteArrayDataOutput output) {
output.writeInt(collection.keySet().size());
for (Map.Entry<String, Collection<String>> entry : collection.asMap().entrySet()) {
output.writeUTF(entry.getKey());
if (includeNames) {
serializeCollection(entry.getValue(), output);
} else {
output.writeInt(entry.getValue().size());
}
}
}
public static void serializeCollection(Collection<?> collection, ByteArrayDataOutput output) {
output.writeInt(collection.size());
for (Object o : collection) {
output.writeUTF(o.toString());
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.util.uuid;
import java.util.Calendar;
import java.util.UUID;
public class CachedUUIDEntry {
private final String name;
private final UUID uuid;
private final Calendar expiry;
public CachedUUIDEntry(String name, UUID uuid, Calendar expiry) {
this.name = name;
this.uuid = uuid;
this.expiry = expiry;
}
public String getName() {
return name;
}
public UUID getUuid() {
return uuid;
}
public Calendar getExpiry() {
return expiry;
}
public boolean expired() {
return Calendar.getInstance().after(expiry);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.util.uuid;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class NameFetcher {
private static OkHttpClient httpClient;
private static final Gson gson = new Gson();
public static void setHttpClient(OkHttpClient httpClient) {
NameFetcher.httpClient = httpClient;
}
public static List<String> nameHistoryFromUuid(UUID uuid) throws IOException {
String name = getName(uuid);
if (name == null) return Collections.emptyList();
return Collections.singletonList(name);
}
public static String getName(UUID uuid) throws IOException {
String url = "https://playerdb.co/api/player/minecraft/" + uuid.toString();
Request request = new Request.Builder()
.addHeader("User-Agent", "RedisBungee-ProxioDev")
.url(url)
.get()
.build();
ResponseBody body = httpClient.newCall(request).execute().body();
String response = body.string();
body.close();
JsonObject json = gson.fromJson(response, JsonObject.class);
if (!json.has("success") || !json.get("success").getAsBoolean()) return null;
if (!json.has("data")) return null;
JsonObject data = json.getAsJsonObject("data");
if (!data.has("player")) return null;
JsonObject player = data.getAsJsonObject("player");
if (!player.has("username")) return null;
return player.get("username").getAsString();
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.util.uuid;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.squareup.okhttp.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
/* Credits to evilmidget38 for this class. I modified it to use Gson. */
public class UUIDFetcher implements Callable<Map<String, UUID>> {
private static final double PROFILES_PER_REQUEST = 100;
private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft";
private static final MediaType JSON = MediaType.parse("application/json");
private final List<String> names;
private final boolean rateLimiting;
private static final Gson gson = new Gson();
public static void setHttpClient(OkHttpClient httpClient) {
UUIDFetcher.httpClient = httpClient;
}
private static OkHttpClient httpClient;
private UUIDFetcher(List<String> names, boolean rateLimiting) {
this.names = ImmutableList.copyOf(names);
this.rateLimiting = rateLimiting;
}
public UUIDFetcher(List<String> names) {
this(names, true);
}
public static UUID getUUID(String id) {
return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32));
}
public Map<String, UUID> call() throws Exception {
Map<String, UUID> uuidMap = new HashMap<>();
int requests = (int) Math.ceil(names.size() / PROFILES_PER_REQUEST);
for (int i = 0; i < requests; i++) {
String body = gson.toJson(names.subList(i * 100, Math.min((i + 1) * 100, names.size())));
Request request = new Request.Builder().url(PROFILE_URL).post(RequestBody.create(JSON, body)).build();
ResponseBody responseBody = httpClient.newCall(request).execute().body();
String response = responseBody.string();
responseBody.close();
Profile[] array = gson.fromJson(response, Profile[].class);
for (Profile profile : array) {
UUID uuid = UUIDFetcher.getUUID(profile.id);
uuidMap.put(profile.name, uuid);
}
if (rateLimiting && i != requests - 1) {
Thread.sleep(100L);
}
}
return uuidMap;
}
private static class Profile {
String id;
String name;
}
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright (c) 2013-present RedisBungee contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.imaginarycode.minecraft.redisbungee.api.util.uuid;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin;
import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask;
import org.checkerframework.checker.nullness.qual.NonNull;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.exceptions.JedisException;
import java.util.Calendar;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public final class UUIDTranslator {
private static final Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}");
private static final Pattern MOJANGIAN_UUID_PATTERN = Pattern.compile("[a-fA-F0-9]{32}");
private final RedisBungeePlugin<?> plugin;
private final Map<String, CachedUUIDEntry> nameToUuidMap = new ConcurrentHashMap<>(128, 0.5f, 4);
private final Map<UUID, CachedUUIDEntry> uuidToNameMap = new ConcurrentHashMap<>(128, 0.5f, 4);
private static final Gson gson = new Gson();
public UUIDTranslator(RedisBungeePlugin<?> plugin) {
this.plugin = plugin;
}
private void addToMaps(String name, UUID uuid) {
// This is why I like LocalDate...
// Cache the entry for three days.
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 3);
// Create the entry and populate the local maps
CachedUUIDEntry entry = new CachedUUIDEntry(name, uuid, calendar);
nameToUuidMap.put(name.toLowerCase(), entry);
uuidToNameMap.put(uuid, entry);
}
public UUID getTranslatedUuid(@NonNull String player, boolean expensiveLookups) {
// If the player is online, give them their UUID.
// Remember, local data > remote data.
if (plugin.getPlayer(player) != null)
return plugin.getPlayerUUID(player);
// Check if it exists in the map
CachedUUIDEntry cachedUUIDEntry = nameToUuidMap.get(player.toLowerCase());
if (cachedUUIDEntry != null) {
if (!cachedUUIDEntry.expired())
return cachedUUIDEntry.getUuid();
else
nameToUuidMap.remove(player);
}
// Check if we can exit early
if (UUID_PATTERN.matcher(player).find()) {
return UUID.fromString(player);
}
if (MOJANGIAN_UUID_PATTERN.matcher(player).find()) {
// Reconstruct the UUID
return UUIDFetcher.getUUID(player);
}
// If we are in offline mode, UUID generation is simple.
// We don't even have to cache the UUID, since this is easy to recalculate.
if (!plugin.isOnlineMode()) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + player).getBytes(Charsets.UTF_8));
}
RedisTask<UUID> redisTask = new RedisTask<UUID>(plugin) {
@Override
public UUID unifiedJedisTask(UnifiedJedis unifiedJedis) {
String stored = unifiedJedis.hget("uuid-cache", player.toLowerCase());
if (stored != null) {
// Found an entry value. Deserialize it.
CachedUUIDEntry entry = gson.fromJson(stored, CachedUUIDEntry.class);
// Check for expiry:
if (entry.expired()) {
unifiedJedis.hdel("uuid-cache", player.toLowerCase());
// Doesn't hurt to also remove the UUID entry as well.
unifiedJedis.hdel("uuid-cache", entry.getUuid().toString());
} else {
nameToUuidMap.put(player.toLowerCase(), entry);
uuidToNameMap.put(entry.getUuid(), entry);
return entry.getUuid();
}
}
// That didn't work. Let's ask Mojang.
if (!expensiveLookups || !plugin.isOnlineMode())
return null;
Map<String, UUID> uuidMap1;
try {
uuidMap1 = new UUIDFetcher(Collections.singletonList(player)).call();
} catch (Exception e) {
plugin.logFatal("Unable to fetch UUID from Mojang for " + player);
return null;
}
for (Map.Entry<String, UUID> entry : uuidMap1.entrySet()) {
if (entry.getKey().equalsIgnoreCase(player)) {
persistInfo(entry.getKey(), entry.getValue(), unifiedJedis);
return entry.getValue();
}
}
return null;
}
};
// Let's try Redis.
try {
return redisTask.execute();
} catch (JedisException e) {
plugin.logFatal("Unable to fetch UUID for " + player);
}
return null; // Nope, game over!
}
public String getNameFromUuid(@NonNull UUID player, boolean expensiveLookups) {
// If the player is online, give them their UUID.
// Remember, local data > remote data.
if (plugin.getPlayer(player) != null)
return plugin.getPlayerName(player);
// Check if it exists in the map
CachedUUIDEntry cachedUUIDEntry = uuidToNameMap.get(player);
if (cachedUUIDEntry != null) {
if (!cachedUUIDEntry.expired())
return cachedUUIDEntry.getName();
else
uuidToNameMap.remove(player);
}
RedisTask<String> redisTask = new RedisTask<String>(plugin) {
@Override
public String unifiedJedisTask(UnifiedJedis unifiedJedis) {
String stored = unifiedJedis.hget("uuid-cache", player.toString());
if (stored != null) {
// Found an entry value. Deserialize it.
CachedUUIDEntry entry = gson.fromJson(stored, CachedUUIDEntry.class);
// Check for expiry:
if (entry.expired()) {
unifiedJedis.hdel("uuid-cache", player.toString());
// Doesn't hurt to also remove the named entry as well.
// TODO: Since UUIDs are fixed, we could look up the name and see if the UUID matches.
unifiedJedis.hdel("uuid-cache", entry.getName());
} else {
nameToUuidMap.put(entry.getName().toLowerCase(), entry);
uuidToNameMap.put(player, entry);
return entry.getName();
}
}
if (!expensiveLookups || !plugin.isOnlineMode())
return null;
// That didn't work. Let's ask PlayerDB.
String name;
try {
name = NameFetcher.getName(player);
} catch (Exception e) {
plugin.logFatal("Unable to fetch name from PlayerDB for " + player);
return null;
}
if (name != null) {
persistInfo(name, player, unifiedJedis);
return name;
}
return null;
}
};
// Okay, it wasn't locally cached. Let's try Redis.
try {
return redisTask.execute();
} catch (JedisException e) {
plugin.logFatal("Unable to fetch name for " + player);
return null;
}
}
public void persistInfo(String name, UUID uuid, UnifiedJedis unifiedJedis) {
addToMaps(name, uuid);
String json = gson.toJson(uuidToNameMap.get(uuid));
unifiedJedis.hset("uuid-cache", ImmutableMap.of(name.toLowerCase(), json, uuid.toString(), json));
}
}

View File

@@ -0,0 +1,203 @@
Eclipse Public License - v 1.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and documentation
distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates'
from a Contributor if it was added to the Program by such Contributor
itself or anyone acting on such Contributor's behalf. Contributions do not
include additions to the Program which: (i) are separate modules of
software distributed in conjunction with the Program under their own
license agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly
perform, distribute and sublicense the Contribution of such Contributor,
if any, and such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of
the Contribution and the Program if, at the time the Contribution is
added by the Contributor, such addition of the Contribution causes such
combination to be covered by the Licensed Patents. The patent license
shall not apply to any other combinations which include the Contribution.
No hardware per se is licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor
disclaims any liability to Recipient for claims brought by any other
entity based on infringement of intellectual property rights or
otherwise. As a condition to exercising the rights and licenses granted
hereunder, each Recipient hereby assumes sole responsibility to secure
any other intellectual property rights needed, if any. For example, if a
third party patent license is required to allow Recipient to distribute
the Program, it is Recipient's responsibility to acquire that license
before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright
license set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties
and conditions, express and implied, including warranties or
conditions of title and non-infringement, and implied warranties or
conditions of merchantability and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and
consequential damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are
offered by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable
manner on or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained
within the Program.
Each Contributor must identify itself as the originator of its Contribution,
if
any, in a manner that reasonably allows subsequent Recipients to identify the
originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a manner
which does not create potential liability for other Contributors. Therefore,
if a Contributor includes the Program in a commercial product offering, such
Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
every other Contributor ("Indemnified Contributor") against any losses,
damages and costs (collectively "Losses") arising from claims, lawsuits and
other legal actions brought by a third party against the Indemnified
Contributor to the extent caused by the acts or omissions of such Commercial
Contributor in connection with its distribution of the Program in a commercial
product offering. The obligations in this section do not apply to any claims
or Losses relating to any actual or alleged intellectual property
infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor to control, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim at
its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
Recipient is solely responsible for determining the appropriateness of using
and distributing the Program and assumes all risks associated with its
exercise of rights under this Agreement , including but not limited to the
risks and costs of program errors, compliance with applicable laws, damage to
or loss of data, programs or equipment, and unavailability or interruption of
operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and does
not cure such failure in a reasonable period of time after becoming aware of
such noncompliance. If all Recipient's rights under this Agreement terminate,
Recipient agrees to cease use and distribution of the Program as soon as
reasonably practicable. However, Recipient's obligations under this Agreement
and any licenses granted by Recipient relating to the Program shall continue
and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of the
Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this Agreement,
whether expressly, by implication, estoppel or otherwise. All rights in the
Program not expressly granted under this Agreement are reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial in
any resulting litigation.

View File

@@ -0,0 +1,142 @@
# RedisBungee configuration file.
# Notice:
# Redis 7.2.4 is last free and open source Redis version after license change
# https://download.redis.io/releases/redis-7.2.4.tar.gz which you have to compile yourself,
# unless your package manager still provide it.
# Here is The alternatives
# - 'ValKey' By linux foundation https://valkey.io/download/
# - 'KeyDB' by Snapchat inc https://docs.keydb.dev/docs/download/
# The 'Redis', 'ValKey', 'KeyDB' server you will use.
# these settings are ignored when cluster mode is enabled.
redis-server: 127.0.0.1
redis-port: 6379
# Cluster Mode
# enabling this option will enable cluster mode.
cluster-mode-enabled: false
# FORMAT:
# redis-cluster-servers:
# - host: 127.0.0.1`
# port: 2020
# - host: 127.0.0.1
# port: 2021
# - host: 127.0.0.1
# port: 2021
# you can set single server and Jedis will automatically discover cluster nodes,
# but might fail if this single redis node is down when Proxy startup, its recommended put the all the nodes
redis-cluster-servers:
- host: 127.0.0.1
port: 6379
# OPTIONAL: if your redis uses acl usernames set the username here. leave empty for no username.
redis-username: ""
# OPTIONAL but recommended: If your Redis server uses AUTH, set the required password.
redis-password: ""
# Maximum connections that will be maintained to the Redis server.
# The default is 10. This setting should be left as-is unless you have some wildly
# inefficient plugins or a lot of players.
max-redis-connections: 10
# since redis can support ssl by version 6 you can use SSL/TLS in redis bungee too!
# but there is more configuration needed to work see https://github.com/ProxioDev/RedisBungee/issues/18
# Keep note that SSL/TLS connections will decrease redis performance so use it when needed.
useSSL: false
# An identifier for this network, which helps to separate redisbungee instances on same redis instance.
# You can use environment variable 'REDISBUNGEE_NETWORK_ID' to override
network-id: "main"
# An identifier for this BungeeCord / Velocity instance. Will randomly generate if leaving it blank.
# You can set Environment variable 'REDISBUNGEE_PROXY_ID' to override
proxy-id: "proxy-1"
# since RedisBungee Internally now uses UnifiedJedis instead of Jedis, JedisPool.
# which will break compatibility with old plugins that uses RedisBungee JedisPool
# so to mitigate this issue, RedisBungee will create an JedisPool for compatibility reasons.
# disabled by default
# Automatically disabled when cluster mode is enabled
enable-jedis-pool-compatibility: false
# max connections for the compatibility pool
compatibility-max-connections: 3
# restore old login behavior before 0.9.0 update
# enabled by default
# when true: when player login and there is old player with same uuid it will get disconnected as result and new player will log in
# when false: when a player login but login will fail because old player is still connected.
kick-when-online: true
# enabled by default
# this option tells RedisBungee handle motd and set online count, when motd is requested
# you can disable this when you want to handle motd yourself, use RedisBungee api to get total players when needed :)
handle-motd: true
# A list of IP addresses for which RedisBungee will not modify the response for, useful for automatic
# restart scripts.
# Automatically disabled if handle-motd is disabled.
exempt-ip-addresses: []
# disabled by default
# RedisBungee will attempt to connect player to last server that was stored.
reconnect-to-last-server: false
# For redis bungee legacy commands
# either can be run using '/rbl glist' for example
# or if 'install' is set to true '/glist' can be used.
# 'install' also overrides the proxy installed commands
#
# In legacy commands each command got it own permissions since they had it own permission pre new command system,
# so it's also applied to subcommands in '/rbl'.
commands:
# Permission redisbungee.legacy.use
redisbungee-legacy:
enabled: false
subcommands:
# Permission redisbungee.command.glist
glist:
enabled: false
install: false
# Permission redisbungee.command.find
find:
enabled: false
install: false
# Permission redisbungee.command.lastseen
lastseen:
enabled: false
install: false
# Permission redisbungee.command.ip
ip:
enabled: false
install: false
# Permission redisbungee.command.pproxy
pproxy:
enabled: false
install: false
# Permission redisbungee.command.sendtoall
sendtoall:
enabled: false
install: false
# Permission redisbungee.command.serverid
serverid:
enabled: false
install: false
# Permission redisbungee.command.serverids
serverids:
enabled: false
install: false
# Permission redisbungee.command.plist
plist:
enabled: false
install: false
# Permission redisbungee.command.use
redisbungee:
enabled: true
# Config version DO NOT CHANGE!!!!
config-version: 2

View File

@@ -0,0 +1,55 @@
# this config file is for messages / Languages
# use MiniMessage format https://docs.advntr.dev/minimessage/format.html
# for colors etc... Legacy chat color is not supported.
# Language codes used in minecraft from the minecraft wiki
# example: en-us for american english and ar-sa for arabic
# all codes can be obtained from link below
# from the colum Locale Code -> In-game
# NOTE: minecraft wiki shows languages like this `en_us` in config it should be `en-us`
# https://minecraft.wiki/w/Language
# example:
# lets assume we want to add arabic language.
# messages:
# logged-in-other-location:
# en-us: "<color:red>You logged in from another location!"
# ar-sa: "<color:red>لقد اتصلت من مكان اخر"
# RedisBungee Prefix if ever used.
prefix: "<color:red>[<color:yellow>Redis<color:red>Bungee]"
# en-us is american English, Which is the default language used when a language for a message isn't defined.
# Warning: IF THE set default locale wasn't defined in the config for all messages, plugin will not load.
# set the Default locale
default-locale: en-us
# send language based on client sent settings
# if you don't have languages configured For client Language
# it will default to language that has been set above
# NOTE: due minecraft protocol not sending player settings during login,
# some of the messages like logged-in-other-location will
# skip translation and use default locale that has been set in default-locale.
use-client-locale: true
# messages that are used during login, and connecting to Last server
messages:
logged-in-other-location:
en-us: "<color:red>You logged in from another location!"
pt-br: "<color:red>Você está logado em outra localização!"
already-logged-in:
en-us: "<color:red>You are already logged in!"
pt-br: "<color:red>Você já está logado!"
server-not-found:
# placeholder <server> displays server name in the message.
en-us: "<color:red>unable to connect you to the last server, because server <server> was not found."
pt-br: "<color:red>falha ao conectar você ao último servidor, porque o servidor <server> não foi encontrado."
server-connecting:
# placeholder <server> displays server name in the message.
en-us: "<color:green>Connecting you to <server>..."
pt-br: "<color:green>Conectando você a <server>..."
# DO NOT CHANGE!!!!!
config-version: 1