1
0
Fork 0
mirror of https://github.com/xHyroM/lighteco.git synced 2024-11-21 22:41:06 +01:00

feat: messaging (#7)

* feat: messaging

* fix: some things

* feat: make it actually working

* feat: disable messaging by default

* fix: avoid infinity loop

* feat: multiple channels

* remove todo message

* fix: global channel should be lighteco:messages

* fix: check if CHANNELS not null

* refactor: set channels in constructor

* fix: push user updates on quit when user is dirty
This commit is contained in:
Jozef Steinhübl 2023-12-24 15:46:13 +01:00 committed by GitHub
parent b1d5ae7c09
commit 35f94eac3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 743 additions and 77 deletions

View file

@ -3,10 +3,13 @@ package dev.xhyrom.lighteco.api;
import dev.xhyrom.lighteco.api.manager.CommandManager;
import dev.xhyrom.lighteco.api.manager.CurrencyManager;
import dev.xhyrom.lighteco.api.manager.UserManager;
import dev.xhyrom.lighteco.api.messaging.MessagingService;
import dev.xhyrom.lighteco.api.platform.Platform;
import dev.xhyrom.lighteco.api.platform.PlayerAdapter;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
public interface LightEco {
/**
* Gets the {@link Platform}, which represents the current platform the
@ -37,6 +40,13 @@ public interface LightEco {
*/
@NonNull CommandManager getCommandManager();
/**
* Gets the {@link MessagingService}, which manages the messaging.
*
* @return the messaging service
*/
@NonNull Optional<MessagingService> getMessagingService();
/**
* Gets the {@link PlayerAdapter} for a player class.
*

View file

@ -0,0 +1,8 @@
package dev.xhyrom.lighteco.api.messaging;
import dev.xhyrom.lighteco.api.model.currency.Currency;
import dev.xhyrom.lighteco.api.model.user.User;
public interface MessagingService {
void pushUserUpdate(User user, Currency currency);
}

View file

@ -0,0 +1,10 @@
package dev.xhyrom.lighteco.api.messenger;
import dev.xhyrom.lighteco.api.messenger.message.Message;
import org.checkerframework.checker.nullness.qual.NonNull;
public interface IncomingMessageConsumer {
void consumeIncomingMessage(@NonNull Message message);
void consumeRawIncomingMessage(@NonNull String message);
}

View file

@ -0,0 +1,12 @@
package dev.xhyrom.lighteco.api.messenger;
import dev.xhyrom.lighteco.api.messenger.message.OutgoingMessage;
import org.checkerframework.checker.nullness.qual.NonNull;
public interface Messenger extends AutoCloseable {
void sendOutgoingMessage(@NonNull OutgoingMessage message, boolean global);
@Override
default void close() {
}
}

View file

@ -0,0 +1,7 @@
package dev.xhyrom.lighteco.api.messenger;
import org.checkerframework.checker.nullness.qual.NonNull;
public interface MessengerProvider {
@NonNull Messenger obtain(@NonNull IncomingMessageConsumer consumer);
}

View file

@ -0,0 +1,12 @@
package dev.xhyrom.lighteco.api.messenger.message;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.UUID;
/**
* Represents a message that can be received by a {@link dev.xhyrom.lighteco.api.messenger.Messenger}.
*/
public interface Message {
@NonNull UUID getId();
}

View file

@ -0,0 +1,15 @@
package dev.xhyrom.lighteco.api.messenger.message;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents a message that can be sent via a {@link dev.xhyrom.lighteco.api.messenger.Messenger}.
*/
public interface OutgoingMessage extends Message {
/**
* Serializes message into a string.
*
* @return serialized message
*/
@NonNull String serialize();
}

View file

@ -0,0 +1,23 @@
package dev.xhyrom.lighteco.api.messenger.message.type;
import dev.xhyrom.lighteco.api.messenger.message.Message;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.math.BigDecimal;
import java.util.UUID;
/**
* Represents a message that is sent when a user updates their profile.
*/
public interface UserUpdateMessage extends Message {
/**
* Gets the unique id of the user that updated their profile.
*
* @return the user's unique id
*/
@NonNull UUID getUserUniqueId();
@NonNull String getCurrencyIdentifier();
@NonNull BigDecimal getNewBalance();
}

View file

@ -8,6 +8,7 @@ import dev.xhyrom.lighteco.bukkit.listeners.BukkitConnectionListener;
import dev.xhyrom.lighteco.bukkit.manager.BukkitCommandManager;
import dev.xhyrom.lighteco.bukkit.manager.BukkitContextManager;
import dev.xhyrom.lighteco.common.manager.currency.StandardCurrencyManager;
import dev.xhyrom.lighteco.common.messaging.MessagingFactory;
import dev.xhyrom.lighteco.common.plugin.AbstractLightEcoPlugin;
import dev.xhyrom.lighteco.common.manager.user.StandardUserManager;
import lombok.Getter;
@ -45,6 +46,11 @@ public class BukkitLightEcoPlugin extends AbstractLightEcoPlugin {
this.contextManager = new BukkitContextManager();
}
@Override
protected MessagingFactory getMessagingFactory() {
return new MessagingFactory(this);
}
@Override
protected void registerApiOnPlatform(LightEco api) {
this.getBootstrap().getLoader().getServer().getServicesManager().register(LightEco.class, api, this.getBootstrap().getLoader(), ServicePriority.Normal);

View file

@ -1,11 +1,11 @@
package dev.xhyrom.lighteco.bukkit.listeners;
import dev.xhyrom.lighteco.bukkit.BukkitLightEcoPlugin;
import dev.xhyrom.lighteco.common.model.currency.Currency;
import dev.xhyrom.lighteco.common.model.user.User;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@ -58,11 +58,10 @@ public class BukkitConnectionListener implements Listener {
}
this.plugin.getUserManager().saveUser(user)
.thenAccept(v -> {
// make sure the player is offline before unloading
if (Bukkit.getPlayer(uniqueId) != null) return;
this.plugin.getUserManager().unload(uniqueId);
});
.thenAccept(v -> this.plugin.getMessagingService().ifPresent(service -> {
for (Currency currency : this.plugin.getCurrencyManager().getRegisteredCurrencies()) {
service.pushUserUpdate(user, currency);
}
}));
}
}

View file

@ -20,6 +20,7 @@ dependencies {
implementation("eu.okaeri:okaeri-configs-validator-okaeri:5.0.0-beta.5")
compileOnly("com.zaxxer:HikariCP:5.0.1")
compileOnly("redis.clients:jedis:5.1.0")
compileOnly("org.projectlombok:lombok:1.18.28")
annotationProcessor("org.projectlombok:lombok:1.18.28")

View file

@ -4,12 +4,15 @@ import dev.xhyrom.lighteco.api.LightEco;
import dev.xhyrom.lighteco.api.manager.CommandManager;
import dev.xhyrom.lighteco.api.manager.CurrencyManager;
import dev.xhyrom.lighteco.api.manager.UserManager;
import dev.xhyrom.lighteco.api.messaging.MessagingService;
import dev.xhyrom.lighteco.api.platform.Platform;
import dev.xhyrom.lighteco.api.platform.PlayerAdapter;
import dev.xhyrom.lighteco.common.api.impl.*;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
public class LightEcoApi implements LightEco {
private final LightEcoPlugin plugin;
@ -49,6 +52,11 @@ public class LightEcoApi implements LightEco {
return this.commandManager;
}
@Override
public @NonNull Optional<MessagingService> getMessagingService() {
return this.plugin.getMessagingService().map(ApiMessagingService::new);
}
@Override
public @NonNull <T> PlayerAdapter<T> getPlayerAdapter(@NonNull Class<T> playerClass) {
Class<?> expected = this.plugin.getContextManager().getPlayerClass();

View file

@ -4,10 +4,10 @@ import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
public abstract class ApiAbstractManager<H> {
protected final LightEcoPlugin plugin;
protected final H handler;
protected final H handle;
protected ApiAbstractManager(LightEcoPlugin plugin, H handler) {
protected ApiAbstractManager(LightEcoPlugin plugin, H handle) {
this.plugin = plugin;
this.handler = handler;
this.handle = handle;
}
}

View file

@ -6,8 +6,8 @@ import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
public class ApiCommandManager extends ApiAbstractManager<dev.xhyrom.lighteco.common.manager.command.CommandManager> implements CommandManager {
public ApiCommandManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.command.CommandManager handler) {
super(plugin, handler);
public ApiCommandManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.command.CommandManager handle) {
super(plugin, handle);
}
@Override
@ -15,7 +15,7 @@ public class ApiCommandManager extends ApiAbstractManager<dev.xhyrom.lighteco.co
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.plugin.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
this.handler.registerCurrencyCommand(internal);
this.handle.registerCurrencyCommand(internal);
}
@Override
@ -23,6 +23,6 @@ public class ApiCommandManager extends ApiAbstractManager<dev.xhyrom.lighteco.co
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.plugin.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
this.handler.registerCurrencyCommand(internal, main);
this.handle.registerCurrencyCommand(internal, main);
}
}

View file

@ -8,29 +8,29 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Collection;
public class ApiCurrencyManager extends ApiAbstractManager<dev.xhyrom.lighteco.common.manager.currency.CurrencyManager> implements CurrencyManager {
public ApiCurrencyManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.currency.CurrencyManager handler) {
super(plugin, handler);
public ApiCurrencyManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.currency.CurrencyManager handle) {
super(plugin, handle);
}
private Currency wrap(dev.xhyrom.lighteco.common.model.currency.Currency handler) {
return handler.getProxy();
private Currency wrap(dev.xhyrom.lighteco.common.model.currency.Currency handle) {
return handle.getProxy();
}
@Override
public @NonNull Collection<Currency> getRegisteredCurrencies() {
return this.handler.values()
return this.handle.values()
.stream().map(this::wrap)
.toList();
}
@Override
public Currency getCurrency(@NonNull String identifier) {
return wrap(this.handler.getIfLoaded(identifier));
return wrap(this.handle.getIfLoaded(identifier));
}
@Override
public void registerCurrency(@NonNull Currency currency) {
dev.xhyrom.lighteco.common.model.currency.Currency internal = new dev.xhyrom.lighteco.common.model.currency.Currency(currency);
this.handler.registerCurrency(internal);
this.handle.registerCurrency(internal);
}
}

View file

@ -0,0 +1,23 @@
package dev.xhyrom.lighteco.common.api.impl;
import dev.xhyrom.lighteco.api.messaging.MessagingService;
import dev.xhyrom.lighteco.api.model.currency.Currency;
import dev.xhyrom.lighteco.api.model.user.User;
import dev.xhyrom.lighteco.common.messaging.InternalMessagingService;
public class ApiMessagingService implements MessagingService {
private final InternalMessagingService handle;
public ApiMessagingService(InternalMessagingService handle) {
this.handle = handle;
}
@Override
public void pushUserUpdate(User user, Currency currency) {
dev.xhyrom.lighteco.common.model.currency.Currency internalCurrency = this.handle.getPlugin()
.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
this.handle.pushUserUpdate(ApiUser.cast(user), internalCurrency);
}
}

View file

@ -10,60 +10,68 @@ import java.math.BigDecimal;
import java.util.UUID;
public class ApiUser implements User {
private final dev.xhyrom.lighteco.common.model.user.User handler;
public static dev.xhyrom.lighteco.common.model.user.User cast(User u) {
if (u instanceof ApiUser) {
return ((ApiUser) u).handle;
}
public ApiUser(dev.xhyrom.lighteco.common.model.user.User handler) {
this.handler = handler;
throw new IllegalArgumentException("Cannot cast " + u.getClass().getName() + " to " + ApiUser.class.getName());
}
private final dev.xhyrom.lighteco.common.model.user.User handle;
public ApiUser(dev.xhyrom.lighteco.common.model.user.User handle) {
this.handle = handle;
}
@Override
public @NonNull UUID getUniqueId() {
return this.handler.getUniqueId();
return this.handle.getUniqueId();
}
@Override
public @Nullable String getUsername() {
return this.handler.getUsername();
return this.handle.getUsername();
}
@Override
public @NonNull BigDecimal getBalance(@NonNull Currency currency) {
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handler.getPlugin()
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handle.getPlugin()
.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
return this.handler.getBalance(internal);
return this.handle.getBalance(internal);
}
@Override
public void setBalance(@NonNull Currency currency, @NonNull BigDecimal balance) {
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handler.getPlugin()
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handle.getPlugin()
.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
this.handler.setBalance(internal, balance);
this.handle.setBalance(internal, balance);
}
@Override
public void deposit(@NonNull Currency currency, @NonNull BigDecimal amount) {
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handler.getPlugin()
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handle.getPlugin()
.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
this.handler.deposit(internal, amount);
this.handle.deposit(internal, amount);
}
@Override
public void withdraw(@NonNull Currency currency, @NonNull BigDecimal amount) {
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handler.getPlugin()
dev.xhyrom.lighteco.common.model.currency.Currency internal = this.handle.getPlugin()
.getCurrencyManager()
.getIfLoaded(currency.getIdentifier());
this.handler.withdraw(internal, amount);
this.handle.withdraw(internal, amount);
}
@Override
public void sendMessage(Component message) {
this.handler.sendMessage(message);
this.handle.sendMessage(message);
}
}

View file

@ -10,13 +10,13 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class ApiUserManager extends ApiAbstractManager<dev.xhyrom.lighteco.common.manager.user.UserManager> implements UserManager {
public ApiUserManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.user.UserManager handler) {
super(plugin, handler);
public ApiUserManager(LightEcoPlugin plugin, dev.xhyrom.lighteco.common.manager.user.UserManager handle) {
super(plugin, handle);
}
private User wrap(dev.xhyrom.lighteco.common.model.user.User handler) {
this.plugin.getUserManager().getHousekeeper().registerUsage(handler.getUniqueId());
return handler.getProxy();
private User wrap(dev.xhyrom.lighteco.common.model.user.User handle) {
this.plugin.getUserManager().getHousekeeper().registerUsage(handle.getUniqueId());
return handle.getProxy();
}
@Override
@ -42,11 +42,11 @@ public class ApiUserManager extends ApiAbstractManager<dev.xhyrom.lighteco.commo
@Override
public @Nullable User getUser(@NonNull UUID uniqueId) {
return wrap(this.handler.getIfLoaded(uniqueId));
return wrap(this.handle.getIfLoaded(uniqueId));
}
@Override
public boolean isLoaded(@NonNull UUID uniqueId) {
return this.handler.isLoaded(uniqueId);
return this.handle.isLoaded(uniqueId);
}
}

View file

@ -2,6 +2,7 @@ package dev.xhyrom.lighteco.common.config;
import dev.xhyrom.lighteco.common.config.housekeeper.HousekeeperConfig;
import dev.xhyrom.lighteco.common.config.message.MessageConfig;
import dev.xhyrom.lighteco.common.config.messaging.MessagingConfig;
import dev.xhyrom.lighteco.common.config.storage.StorageConfig;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
@ -20,6 +21,9 @@ public class Config extends OkaeriConfig {
@Comment("Storage settings.")
public StorageConfig storage = new StorageConfig();
@Comment("Messaging settings.")
public MessagingConfig messaging = new MessagingConfig();
@Comment("Save interval to storage in seconds.")
public long saveInterval = 5L;

View file

@ -0,0 +1,15 @@
package dev.xhyrom.lighteco.common.config.messaging;
import dev.xhyrom.lighteco.common.messaging.MessagingType;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class MessagingConfig extends OkaeriConfig {
@Comment("Messaging provider.")
@Comment("Available providers: redis")
public MessagingType provider = MessagingType.NONE;
@Comment("Data storage settings.")
@Comment("You don't need to worry about this if you're using plugin message.")
public MessagingDataConfig data = new MessagingDataConfig();
}

View file

@ -0,0 +1,16 @@
package dev.xhyrom.lighteco.common.config.messaging;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class MessagingDataConfig extends OkaeriConfig {
@Comment("Define the address and port of the messaging service.")
public String address = "localhost";
@Comment("Credentials for connecting to the messaging service.")
public String username = "root";
public String password = "password";
@Comment("Whether to use SSL to connect to the messaging service.")
public boolean ssl = false;
}

View file

@ -61,6 +61,29 @@ public enum Dependency {
"postgresql",
"42.6.0",
Relocation.of("postgresql", "org{}postgresql")
),
JEDIS(
"redis.clients",
"jedis",
"5.1.0",
Relocation.of("jedis", "redis{}clients{}jedis"),
Relocation.of("commonspool2", "org{}apache{}commons{}pool2")
),
SLF4J_SIMPLE(
"org.slf4j",
"slf4j-simple",
"1.7.30"
),
SLF4J_API(
"org.slf4j",
"slf4j-api",
"1.7.30"
),
COMMONS_POOL_2(
"org.apache.commons",
"commons-pool2",
"2.9.0",
Relocation.of("commonspool2", "org{}apache{}commons{}pool2")
);
private final String fullPath;

View file

@ -1,5 +1,6 @@
package dev.xhyrom.lighteco.common.dependencies;
import dev.xhyrom.lighteco.common.messaging.MessagingType;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.util.Set;
@ -8,6 +9,7 @@ public interface DependencyManager extends AutoCloseable {
void loadDependencies(Set<Dependency> dependencies);
void loadStorageDependencies(Set<StorageType> types);
void loadMessagingDependencies(Set<MessagingType> types);
ClassLoader obtainClassLoaderWith(Set<Dependency> dependencies);

View file

@ -5,10 +5,11 @@ import com.google.common.io.MoreFiles;
import dev.xhyrom.lighteco.common.config.Config;
import dev.xhyrom.lighteco.common.dependencies.relocation.Relocation;
import dev.xhyrom.lighteco.common.dependencies.relocation.RelocationHandler;
import dev.xhyrom.lighteco.common.messaging.MessagingType;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.plugin.logger.PluginLogger;
import dev.xhyrom.lighteco.common.util.URLClassLoaderAccess;
import dev.xhyrom.lighteco.common.storage.StorageType;
import dev.xhyrom.lighteco.common.util.URLClassLoaderAccess;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import java.io.IOException;
@ -17,8 +18,6 @@ import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
public class DependencyManagerImpl implements DependencyManager {
private final EnumMap<Dependency, Path> loaded = new EnumMap<>(Dependency.class);
@ -49,18 +48,14 @@ public class DependencyManagerImpl implements DependencyManager {
@Override
public void loadDependencies(Set<Dependency> dependencies) {
CountDownLatch latch = new CountDownLatch(dependencies.size());
if (this.config.debug)
this.logger.info("Loading dependencies: " + dependencies);
for (Dependency dependency : dependencies) {
if (this.loaded.containsKey(dependency)) {
latch.countDown();
continue;
}
CompletableFuture.runAsync(() -> {
if (this.config.debug)
this.logger.info("Loading dependency " + dependency);
@ -69,22 +64,13 @@ public class DependencyManagerImpl implements DependencyManager {
} catch (Exception e) {
throw new RuntimeException("Failed to load dependency " + dependency, e);
} finally {
latch.countDown();
if (this.config.debug)
this.logger.info("Loaded dependency " + dependency);
}
});
}
try {
latch.await();
if (this.config.debug)
this.logger.info("Loaded dependencies: " + dependencies);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void loadDependency(Dependency dependency) throws Exception {
@ -142,6 +128,11 @@ public class DependencyManagerImpl implements DependencyManager {
loadDependencies(this.registry.resolveStorageDependencies(types));
}
@Override
public void loadMessagingDependencies(Set<MessagingType> types) {
loadDependencies(this.registry.resolveMessagingDependencies(types));
}
@Override
public ClassLoader obtainClassLoaderWith(Set<Dependency> dependencies) {
ImmutableSet<Dependency> set = ImmutableSet.copyOf(dependencies);

View file

@ -2,6 +2,7 @@ package dev.xhyrom.lighteco.common.dependencies;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.SetMultimap;
import dev.xhyrom.lighteco.common.messaging.MessagingType;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.util.LinkedHashSet;
@ -16,6 +17,10 @@ public class DependencyRegistry {
.putAll(StorageType.POSTGRESQL, Dependency.POSTGRESQL_DRIVER, Dependency.HIKARI)
.build();
private static final SetMultimap<MessagingType, Dependency> MESSAGING_DEPENDENCIES = ImmutableSetMultimap.<MessagingType, Dependency>builder()
.putAll(MessagingType.REDIS, Dependency.COMMONS_POOL_2, Dependency.JEDIS, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE)
.build();
public Set<Dependency> resolveStorageDependencies(Set<StorageType> types) {
Set<Dependency> dependencies = new LinkedHashSet<>();
@ -26,6 +31,16 @@ public class DependencyRegistry {
return dependencies;
}
public Set<Dependency> resolveMessagingDependencies(Set<MessagingType> types) {
Set<Dependency> dependencies = new LinkedHashSet<>();
for (MessagingType type : types) {
dependencies.addAll(MESSAGING_DEPENDENCIES.get(type));
}
return dependencies;
}
public boolean shouldAutoLoad(Dependency dependency) {
return switch (dependency) {
case H2_DRIVER, SQLITE_DRIVER -> false;

View file

@ -0,0 +1,13 @@
package dev.xhyrom.lighteco.common.messaging;
import dev.xhyrom.lighteco.common.model.currency.Currency;
import dev.xhyrom.lighteco.common.model.user.User;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
public interface InternalMessagingService {
LightEcoPlugin getPlugin();
void pushUserUpdate(User user, Currency currency);
void shutdown();
}

View file

@ -0,0 +1,145 @@
package dev.xhyrom.lighteco.common.messaging;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import dev.xhyrom.lighteco.api.messenger.IncomingMessageConsumer;
import dev.xhyrom.lighteco.api.messenger.Messenger;
import dev.xhyrom.lighteco.api.messenger.MessengerProvider;
import dev.xhyrom.lighteco.api.messenger.message.Message;
import dev.xhyrom.lighteco.api.messenger.message.type.UserUpdateMessage;
import dev.xhyrom.lighteco.common.cache.ExpiringSet;
import dev.xhyrom.lighteco.common.messaging.message.MessageType;
import dev.xhyrom.lighteco.common.messaging.message.UserUpdateMessageImpl;
import dev.xhyrom.lighteco.common.model.currency.Currency;
import dev.xhyrom.lighteco.common.model.user.User;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.util.gson.GsonProvider;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class LightEcoMessagingService implements InternalMessagingService, IncomingMessageConsumer {
@Getter
private final LightEcoPlugin plugin;
private final ExpiringSet<UUID> receivedMessages;
private final Messenger messenger;
private final MessengerProvider provider;
public LightEcoMessagingService(LightEcoPlugin plugin, MessengerProvider provider) {
this.plugin = plugin;
this.provider = provider;
this.messenger = provider.obtain(this);
this.receivedMessages = new ExpiringSet<>(30, TimeUnit.MINUTES);
}
private UUID generateMessageId() {
UUID id = UUID.randomUUID();
this.receivedMessages.add(id);
return id;
}
@Override
public void pushUserUpdate(User user, Currency currency) {
this.plugin.getBootstrap().getScheduler().async().execute(() ->
this.messenger.sendOutgoingMessage(
new UserUpdateMessageImpl(generateMessageId(), user.getUniqueId(), currency.getIdentifier(), user.getBalance(currency)),
currency.getType() == dev.xhyrom.lighteco.api.model.currency.Currency.Type.GLOBAL
)
);
}
public static @NonNull String serialize(MessageType type, UUID id, JsonElement content) {
JsonObject data = new JsonObject();
data.add("i", new JsonPrimitive(id.toString()));
data.add("t", new JsonPrimitive(type.name()));
data.add("c", content);
return GsonProvider.get().toJson(data);
}
@Override
public void consumeIncomingMessage(@NonNull Message message) {
if (!this.receivedMessages.add(message.getId())) {
return;
}
this.processIncomingMessage(message);
}
@Override
public void consumeRawIncomingMessage(@NonNull String message) {
try {
deserializeAndConsumeRawIncomingMessage(message);
} catch (Exception e) {
this.plugin.getBootstrap().getLogger().warn("Failed to deserialize incoming message: " + message, e);
}
}
private void deserializeAndConsumeRawIncomingMessage(@NonNull String message) {
JsonObject parsed = GsonProvider.get().fromJson(message, JsonObject.class);
JsonElement idElement = parsed.get("i");
if (idElement == null) {
throw new IllegalStateException("Missing message id: " + message);
}
UUID id = UUID.fromString(idElement.getAsString());
if (!this.receivedMessages.add(id)) {
return;
}
JsonElement typeElement = parsed.get("t");
if (typeElement == null) {
throw new IllegalStateException("Missing message type: " + message);
}
MessageType type = MessageType.valueOf(typeElement.getAsString());
@Nullable JsonElement contentElement = parsed.get("c");
Message deserialized;
switch (type) {
case USER_UPDATE:
deserialized = UserUpdateMessageImpl.deserialize(id, contentElement);
break;
default:
return;
}
this.processIncomingMessage(deserialized);
}
private void processIncomingMessage(Message message) {
if (message instanceof UserUpdateMessage userUpdateMessage) {
this.plugin.getBootstrap().getScheduler().async().execute(() -> {
User user = this.plugin.getUserManager().getIfLoaded(userUpdateMessage.getUserUniqueId());
if (user == null) {
return;
}
Currency currency = this.plugin.getCurrencyManager().getIfLoaded(userUpdateMessage.getCurrencyIdentifier());
if (currency == null) {
return;
}
user.setBalance(currency, userUpdateMessage.getNewBalance(), false, false);
});
} else {
throw new IllegalStateException("Unknown message type: " + message.getClass().getName());
}
}
@Override
public void shutdown() {
this.messenger.close();
}
}

View file

@ -0,0 +1,38 @@
package dev.xhyrom.lighteco.common.messaging;
import dev.xhyrom.lighteco.api.messenger.MessengerProvider;
import dev.xhyrom.lighteco.common.messaging.type.redis.RedisMessengerProvider;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import java.util.Set;
public class MessagingFactory {
private final LightEcoPlugin plugin;
public MessagingFactory(LightEcoPlugin plugin) {
this.plugin = plugin;
}
protected LightEcoPlugin getPlugin() {
return this.plugin;
}
public Set<MessagingType> getRequiredTypes() {
return Set.of(this.plugin.getConfig().messaging.provider);
}
public InternalMessagingService get() {
MessagingType type = this.plugin.getConfig().messaging.provider;
MessengerProvider provider = this.createProvider(type);
if (provider == null) return null;
return new LightEcoMessagingService(this.plugin, provider);
}
private MessengerProvider createProvider(MessagingType type) {
return switch (type) {
case REDIS -> new RedisMessengerProvider(this.plugin);
default -> null;
};
}
}

View file

@ -0,0 +1,6 @@
package dev.xhyrom.lighteco.common.messaging;
public enum MessagingType {
NONE,
REDIS
}

View file

@ -0,0 +1,20 @@
package dev.xhyrom.lighteco.common.messaging.message;
import dev.xhyrom.lighteco.api.messenger.message.Message;
import dev.xhyrom.lighteco.api.messenger.message.OutgoingMessage;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.UUID;
public abstract class AbstractMessage implements Message, OutgoingMessage {
private final UUID id;
protected AbstractMessage(UUID id) {
this.id = id;
}
@Override
public @NonNull UUID getId() {
return this.id;
}
}

View file

@ -0,0 +1,5 @@
package dev.xhyrom.lighteco.common.messaging.message;
public enum MessageType {
USER_UPDATE;
}

View file

@ -0,0 +1,49 @@
package dev.xhyrom.lighteco.common.messaging.message;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import dev.xhyrom.lighteco.api.messenger.message.type.UserUpdateMessage;
import dev.xhyrom.lighteco.common.messaging.LightEcoMessagingService;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.math.BigDecimal;
import java.util.UUID;
public class UserUpdateMessageImpl extends AbstractMessage implements UserUpdateMessage {
private static final MessageType TYPE = MessageType.USER_UPDATE;
@Getter
private final UUID userUniqueId;
@Getter
private final String currencyIdentifier;
@Getter
private final BigDecimal newBalance;
public static UserUpdateMessage deserialize(UUID id, @NonNull JsonElement data) {
JsonObject obj = data.getAsJsonObject();
UUID userUniqueId = UUID.fromString(obj.get("u").getAsString());
String currencyIdentifier = obj.get("c").getAsString();
BigDecimal newBalance = obj.get("b").getAsBigDecimal();
return new UserUpdateMessageImpl(id, userUniqueId, currencyIdentifier, newBalance);
}
public UserUpdateMessageImpl(UUID id, UUID userUniqueId, String currencyIdentifier, BigDecimal newBalance) {
super(id);
this.userUniqueId = userUniqueId;
this.currencyIdentifier = currencyIdentifier;
this.newBalance = newBalance;
}
@Override
public @NonNull String serialize() {
JsonObject data = new JsonObject();
data.add("u", new JsonPrimitive(this.userUniqueId.toString()));
data.add("c", new JsonPrimitive(this.currencyIdentifier));
data.add("b", new JsonPrimitive(this.newBalance));
return LightEcoMessagingService.serialize(TYPE, getId(), data);
}
}

View file

@ -0,0 +1,94 @@
package dev.xhyrom.lighteco.common.messaging.type.redis;
import dev.xhyrom.lighteco.api.messenger.IncomingMessageConsumer;
import dev.xhyrom.lighteco.api.messenger.Messenger;
import dev.xhyrom.lighteco.api.messenger.message.OutgoingMessage;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import redis.clients.jedis.*;
public class RedisMessenger implements Messenger {
private static final String CHANNEL = "lighteco:{}:messages";
@Getter
private final String[] channels;
private final LightEcoPlugin plugin;
private final IncomingMessageConsumer consumer;
private UnifiedJedis jedis;
private Subscription sub;
public RedisMessenger(LightEcoPlugin plugin, IncomingMessageConsumer consumer) {
this.plugin = plugin;
this.consumer = consumer;
this.channels = new String[] {
CHANNEL.replace("{}:", ""),
CHANNEL.replace("{}", this.plugin.getConfig().server)
};
}
public void init(@Nullable String address, @Nullable String username, String password, boolean ssl) {
this.init(new JedisPooled(
parseAddress(address),
jedisConfig(username, password, ssl)
));
}
private void init(UnifiedJedis jedis) {
this.jedis = jedis;
this.sub = new Subscription(this);
this.plugin.getBootstrap().getScheduler().async().execute(() -> {
this.jedis.subscribe(this.sub, this.getChannels());
});
}
private static JedisClientConfig jedisConfig(@Nullable String username, @Nullable String password, boolean ssl) {
return DefaultJedisClientConfig.builder()
.user(username)
.password(password)
.ssl(ssl)
.timeoutMillis(Protocol.DEFAULT_TIMEOUT)
.build();
}
private static HostAndPort parseAddress(String address) {
String[] addressSplit = address.split(":");
String host = addressSplit[0];
int port = addressSplit.length > 1 ? Integer.parseInt(addressSplit[1]) : Protocol.DEFAULT_PORT;
return new HostAndPort(host, port);
}
@Override
public void sendOutgoingMessage(@NonNull OutgoingMessage message, boolean global) {
this.jedis.publish(global ? getChannels()[0] : getChannels()[1], message.serialize());
}
@Override
public void close() {
this.sub.unsubscribe();
this.jedis.close();
}
private static class Subscription extends JedisPubSub {
private final RedisMessenger messenger;
public Subscription(RedisMessenger messenger) {
this.messenger = messenger;
}
@Override
public void onMessage(String channel, String message) {
String[] channels = this.messenger.getChannels();
if (!channel.equals(channels[0]) && !channel.equals(channels[1])) {
return;
}
this.messenger.consumer.consumeRawIncomingMessage(message);
}
}
}

View file

@ -0,0 +1,33 @@
package dev.xhyrom.lighteco.common.messaging.type.redis;
import dev.xhyrom.lighteco.api.messenger.IncomingMessageConsumer;
import dev.xhyrom.lighteco.api.messenger.Messenger;
import dev.xhyrom.lighteco.api.messenger.MessengerProvider;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
public class RedisMessengerProvider implements MessengerProvider {
private final LightEcoPlugin plugin;
public RedisMessengerProvider(LightEcoPlugin plugin) {
this.plugin = plugin;
}
@Override
public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer consumer) {
RedisMessenger messenger = new RedisMessenger(this.plugin, consumer);
String address = this.plugin.getConfig().messaging.data.address;
String username = this.plugin.getConfig().messaging.data.username;
String password = this.plugin.getConfig().messaging.data.password;
if (password.isEmpty()) password = null;
if (username.isEmpty()) username = null;
boolean ssl = this.plugin.getConfig().messaging.data.ssl;
messenger.init(address, username, password, ssl);
return messenger;
}
}

View file

@ -4,6 +4,7 @@ import dev.xhyrom.lighteco.api.exception.CannotBeGreaterThan;
import dev.xhyrom.lighteco.api.exception.CannotBeNegative;
import dev.xhyrom.lighteco.common.api.impl.ApiUser;
import dev.xhyrom.lighteco.common.cache.RedisBackedMap;
import dev.xhyrom.lighteco.common.messaging.InternalMessagingService;
import dev.xhyrom.lighteco.common.model.currency.Currency;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import lombok.Getter;
@ -14,6 +15,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Optional;
import java.util.UUID;
@Getter
@ -50,10 +52,14 @@ public class User {
}
public void setBalance(@NonNull Currency currency, @NonNull BigDecimal balance) throws CannotBeNegative, CannotBeGreaterThan {
this.setBalance(currency, balance, false);
this.setBalance(currency, balance, false, true);
}
public void setBalance(@NonNull Currency currency, @NonNull BigDecimal balance, boolean force) throws CannotBeNegative, CannotBeGreaterThan {
this.setBalance(currency, balance, force, true);
}
public void setBalance(@NonNull Currency currency, @NonNull BigDecimal balance, boolean force, boolean publish) throws CannotBeNegative, CannotBeGreaterThan {
if (balance.compareTo(BigDecimal.ZERO) < 0) {
throw new CannotBeNegative("Balance cannot be negative");
}
@ -67,6 +73,11 @@ public class User {
if (!force)
this.setDirty(true);
if (publish) {
@NonNull Optional<InternalMessagingService> messagingService = this.plugin.getMessagingService();
messagingService.ifPresent(internalMessagingService -> internalMessagingService.pushUserUpdate(this, currency));
}
}
public void deposit(@NonNull Currency currency, @NonNull BigDecimal amount) throws CannotBeNegative, CannotBeGreaterThan {

View file

@ -6,13 +6,17 @@ import dev.xhyrom.lighteco.common.api.LightEcoApi;
import dev.xhyrom.lighteco.common.config.Config;
import dev.xhyrom.lighteco.common.dependencies.DependencyManager;
import dev.xhyrom.lighteco.common.dependencies.DependencyManagerImpl;
import dev.xhyrom.lighteco.common.messaging.MessagingFactory;
import dev.xhyrom.lighteco.common.messaging.InternalMessagingService;
import dev.xhyrom.lighteco.common.storage.Storage;
import dev.xhyrom.lighteco.common.storage.StorageFactory;
import dev.xhyrom.lighteco.common.task.UserSaveTask;
import eu.okaeri.configs.ConfigManager;
import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Getter
@ -23,6 +27,7 @@ public abstract class AbstractLightEcoPlugin implements LightEcoPlugin {
@Getter
private Storage storage;
private InternalMessagingService messagingService;
private LightEcoApi api;
private UserSaveTask userSaveTask;
@ -41,10 +46,18 @@ public abstract class AbstractLightEcoPlugin implements LightEcoPlugin {
public final void enable() {
// setup storage
StorageFactory factory = new StorageFactory(this);
this.dependencyManager.loadStorageDependencies(factory.getRequiredTypes());
StorageFactory storageFactory = new StorageFactory(this);
this.dependencyManager.loadStorageDependencies(
storageFactory.getRequiredTypes()
);
this.storage = factory.get();
MessagingFactory messagingFactory = this.getMessagingFactory();
this.dependencyManager.loadMessagingDependencies(
messagingFactory.getRequiredTypes()
);
this.storage = storageFactory.get();
this.messagingService = messagingFactory.get();
// register listeners
this.registerListeners();
@ -74,6 +87,9 @@ public abstract class AbstractLightEcoPlugin implements LightEcoPlugin {
// shutdown storage
this.storage.shutdown();
if (this.messagingService != null)
this.messagingService.shutdown();
// close isolated class loaders
this.dependencyManager.close();
}
@ -81,8 +97,14 @@ public abstract class AbstractLightEcoPlugin implements LightEcoPlugin {
protected abstract void registerListeners();
protected abstract void setupManagers();
protected abstract MessagingFactory getMessagingFactory();
protected abstract void registerApiOnPlatform(LightEco api);
protected abstract void registerPlatformHooks();
protected abstract void removePlatformHooks();
@Override
public @NonNull Optional<InternalMessagingService> getMessagingService() {
return Optional.ofNullable(this.messagingService);
}
}

View file

@ -7,10 +7,13 @@ import dev.xhyrom.lighteco.common.dependencies.DependencyManager;
import dev.xhyrom.lighteco.common.manager.command.CommandManager;
import dev.xhyrom.lighteco.common.manager.currency.CurrencyManager;
import dev.xhyrom.lighteco.common.manager.user.UserManager;
import dev.xhyrom.lighteco.common.messaging.InternalMessagingService;
import dev.xhyrom.lighteco.common.plugin.bootstrap.LightEcoBootstrap;
import dev.xhyrom.lighteco.common.storage.Storage;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
public interface LightEcoPlugin {
Platform.@NonNull Type getPlatformType();
@ -24,5 +27,7 @@ public interface LightEcoPlugin {
@NonNull DependencyManager getDependencyManager();
@NonNull Optional<InternalMessagingService> getMessagingService();
@NonNull Storage getStorage();
}

View file

@ -1,6 +1,5 @@
package dev.xhyrom.lighteco.common.storage;
import com.google.common.collect.ImmutableSet;
import dev.xhyrom.lighteco.api.storage.StorageProvider;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.provider.memory.MemoryStorageProvider;
@ -21,7 +20,7 @@ public class StorageFactory {
}
public Set<StorageType> getRequiredTypes() {
return ImmutableSet.of(this.plugin.getConfig().storage.provider);
return Set.of(this.plugin.getConfig().storage.provider);
}
public Storage get() {

View file

@ -0,0 +1,12 @@
package dev.xhyrom.lighteco.common.util.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public final class GsonProvider {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
public static Gson get() {
return GSON;
}
}

View file

@ -6,6 +6,7 @@ import dev.xhyrom.lighteco.api.platform.Platform;
import dev.xhyrom.lighteco.common.manager.command.CommandManager;
import dev.xhyrom.lighteco.common.manager.currency.StandardCurrencyManager;
import dev.xhyrom.lighteco.common.manager.user.StandardUserManager;
import dev.xhyrom.lighteco.common.messaging.MessagingFactory;
import dev.xhyrom.lighteco.common.plugin.AbstractLightEcoPlugin;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
@ -41,6 +42,11 @@ public class SpongeLightEcoPlugin extends AbstractLightEcoPlugin {
//this.contextManager = new BukkitContextManager();
}
@Override
protected MessagingFactory getMessagingFactory() {
return new MessagingFactory(this);
}
@Override
protected void registerApiOnPlatform(LightEco api) {
//this.getBootstrap().getServer().getServicesManager().register(LightEco.class, api, this.getBootstrap(), ServicePriority.Normal);