diff --git a/pom.xml b/pom.xml index 6a163a2..69ce66e 100644 --- a/pom.xml +++ b/pom.xml @@ -81,5 +81,11 @@ 1.20.2-R0.1-SNAPSHOT provided + + com.zaxxer + HikariCP + 5.1.0 + compile + diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/CloudnodeMSG.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/CloudnodeMSG.java index 8fdfe44..f17909e 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/CloudnodeMSG.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/CloudnodeMSG.java @@ -1,19 +1,31 @@ package pro.cloudnode.smp.cloudnodemsg; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import org.bukkit.entity.Player; import org.bukkit.metadata.MetadataValue; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import pro.cloudnode.smp.cloudnodemsg.command.IgnoreCommand; +import pro.cloudnode.smp.cloudnodemsg.command.MailCommand; import pro.cloudnode.smp.cloudnodemsg.command.MainCommand; import pro.cloudnode.smp.cloudnodemsg.command.MessageCommand; import pro.cloudnode.smp.cloudnodemsg.command.ReplyCommand; import pro.cloudnode.smp.cloudnodemsg.command.TeamMessageCommand; +import pro.cloudnode.smp.cloudnodemsg.command.ToggleMessageCommand; import pro.cloudnode.smp.cloudnodemsg.command.UnIgnoreCommand; import pro.cloudnode.smp.cloudnodemsg.listener.AsyncChatListener; -import pro.cloudnode.smp.cloudnodemsg.command.ToggleMessageCommand; +import pro.cloudnode.smp.cloudnodemsg.listener.PlayerJoinListener; +import java.io.InputStream; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Map; import java.util.Objects; +import java.util.logging.Level; public final class CloudnodeMSG extends JavaPlugin { public static @NotNull CloudnodeMSG getInstance() { @@ -23,6 +35,8 @@ public final class CloudnodeMSG extends JavaPlugin { public void reload() { getInstance().reloadConfig(); getInstance().config.config = getInstance().getConfig(); + setupDbSource(); + runDDL(); } @Override @@ -37,13 +51,18 @@ public void onEnable() { Objects.requireNonNull(getCommand("unignore")).setExecutor(new UnIgnoreCommand()); Objects.requireNonNull(getCommand("togglemsg")).setExecutor(new ToggleMessageCommand()); Objects.requireNonNull(getCommand("teammsg")).setExecutor(new TeamMessageCommand()); + Objects.requireNonNull(getCommand("mail")).setExecutor(new MailCommand()); getServer().getPluginManager().registerEvents(new AsyncChatListener(), this); + getServer().getPluginManager().registerEvents(new PlayerJoinListener(), this); + + minuteLoop = minuteLoop(); } @Override public void onDisable() { - // Plugin shutdown logic + if (dbSource != null) dbSource.close(); + if (minuteLoop != null) minuteLoop.cancel(); } public static boolean isVanished(final @NotNull Player player) { @@ -57,4 +76,67 @@ public static boolean isVanished(final @NotNull Player player) { public @NotNull PluginConfig config() { return config; } + + public final @NotNull HikariConfig hikariConfig = new HikariConfig(); + private @Nullable HikariDataSource dbSource; + + public @NotNull HikariDataSource db() { + assert dbSource != null; + return dbSource; + } + + private void setupDbSource() { + if (dbSource != null) dbSource.close(); + hikariConfig.setDriverClassName("org.sqlite.JDBC"); + hikariConfig.setJdbcUrl("jdbc:sqlite:" + getDataFolder().getAbsolutePath() + "/" + config().dbSqliteFile()); + + for (final @NotNull Map.Entry<@NotNull String, @NotNull String> entry : config().dbHikariProperties().entrySet()) + hikariConfig.addDataSourceProperty(entry.getKey(), entry.getValue()); + + dbSource = new HikariDataSource(hikariConfig); + } + + /** + * Run DDL script + */ + public void runDDL() { + final @NotNull String file = "ddl/sqlite.sql"; + final @NotNull String @NotNull [] queries; + try (final @Nullable InputStream inputStream = getClassLoader().getResourceAsStream(file)) { + queries = Arrays.stream( + new String(Objects.requireNonNull(inputStream).readAllBytes()).split(";") + ).map(q -> q.stripTrailing().stripIndent().replaceAll("^\\s+(?:--.+)*", "")).toArray(String[]::new); + } + catch (final @NotNull Exception exception) { + getLogger().log(Level.SEVERE, "Could not read DDL script: " + file, exception); + getServer().getPluginManager().disablePlugin(this); + return; + } + for (final @NotNull String query : queries) { + if (query.isBlank()) continue; + try (final @NotNull PreparedStatement stmt = db().getConnection().prepareStatement(query)) { + stmt.execute(); + } + catch (final @NotNull SQLException exception) { + getLogger().log(Level.SEVERE, "Could not execute DDL query: " + query, exception); + getServer().getPluginManager().disablePlugin(this); + return; + } + } + getLogger().info("Database successfully initialised with DDL"); + } + + /** + * Run code asynchronously + */ + public static void runAsync(final @NotNull Runnable runnable) { + getInstance().getServer().getScheduler().runTaskAsynchronously(getInstance(), runnable); + } + + private @NotNull BukkitTask minuteLoop() { + return getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { + if ((System.currentTimeMillis() / 1000) % config().mailNotifyInterval() == 0) Mail.notifyUnread(); + }, 0, 20 * 60); + } + private @Nullable BukkitTask minuteLoop; } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/Mail.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/Mail.java new file mode 100644 index 0000000..296306f --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/Mail.java @@ -0,0 +1,119 @@ +package pro.cloudnode.smp.cloudnodemsg; + +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; + +public final class Mail { + public final @NotNull UUID id; + public final @NotNull OfflinePlayer sender; + public final @NotNull OfflinePlayer recipient; + public final @NotNull String message; + public boolean seen; + + public boolean starred; + + public Mail(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull String message) { + this.id = UUID.randomUUID(); + this.sender = sender; + this.recipient = recipient; + this.message = message; + this.seen = false; + this.starred = false; + } + + public Mail(final @NotNull ResultSet rs) throws SQLException { + final @NotNull Server server = CloudnodeMSG.getInstance().getServer(); + + this.id = UUID.fromString(rs.getString("id")); + this.sender = server.getOfflinePlayer(UUID.fromString(rs.getString("sender"))); + this.recipient = server.getOfflinePlayer(UUID.fromString(rs.getString("recipient"))); + this.message = rs.getString("message"); + this.seen = rs.getBoolean("seen"); + this.starred = rs.getBoolean("starred"); + } + + public void update() { + try (final @NotNull PreparedStatement stmt = CloudnodeMSG.getInstance().db().getConnection().prepareStatement("UPDATE `cloudnodemsg_mail` SET `seen` = ?, `starred` = ? WHERE `id` = ?")) { + stmt.setBoolean(1, seen); + stmt.setBoolean(2, starred); + stmt.setString(3, id.toString()); + stmt.executeUpdate(); + } + catch (final @NotNull SQLException exception) { + CloudnodeMSG.getInstance().getLogger().log(Level.SEVERE, "Could not update mail: " + id, exception); + } + } + + public void delete() { + try (final @NotNull PreparedStatement stmt = CloudnodeMSG.getInstance().db().getConnection().prepareStatement("DELETE FROM `cloudnodemsg_mail` WHERE `id` = ?")) { + stmt.setString(1, id.toString()); + stmt.executeUpdate(); + } + catch (final @NotNull SQLException exception) { + CloudnodeMSG.getInstance().getLogger().log(Level.SEVERE, "Could not delete mail: " + id, exception); + } + } + + public void insert() { + try (final @NotNull PreparedStatement stmt = CloudnodeMSG.getInstance().db().getConnection().prepareStatement("INSERT INTO `cloudnodemsg_mail` (`id`, `sender`, `recipient`, `message`, `seen`, `starred`) VALUES (?, ?, ?, ?, ?, ?)")) { + stmt.setString(1, id.toString()); + stmt.setString(2, sender.getUniqueId().toString()); + stmt.setString(3, recipient.getUniqueId().toString()); + stmt.setString(4, message); + stmt.setBoolean(5, seen); + stmt.setBoolean(6, starred); + stmt.executeUpdate(); + } + catch (final @NotNull SQLException exception) { + CloudnodeMSG.getInstance().getLogger().log(Level.SEVERE, "Could not insert mail: " + id, exception); + } + } + + public static @NotNull Optional<@NotNull Mail> get(final @NotNull UUID id) { + try (final @NotNull PreparedStatement stmt = CloudnodeMSG.getInstance().db().getConnection().prepareStatement("SELECT * FROM `cloudnodemsg_mail` WHERE `id` = ? LIMIT 1")) { + stmt.setString(1, id.toString()); + final @NotNull ResultSet rs = stmt.executeQuery(); + if (!rs.next()) return Optional.empty(); + return Optional.of(new Mail(rs)); + } + catch (final @NotNull SQLException exception) { + CloudnodeMSG.getInstance().getLogger().log(Level.SEVERE, "Could not get mail: " + id, exception); + return Optional.empty(); + } + } + + public static int unread(final @NotNull OfflinePlayer player) { + try (final @NotNull PreparedStatement stmt = CloudnodeMSG.getInstance().db().getConnection().prepareStatement("SELECT COUNT(`id`) as `n` FROM `cloudnodemsg_mail` WHERE `recipient` = ? AND `seen` = 0")) { + stmt.setString(1, player.getUniqueId().toString()); + final @NotNull ResultSet rs = stmt.executeQuery(); + if (!rs.next()) return 0; + return rs.getInt("n"); + } + catch (final @NotNull SQLException exception) { + CloudnodeMSG.getInstance().getLogger().log(Level.SEVERE, "Could not get unread mails: " + player.getName(), exception); + return 0; + } + } + + /** + * Notify online players for their unread mail + */ + public static void notifyUnread() { + CloudnodeMSG.runAsync(() -> { + for (final @NotNull Player player : CloudnodeMSG.getInstance().getServer().getOnlinePlayers()) { + if (!player.hasPermission(Permission.MAIL)) continue; + final int unread = Mail.unread(player); + if (unread > 0) player.sendMessage(CloudnodeMSG.getInstance().config().mailNotify(unread)); + } + }); + } +} diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/Message.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/Message.java index b206444..e41af5c 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/Message.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/Message.java @@ -36,24 +36,11 @@ public Message(@NotNull OfflinePlayer sender, @NotNull OfflinePlayer recipient, this(sender, recipient, Component.text(message)); } - private @NotNull String playerOrServerUsername(final @NotNull OfflinePlayer player) throws InvalidPlayerError { - if (player.getUniqueId().equals(console.getUniqueId())) - return CloudnodeMSG.getInstance().config().consoleName(); - else { - final @NotNull Optional<@NotNull String> name = Optional.ofNullable(player.getName()); - if (name.isEmpty()) throw new InvalidPlayerError(); - else return name.get(); - } - } - public void send() throws InvalidPlayerError { send(Context.REGULAR); } public void send(final @NotNull Context context) throws InvalidPlayerError { - final @NotNull String senderUsername = playerOrServerUsername(this.sender); - final @NotNull String recipientUsername = playerOrServerUsername(this.recipient); - final @NotNull Optional<@NotNull Player> senderPlayer = Optional.ofNullable(this.sender.getPlayer()); final @NotNull Optional<@NotNull Player> recipientPlayer = Optional.ofNullable(this.recipient.getPlayer()); @@ -61,29 +48,28 @@ public void send(final @NotNull Context context) throws InvalidPlayerError { if (context == Context.CHANNEL) { final @NotNull Player player = Objects.requireNonNull(sender.getPlayer()); Message.exitChannel(player); - new ChannelOfflineError(player.getName(), Optional.ofNullable(recipient.getName()) - .orElse("Unknown Player")).send(player); + new ChannelOfflineError(sender, recipient).send(player); } else { final @NotNull Audience senderAudience = senderPlayer.isPresent() ? senderPlayer.get() : CloudnodeMSG.getInstance().getServer().getConsoleSender(); - if (context == Context.REPLY) new ReplyOfflineError(recipientUsername).send(senderAudience); - else new PlayerNotFoundError(recipientUsername).send(senderAudience); + if (context == Context.REPLY) new ReplyOfflineError(recipient).send(senderAudience); + else new PlayerNotFoundError(name(this.recipient)).send(senderAudience); } return; } if (recipientPlayer.isPresent() && senderPlayer.isPresent() && !Message.isIncomingEnabled(recipientPlayer.get()) && !senderPlayer .get().hasPermission(Permission.TOGGLE_BYPASS)) { - new PlayerHasIncomingDisabledError(recipientPlayer.get().getName()).send(senderPlayer.get()); + new PlayerHasIncomingDisabledError(recipientPlayer.get()).send(senderPlayer.get()); return; } sendSpyMessage(sender, recipient, message); - sendMessage(sender, CloudnodeMSG.getInstance().config().outgoing(senderUsername, recipientUsername, message)); + sendMessage(sender, CloudnodeMSG.getInstance().config().outgoing(sender, recipient, message)); if ((recipientPlayer.isPresent() && Message.isIgnored(recipientPlayer.get(), sender)) && (senderPlayer.isPresent() && !senderPlayer .get().hasPermission(Permission.IGNORE_BYPASS))) return; sendMessage(recipient, CloudnodeMSG.getInstance().config() - .incoming(senderUsername, recipientUsername, message)); + .incoming(sender, recipient, message)); if (sender.getUniqueId().equals(console.getUniqueId()) || (senderPlayer.isPresent() && !Message.hasChannel(senderPlayer.get(), recipient))) setReplyTo(sender, recipient); @@ -98,6 +84,12 @@ public void send(final @NotNull Context context) throws InvalidPlayerError { return executor instanceof final @NotNull Player player ? player : console; } + public static @NotNull String name(final @NotNull OfflinePlayer player) { + if (player.getUniqueId().equals(console.getUniqueId())) return CloudnodeMSG.getInstance().config().consoleName(); + final @NotNull Optional<@NotNull String> name = Optional.ofNullable(player.getName()); + return name.orElse(CloudnodeMSG.getInstance().config().unknownName()); + } + public static void sendMessage(final @NotNull OfflinePlayer recipient, final @NotNull Component message) { if (recipient.getUniqueId() == console.getUniqueId()) CloudnodeMSG.getInstance().getServer().getConsoleSender().sendMessage(message); @@ -108,19 +100,16 @@ public static void sendMessage(final @NotNull OfflinePlayer recipient, final @No * Send social spy to online players with permission */ public static void sendSpyMessage(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull Component message) { - final @NotNull String senderName = sender.getUniqueId().equals(console.getUniqueId()) ? CloudnodeMSG - .getInstance().config().consoleName() : Optional.ofNullable(sender.getName()).orElse("Unknown Player"); - final @NotNull String recipientName = recipient.getUniqueId().equals(console.getUniqueId()) ? CloudnodeMSG - .getInstance().config().consoleName() : Optional.ofNullable(recipient.getName()) - .orElse("Unknown Player"); for (final @NotNull Player player : CloudnodeMSG.getInstance().getServer().getOnlinePlayers()) { - if (!player.hasPermission(Permission.SPY) || player.getUniqueId().equals(sender.getUniqueId()) || player - .getUniqueId().equals(recipient.getUniqueId())) continue; - sendMessage(player, CloudnodeMSG.getInstance().config().spy(senderName, recipientName, message)); + if ( + !player.hasPermission(Permission.SPY) + || player.getUniqueId().equals(sender.getUniqueId()) + || player.getUniqueId().equals(recipient.getUniqueId()) + ) continue; + sendMessage(player, CloudnodeMSG.getInstance().config().spy(sender, recipient, message)); } - if (!sender.getUniqueId().equals(console.getUniqueId()) && !recipient.getUniqueId() - .equals(console.getUniqueId())) - sendMessage(console, CloudnodeMSG.getInstance().config().spy(senderName, recipientName, message)); + if (!sender.getUniqueId().equals(console.getUniqueId()) && !recipient.getUniqueId().equals(console.getUniqueId())) + sendMessage(console, CloudnodeMSG.getInstance().config().spy(sender, recipient, message)); } private static @Nullable UUID consoleReply; @@ -143,12 +132,6 @@ else if (sender.isOnline()) Objects.requireNonNull(sender.getPlayer()).getPersis return Optional.empty(); } - public static void removeReplyTo(final @NotNull OfflinePlayer player) { - if (player.getUniqueId().equals(console.getUniqueId())) consoleReply = null; - else if (player.isOnline()) - Objects.requireNonNull(player.getPlayer()).getPersistentDataContainer().remove(REPLY_TO); - } - public static final @NotNull NamespacedKey IGNORED_PLAYERS = new NamespacedKey(CloudnodeMSG.getInstance(), "ignored"); public static final @NotNull NamespacedKey CHANNEL_RECIPIENT = new NamespacedKey(CloudnodeMSG.getInstance(), "channel-recipient"); public static final @NotNull NamespacedKey CHANNEL_TEAM = new NamespacedKey(CloudnodeMSG.getInstance(), "channel-team"); diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/Permission.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/Permission.java index 6b4bcb8..a43f29a 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/Permission.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/Permission.java @@ -52,4 +52,9 @@ public final class Permission { * Allows to see the private messages of other players */ public final static @NotNull String SPY = "cloudnodemsg.spy"; + + /** + * Allows using the /mail command + */ + public final static @NotNull String MAIL = "cloudnodemsg.mail"; } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/PluginConfig.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/PluginConfig.java index 1341017..587aefc 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/PluginConfig.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/PluginConfig.java @@ -2,11 +2,16 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.OfflinePlayer; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.scoreboard.Team; import org.jetbrains.annotations.NotNull; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; public final class PluginConfig { @@ -16,6 +21,31 @@ public PluginConfig(final @NotNull FileConfiguration config) { this.config = config; } + /** + * Database file name (in the plugin's folder) + *

If the file does not exist, it will be created

+ */ + public @NotNull String dbSqliteFile() { + return Objects.requireNonNull(config.getString("db.sqlite.file")); + } + + /** + * Advanced DB configuration / HikariCP properties + *

Only change if you know what you're doing; you can add or remove any property you want

+ */ + public @NotNull HashMap<@NotNull String, @NotNull String> dbHikariProperties() { + final @NotNull List<@NotNull Map> mapList = config.getMapList("db.hikaricp"); + final @NotNull HashMap<@NotNull String, @NotNull String> properties = new HashMap<>(mapList.size()); + + for (final @NotNull Map map : mapList) + if ( + map.get("name") instanceof final @NotNull String name + && map.get("value") instanceof final @NotNull String value + ) properties.put(name, value); + + return properties; + } + /** * Incoming message format (recipient's point of view) *

Placeholders:

@@ -25,14 +55,14 @@ public PluginConfig(final @NotNull FileConfiguration config) { *
  • {@code } - the message text
  • * * - * @param sender The username of the message sender - * @param recipient The username of the message recipient + * @param sender The message sender + * @param recipient The message recipient * @param message The message text */ - public @NotNull Component incoming(final @NotNull String sender, final @NotNull String recipient, final @NotNull Component message) { + public @NotNull Component incoming(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull Component message) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("incoming")) - .replace("", sender) - .replace("", recipient), + .replace("", Message.name(sender)) + .replace("", Message.name(recipient)), Placeholder.component("message", message) ); } @@ -41,19 +71,19 @@ public PluginConfig(final @NotNull FileConfiguration config) { * Outgoing message format (sender's point of view) *

    Placeholders:

    *
      - *
    • {@code } - the username of the message sender
    • + *
    • {@code } - the message sender
    • *
    • {@code } - the username of the message recipient
    • *
    • {@code } - the message text
    • *
    * - * @param sender The username of the message sender - * @param recipient The username of the message recipient + * @param sender The message sender + * @param recipient The message recipient * @param message The message text */ - public @NotNull Component outgoing(final @NotNull String sender, final @NotNull String recipient, final @NotNull Component message) { + public @NotNull Component outgoing(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull Component message) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("outgoing")) - .replace("", sender) - .replace("", recipient), + .replace("", Message.name(sender)) + .replace("", Message.name(recipient)), Placeholder.component("message", message) ); } @@ -72,9 +102,9 @@ public PluginConfig(final @NotNull FileConfiguration config) { * @param team The team * @param message The message text */ - public @NotNull Component team(final @NotNull String sender, final @NotNull Team team, final @NotNull Component message) { + public @NotNull Component team(final @NotNull OfflinePlayer sender, final @NotNull Team team, final @NotNull Component message) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("team")) - .replace("", sender), + .replace("", Message.name(sender)), Placeholder.component("team", team.displayName()), Placeholder.component("message", message) ); @@ -89,14 +119,14 @@ public PluginConfig(final @NotNull FileConfiguration config) { *
  • {@code } - the message text
  • * * - * @param sender The username of the message sender - * @param recipient The username of the message recipient + * @param sender The message sender + * @param recipient The message recipient * @param message The message text */ - public @NotNull Component spy(final @NotNull String sender, final @NotNull String recipient, final @NotNull Component message) { + public @NotNull Component spy(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull Component message) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("spy")) - .replace("", sender) - .replace("", recipient), + .replace("", Message.name(sender)) + .replace("", Message.name(recipient)), Placeholder.component("message", message) ); } @@ -114,9 +144,9 @@ public PluginConfig(final @NotNull FileConfiguration config) { * @param team The team * @param message The message text */ - public @NotNull Component teamSpy(final @NotNull String sender, final @NotNull Team team, final @NotNull Component message) { + public @NotNull Component teamSpy(final @NotNull OfflinePlayer sender, final @NotNull Team team, final @NotNull Component message) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("team-spy")) - .replace("", sender), + .replace("", Message.name(sender)), Placeholder.component("team", team.displayName()), Placeholder.component("message", message) ); @@ -127,11 +157,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the username of the player
    * - * @param player The username of the player + * @param player The player */ - public @NotNull Component ignored(final @NotNull String player) { + public @NotNull Component ignored(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("ignored")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -140,11 +170,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the username of the player
    * - * @param player The username of the player + * @param player The player */ - public @NotNull Component unignored(final @NotNull String player) { + public @NotNull Component unignored(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("unignored")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -157,14 +187,14 @@ public PluginConfig(final @NotNull FileConfiguration config) { *
  • {@code } - the command used, e.g. `msg`, `dm`, etc.
  • * * - * @param sender The username of the message sender - * @param recipient The username of the message recipient + * @param sender The message sender + * @param recipient The message recipient * @param command The command used, e.g. `msg`, `dm`, etc. */ - public @NotNull Component channelCreated(final @NotNull String sender, final @NotNull String recipient, final @NotNull String command) { + public @NotNull Component channelCreated(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull String command) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("channel.created")) - .replace("", sender) - .replace("", recipient) + .replace("", Message.name(sender)) + .replace("", Message.name(recipient)) .replace("", command)); } @@ -177,14 +207,14 @@ public PluginConfig(final @NotNull FileConfiguration config) { *
  • {@code } - the command used, e.g. `msg`, `dm`, etc.
  • * * - * @param sender The username of the message sender - * @param recipient The username of the message recipient + * @param sender The message sender + * @param recipient The message recipient * @param command The command used, e.g. `msg`, `dm`, etc. */ - public @NotNull Component channelClosed(final @NotNull String sender, final @NotNull String recipient, final @NotNull String command) { + public @NotNull Component channelClosed(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient, final @NotNull String command) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("channel.closed")) - .replace("", sender) - .replace("", recipient) + .replace("", Message.name(sender)) + .replace("", Message.name(recipient)) .replace("", command)); } @@ -196,13 +226,13 @@ public PluginConfig(final @NotNull FileConfiguration config) { *
  • {@code } - the username of the message recipient
  • * * - * @param sender The username of the message sender - * @param recipient The username of the message recipient + * @param sender The message sender + * @param recipient The message recipient */ - public @NotNull Component channelOffline(final @NotNull String sender, final @NotNull String recipient) { + public @NotNull Component channelOffline(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("channel.offline")) - .replace("", sender) - .replace("", recipient)); + .replace("", Message.name(sender)) + .replace("", Message.name(recipient))); } /** @@ -216,9 +246,9 @@ public PluginConfig(final @NotNull FileConfiguration config) { * @param sender The username of the message sender * @param team The team@ */ - public @NotNull Component channelTeamCreated(final @NotNull String sender, final @NotNull Team team) { + public @NotNull Component channelTeamCreated(final @NotNull OfflinePlayer sender, final @NotNull Team team) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("channel.team-created")) - .replace("", sender), + .replace("", Message.name(sender)), Placeholder.component("team", team.displayName())); } @@ -233,9 +263,9 @@ public PluginConfig(final @NotNull FileConfiguration config) { * @param sender The username of the message sender * @param team The team */ - public @NotNull Component channelTeamClosed(final @NotNull String sender, final @NotNull Team team) { + public @NotNull Component channelTeamClosed(final @NotNull OfflinePlayer sender, final @NotNull Team team) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("channel.team-closed")) - .replace("", sender), + .replace("", Message.name(sender)), Placeholder.component("team", team.displayName())); } @@ -271,6 +301,13 @@ public PluginConfig(final @NotNull FileConfiguration config) { return Objects.requireNonNull(config.getString("console-name")); } + /** + * Name for player when name not found (usually very unlikely to happen) + */ + public @NotNull String unknownName() { + return Objects.requireNonNull(config.getString("unknown-name")); + } + public @NotNull Component toggleDisable() { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("toggle.disable.message"))); } @@ -280,11 +317,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player the player's username + * @param player the player */ - public @NotNull Component toggleDisableOther(final @NotNull String player) { + public @NotNull Component toggleDisableOther(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("toggle.disable.other")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -297,14 +334,89 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player the player's username + * @param player the player */ - public @NotNull Component toggleEnableOther(final @NotNull String player) { + public @NotNull Component toggleEnableOther(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("toggle.enable.other")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } + /** + * Should players be notified for unread mail on login? + */ + public boolean mailNotifyOnLogin() { + return config.getBoolean("mail.notify-on-login"); + } + + /** + * Interval (in minutes) to notify players of unread mail. Set to 0 to disable. + */ + public int mailNotifyInterval() { + return config.getInt("mail.notify-interval"); + } + + /** + * Unread mail notification message + *

    Placeholders:

    + *
    • {@code } - the number of unread mail messages
    + * + * @param unread the number of unread mail messages + */ + public @NotNull Component mailNotify(final int unread) { + return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("mail.notify")), + Placeholder.unparsed("unread", String.valueOf(unread)) + ); + } + + /** + * Successfully sent mail + *

    Placeholders:

    + *
      + *
    • {@code } - the username of the sender
    • + *
    • {@code } - the username of the recipient
    • + *
    • {@code } - the message
    • + *
    • {@code } - whether the mail was read/opened by the recipient (see: Insert a choice)
    • + *
    • {@code } - whether the recipient has starred the mail (see: Insert a choice)
    • + *
    • {@code } - the ID of the mail
    • + *
    + * + * @param mail the mail + */ + public @NotNull Component mailSent(final @NotNull Mail mail) { + return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("mail.sent")) + .replace("", mail.id.toString()) + .replace("", Message.name(mail.sender)) + .replace("", Message.name(mail.recipient)), + Formatter.booleanChoice("seen", mail.seen), + Formatter.booleanChoice("starred", mail.starred) + ).replaceText(configurer -> configurer.matchLiteral("").replacement(Component.text(mail.message))); + } + + /** + * Received mail while online + *

    Placeholders:

    + *
      + *
    • {@code } - the username of the sender
    • + *
    • {@code } - the username of the recipient
    • + *
    • {@code } - the message
    • + *
    • {@code } - whether the mail was read/opened by the recipient (see: Insert a choice)
    • + *
    • {@code } - whether the recipient has starred the mail (see: Insert a choice)
    • + *
    • {@code } - the ID of the mail
    • + *
    + * + * @param mail the mail + */ + public @NotNull Component mailReceived(final @NotNull Mail mail) { + return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("mail.received")) + .replace("", mail.id.toString()) + .replace("", Message.name(mail.sender)) + .replace("", Message.name(mail.recipient)), + Formatter.booleanChoice("seen", mail.seen), + Formatter.booleanChoice("starred", mail.starred) + ).replaceText(configurer -> configurer.matchLiteral("").replacement(Component.text(mail.message))); + } + /** * No permission */ @@ -324,7 +436,7 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player The player's username + * @param player The player */ public @NotNull Component playerNotFound(final @NotNull String player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("errors.player-not-found")), @@ -345,11 +457,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player The player's username + * @param player The player */ - public @NotNull Component replyOffline(final @NotNull String player) { + public @NotNull Component replyOffline(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("errors.reply-offline")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -365,11 +477,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player The player's username + * @param player The player */ - public @NotNull Component notIgnored(final @NotNull String player) { + public @NotNull Component notIgnored(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("errors.not-ignored")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -378,11 +490,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player The player's username + * @param player The player */ - public @NotNull Component cannotIgnore(final @NotNull String player) { + public @NotNull Component cannotIgnore(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("errors.cannot-ignore")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -398,11 +510,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player The player's username + * @param player The player */ - public @NotNull Component neverJoined(final @NotNull String player) { + public @NotNull Component neverJoined(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("errors.never-joined")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } @@ -411,11 +523,11 @@ public PluginConfig(final @NotNull FileConfiguration config) { *

    Placeholders:

    *
    • {@code } - the player's username
    * - * @param player The player's username + * @param player The player */ - public @NotNull Component incomingDisabled(final @NotNull String player) { + public @NotNull Component incomingDisabled(final @NotNull OfflinePlayer player) { return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("errors.incoming-disabled")), - Placeholder.unparsed("player", player) + Placeholder.unparsed("player", Message.name(player)) ); } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/Command.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/Command.java index c168fd9..d9c7ca4 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/Command.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/Command.java @@ -6,8 +6,11 @@ import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; +import java.util.List; + public abstract class Command implements TabCompleter, CommandExecutor { public static boolean sendMessage(final @NotNull Audience recipient, final @NotNull Component message) { recipient.sendMessage(message); @@ -15,12 +18,18 @@ public static boolean sendMessage(final @NotNull Audience recipient, final @NotN } public abstract boolean run(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args); + public abstract @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args); @Override public final boolean onCommand(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, @NotNull String @NotNull [] args) { - CloudnodeMSG.getInstance().getServer().getScheduler().runTaskAsynchronously(CloudnodeMSG.getInstance(), () -> { + CloudnodeMSG.runAsync(() -> { final boolean ignored = run(sender, label, args); }); return true; } + + @Override + public final @Nullable List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + return tab(sender, label, args); + } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/IgnoreCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/IgnoreCommand.java index c33b8a9..e7de186 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/IgnoreCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/IgnoreCommand.java @@ -6,17 +6,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; +import pro.cloudnode.smp.cloudnodemsg.Message; import pro.cloudnode.smp.cloudnodemsg.Permission; import pro.cloudnode.smp.cloudnodemsg.error.CannotIgnoreError; import pro.cloudnode.smp.cloudnodemsg.error.NeverJoinedError; import pro.cloudnode.smp.cloudnodemsg.error.NoPermissionError; import pro.cloudnode.smp.cloudnodemsg.error.NotPlayerError; -import pro.cloudnode.smp.cloudnodemsg.Message; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; public final class IgnoreCommand extends Command { public static final @NotNull String usage = ""; @@ -33,23 +32,19 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la } public static boolean ignore(final @NotNull Player player, final @NotNull OfflinePlayer target) { - if (target.isOnline() && Objects.requireNonNull(target.getPlayer()).hasPermission(Permission.IGNORE_BYPASS)) - return new CannotIgnoreError(Optional.ofNullable(target.getName()).orElse("Unknown Player")).send(player); - if (!target.isOnline() && !target.hasPlayedBefore()) - return new NeverJoinedError(Optional.ofNullable(target.getName()).orElse("Unknown Player")).send(player); + if (target.isOnline() && Objects.requireNonNull(target.getPlayer()).hasPermission(Permission.IGNORE_BYPASS)) return new CannotIgnoreError(target).send(player); + if (!target.isOnline() && !target.hasPlayedBefore()) return new NeverJoinedError(target).send(player); Message.ignore(player, target); - return sendMessage(player, CloudnodeMSG.getInstance().config() - .ignored(Optional.ofNullable(target.getName()).orElse("Unknown Player"))); + return sendMessage(player, CloudnodeMSG.getInstance().config().ignored(target)); } public static boolean unignore(final @NotNull Player player, final @NotNull OfflinePlayer target) { Message.unignore(player, target); - return sendMessage(player, CloudnodeMSG.getInstance().config() - .unignored(Optional.ofNullable(target.getName()).orElse("Unknown Player"))); + return sendMessage(player, CloudnodeMSG.getInstance().config().unignored(target)); } @Override - public @Nullable List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + public @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { if (args.length == 1 && sender.hasPermission(Permission.IGNORE) && sender instanceof Player) return null; return new ArrayList<>(); } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MailCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MailCommand.java new file mode 100644 index 0000000..8bf283c --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MailCommand.java @@ -0,0 +1,56 @@ +package pro.cloudnode.smp.cloudnodemsg.command; + +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; +import pro.cloudnode.smp.cloudnodemsg.Mail; +import pro.cloudnode.smp.cloudnodemsg.Message; +import pro.cloudnode.smp.cloudnodemsg.Permission; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public final class MailCommand extends Command { + + @Override + public boolean run(@NotNull CommandSender sender, @NotNull String label, @NotNull String @NotNull [] args) { + if (!sender.hasPermission(Permission.MAIL)) return sendMessage(sender, CloudnodeMSG.getInstance().config().noPermission()); + if (args.length == 0) return help(sender); + final @NotNull String @NotNull [] subCommandArgs = Arrays.copyOfRange(args, 1, args.length); + final @NotNull String subCommandLabel = label + " " + args[0]; + return switch (args[0]) { + case "send", "new", "to" -> send(sender, subCommandLabel, subCommandArgs); + default -> help(sender); + }; + } + + public static boolean help(final @NotNull CommandSender sender) { + // TODO + return false; + } + + public static boolean send(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String[] args) { + if (args.length == 0) return sendMessage(sender, CloudnodeMSG.getInstance().config().usage(label, " ")); + if (args.length == 1) return sendMessage(sender, CloudnodeMSG.getInstance().config().usage(label, args[0] + " ")); + final @NotNull String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + final @NotNull OfflinePlayer recipient = CloudnodeMSG.getInstance().getServer().getOfflinePlayer(args[0]); + if (!recipient.hasPlayedBefore()) return sendMessage(sender, CloudnodeMSG.getInstance().config().playerNotFound(Message.name(recipient))); + final @NotNull Mail mail = new Mail(Message.offlinePlayer(sender), recipient, message); + mail.insert(); + final @NotNull OfflinePlayer senderOfflinePlayer = Message.offlinePlayer(sender); + final @NotNull Optional<@NotNull Player> senderPlayer = Optional.ofNullable(senderOfflinePlayer.getPlayer()); + final @NotNull Optional<@NotNull Player> recipientPlayer = Optional.ofNullable(recipient.getPlayer()); + if (recipientPlayer.isPresent() && (!Message.isIgnored(recipientPlayer.get(), senderOfflinePlayer) || senderPlayer.isEmpty() || senderPlayer.get().hasPermission(Permission.IGNORE_BYPASS))) + sendMessage(recipientPlayer.get(), CloudnodeMSG.getInstance().config().mailReceived(mail)); + return sendMessage(sender, CloudnodeMSG.getInstance().config().mailSent(mail)); + } + + @Override + public @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { + return null; + } +} diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MainCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MainCommand.java index 186a09c..ce0d0c7 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MainCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MainCommand.java @@ -37,7 +37,7 @@ private boolean reload(final @NotNull CommandSender sender) { } @Override - public @NotNull List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + public @NotNull List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { final @NotNull List<@NotNull String> completions = new ArrayList<>(); if (args.length == 1) if (sender.hasPermission(Permission.RELOAD) && "reload".startsWith(args[0].toLowerCase())) completions.add("reload"); diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MessageCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MessageCommand.java index 2ce01de..5dc026b 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MessageCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/MessageCommand.java @@ -41,13 +41,13 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la final @NotNull OfflinePlayer recipientOffline = CloudnodeMSG.getInstance().getServer().getOfflinePlayer(args[0]); if (Message.getChannel(player).map(r -> r.getUniqueId().equals(recipientOffline.getUniqueId())).orElse(false)) { Message.exitChannel(player); - return sendMessage(player, CloudnodeMSG.getInstance().config().channelClosed(player.getName(), Optional.ofNullable(recipientOffline.getName()).orElse("Unknown Player"), label)); + return sendMessage(player, CloudnodeMSG.getInstance().config().channelClosed(player, recipientOffline, label)); } } if (recipient.isEmpty() || (CloudnodeMSG.isVanished(recipient.get()) && !player.hasPermission(Permission.SEND_VANISHED))) return new PlayerNotFoundError(args[0]).send(player); - if (!Message.isIncomingEnabled(recipient.get())) return new PlayerHasIncomingDisabledError(recipient.get().getName()).send(player); + if (!Message.isIncomingEnabled(recipient.get())) return new PlayerHasIncomingDisabledError(recipient.get()).send(player); Message.createChannel(player, recipient.get()); - return sendMessage(player, CloudnodeMSG.getInstance().config().channelCreated(player.getName(), recipient.get().getName(), label)); + return sendMessage(player, CloudnodeMSG.getInstance().config().channelCreated(player, recipient.get(), label)); } try { @@ -61,7 +61,7 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la } @Override - public @Nullable List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + public @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { if (!sender.hasPermission(Permission.USE)) return new ArrayList<>(); // `null` works for list of players if (args.length == 1) return null; diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ReplyCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ReplyCommand.java index a7757c5..2b7c353 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ReplyCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ReplyCommand.java @@ -35,7 +35,7 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la } @Override - public @NotNull List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + public @NotNull List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { return new ArrayList<>(); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/TeamMessageCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/TeamMessageCommand.java index cea49bf..16aedfb 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/TeamMessageCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/TeamMessageCommand.java @@ -28,19 +28,19 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la if (Message.hasTeamChannel(player)) { Message.exitTeamChannel(player); return sendMessage(player, CloudnodeMSG.getInstance().config() - .channelTeamClosed(player.getName(), team.get())); + .channelTeamClosed(player, team.get())); } else { Message.createTeamChannel(player); return sendMessage(player, CloudnodeMSG.getInstance().config() - .channelTeamCreated(player.getName(), team.get())); + .channelTeamCreated(player, team.get())); } } return sendTeamMessage(player, team.get(), Component.text(String.join(" ", args))); } @Override - public @Nullable List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final org.bukkit.command.@NotNull Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + public @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { return new ArrayList<>(); } @@ -52,12 +52,12 @@ public static boolean sendTeamMessage(final @NotNull Player sender, final @NotNu if (Message.isIgnored(player, sender)) continue; if (Optional.ofNullable(player.getScoreboard().getPlayerTeam(player)).map(t -> t.equals(team)) .orElse(false)) - sendMessage(player, CloudnodeMSG.getInstance().config().team(sender.getName(), team, message)); + sendMessage(player, CloudnodeMSG.getInstance().config().team(sender, team, message)); else if (player.hasPermission(Permission.SPY)) - sendMessage(player, CloudnodeMSG.getInstance().config().teamSpy(sender.getName(), team, message)); + sendMessage(player, CloudnodeMSG.getInstance().config().teamSpy(sender, team, message)); } sender.getServer().getConsoleSender() - .sendMessage(CloudnodeMSG.getInstance().config().teamSpy(sender.getName(), team, message)); + .sendMessage(CloudnodeMSG.getInstance().config().teamSpy(sender, team, message)); return true; } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ToggleMessageCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ToggleMessageCommand.java index 6dbbd63..2652bfb 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ToggleMessageCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/ToggleMessageCommand.java @@ -6,15 +6,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; +import pro.cloudnode.smp.cloudnodemsg.Message; import pro.cloudnode.smp.cloudnodemsg.Permission; import pro.cloudnode.smp.cloudnodemsg.error.NeverJoinedError; import pro.cloudnode.smp.cloudnodemsg.error.NoPermissionError; import pro.cloudnode.smp.cloudnodemsg.error.NotPlayerError; -import pro.cloudnode.smp.cloudnodemsg.Message; import java.util.ArrayList; import java.util.List; -import java.util.Optional; public final class ToggleMessageCommand extends Command { @Override @@ -24,18 +23,16 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la if (args.length == 1) { final @NotNull OfflinePlayer recipient = CloudnodeMSG.getInstance().getServer().getOfflinePlayer(args[0]); - if (recipient.getPlayer() == null) return new NeverJoinedError(Optional.ofNullable(recipient.getName()) - .orElse("Unknown Player")).send(sender); + if (recipient.getPlayer() == null) + return new NeverJoinedError(recipient).send(sender); if (Message.isIncomingEnabled(recipient.getPlayer())) { Message.incomingDisable(recipient.getPlayer()); - return sendMessage(sender, CloudnodeMSG.getInstance().config() - .toggleDisableOther(Optional.of(recipient.getPlayer().getName()).orElse("Unknown Player"))); + return sendMessage(sender, CloudnodeMSG.getInstance().config().toggleDisableOther(recipient)); } Message.incomingEnable(recipient.getPlayer()); - return sendMessage(sender, CloudnodeMSG.getInstance().config() - .toggleEnableOther(Optional.of(recipient.getPlayer().getName()).orElse("Unknown Player"))); + return sendMessage(sender, CloudnodeMSG.getInstance().config().toggleEnableOther(recipient)); } if (!(sender instanceof final @NotNull Player player)) return new NotPlayerError().send(sender); @@ -49,8 +46,9 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la } @Override - public @Nullable List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final org.bukkit.command.@NotNull Command command, final @NotNull String s, final @NotNull String @NotNull [] strings) { - if (sender.hasPermission(Permission.TOGGLE_OTHER)) return null; + public @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { + if (sender.hasPermission(Permission.TOGGLE_OTHER)) + return null; return new ArrayList<>(); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/UnIgnoreCommand.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/UnIgnoreCommand.java index d165c38..6e5104b 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/UnIgnoreCommand.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/command/UnIgnoreCommand.java @@ -29,11 +29,11 @@ public boolean run(final @NotNull CommandSender sender, final @NotNull String la if (args.length == 0) return sendMessage(sender, CloudnodeMSG.getInstance().config().usage(label, usage)); final @NotNull OfflinePlayer target = CloudnodeMSG.getInstance().getServer().getOfflinePlayer(args[0]); if (Message.isIgnored(player, target)) return IgnoreCommand.unignore(player, target); - return new NotIgnoredError(args[0]).send(sender); + return new NotIgnoredError(target).send(sender); } @Override - public @Nullable List<@NotNull String> onTabComplete(final @NotNull CommandSender sender, final @NotNull org.bukkit.command.Command command, final @NotNull String label, final @NotNull String @NotNull [] args) { + public @Nullable List<@NotNull String> tab(final @NotNull CommandSender sender, final @NotNull String label, final @NotNull String @NotNull [] args) { if (args.length == 1 && sender.hasPermission(Permission.IGNORE) && sender instanceof final @NotNull Player player) { final @NotNull HashSet<@NotNull UUID> ignored = Message.getIgnored(player); final @NotNull Server server = CloudnodeMSG.getInstance().getServer(); diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/CannotIgnoreError.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/CannotIgnoreError.java index 7c8ee01..50ee0e0 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/CannotIgnoreError.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/CannotIgnoreError.java @@ -1,5 +1,6 @@ package pro.cloudnode.smp.cloudnodemsg.error; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; @@ -7,7 +8,7 @@ * Player cannot be ignored */ public final class CannotIgnoreError extends Error { - public CannotIgnoreError(final @NotNull String player) { + public CannotIgnoreError(final @NotNull OfflinePlayer player) { super(CloudnodeMSG.getInstance().config().cannotIgnore(player)); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ChannelOfflineError.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ChannelOfflineError.java index db6f776..0de6ddc 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ChannelOfflineError.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ChannelOfflineError.java @@ -1,5 +1,6 @@ package pro.cloudnode.smp.cloudnodemsg.error; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; @@ -13,7 +14,7 @@ public final class ChannelOfflineError extends Error { * @param sender The username of the message sender * @param recipient The username of the message recipient */ - public ChannelOfflineError(final @NotNull String sender, final @NotNull String recipient) { + public ChannelOfflineError(final @NotNull OfflinePlayer sender, final @NotNull OfflinePlayer recipient) { super(CloudnodeMSG.getInstance().config().channelOffline(sender, recipient)); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NeverJoinedError.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NeverJoinedError.java index e1e1780..5cc9753 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NeverJoinedError.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NeverJoinedError.java @@ -1,5 +1,6 @@ package pro.cloudnode.smp.cloudnodemsg.error; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; @@ -12,7 +13,7 @@ public final class NeverJoinedError extends Error { * * @param player the player's username */ - public NeverJoinedError(final @NotNull String player) { + public NeverJoinedError(final @NotNull OfflinePlayer player) { super(CloudnodeMSG.getInstance().config().neverJoined(player)); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NotIgnoredError.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NotIgnoredError.java index 1295628..c92e181 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NotIgnoredError.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/NotIgnoredError.java @@ -1,5 +1,6 @@ package pro.cloudnode.smp.cloudnodemsg.error; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; @@ -12,7 +13,7 @@ public final class NotIgnoredError extends Error { * * @param player the player's username */ - public NotIgnoredError(final @NotNull String player) { + public NotIgnoredError(final @NotNull OfflinePlayer player) { super(CloudnodeMSG.getInstance().config().notIgnored(player)); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/PlayerHasIncomingDisabledError.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/PlayerHasIncomingDisabledError.java index 980dd55..032ef3f 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/PlayerHasIncomingDisabledError.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/PlayerHasIncomingDisabledError.java @@ -1,10 +1,11 @@ package pro.cloudnode.smp.cloudnodemsg.error; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; public class PlayerHasIncomingDisabledError extends Error { - public PlayerHasIncomingDisabledError(final @NotNull String player) { + public PlayerHasIncomingDisabledError(final @NotNull OfflinePlayer player) { super(CloudnodeMSG.getInstance().config().incomingDisabled(player)); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ReplyOfflineError.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ReplyOfflineError.java index 3122050..6d29c90 100644 --- a/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ReplyOfflineError.java +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/error/ReplyOfflineError.java @@ -1,5 +1,6 @@ package pro.cloudnode.smp.cloudnodemsg.error; +import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; @@ -12,7 +13,7 @@ public final class ReplyOfflineError extends Error { * * @param player The player's username */ - public ReplyOfflineError(final @NotNull String player) { + public ReplyOfflineError(final @NotNull OfflinePlayer player) { super(CloudnodeMSG.getInstance().config().replyOffline(player)); } } diff --git a/src/main/java/pro/cloudnode/smp/cloudnodemsg/listener/PlayerJoinListener.java b/src/main/java/pro/cloudnode/smp/cloudnodemsg/listener/PlayerJoinListener.java new file mode 100644 index 0000000..8881e48 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/cloudnodemsg/listener/PlayerJoinListener.java @@ -0,0 +1,23 @@ +package pro.cloudnode.smp.cloudnodemsg.listener; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.cloudnodemsg.CloudnodeMSG; +import pro.cloudnode.smp.cloudnodemsg.Mail; +import pro.cloudnode.smp.cloudnodemsg.Permission; + +public final class PlayerJoinListener implements Listener { + @EventHandler + public void onPlayerJoin(final @NotNull PlayerJoinEvent event) { + CloudnodeMSG.runAsync(() -> { + if (!CloudnodeMSG.getInstance().config().mailNotifyOnLogin()) return; + final @NotNull Player player = event.getPlayer(); + if (!player.hasPermission(Permission.MAIL)) return; + final int unread = Mail.unread(player); + if (unread > 0) player.sendMessage(CloudnodeMSG.getInstance().config().mailNotify(unread)); + }); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ade838b..bb8ce0d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,3 +1,35 @@ +# Database configuration +db: + sqlite: + # Database file name (in the plugin's folder) + # If the file does not exist, it will be created + file: 'cloudnodemsg.db' + + # Advanced DB configuration / HikariCP properties + # Only change if you know what you're doing; you can add or remove any property you want + hikaricp: + - name: "cachePrepStmts" + value: "true" + - name: "prepStmtCacheSize" + value: "250" + - name: "prepStmtCacheSqlLimit" + value: "2048" + - name: "useServerPrepStmts" + value: "true" + - name: "useLocalSessionState" + value: "true" + - name: "rewriteBatchedStatements" + value: "true" + - name: "cacheResultSetMetadata" + value: "true" + - name: "cacheServerConfiguration" + value: "true" + - name: "elideSetAutoCommits" + value: "true" + - name: "maintainTimeStats" + value: "false" + + # Incoming message format (recipient's point of view) # Placeholders: # - the username of the message sender @@ -65,6 +97,9 @@ channel: # Name for console/server that should appear as or in messages console-name: "Server" +# Name for player when name not found (usually very unlikely to happen) +unknown-name: "Unknown Player" + # Command usage format # Placeholders: # - the command name @@ -88,6 +123,38 @@ toggle: # - the player's username other: "(!) Re-enabled receiving of private messages for ." +mail: + # Should players be notified for unread mail on login? + notify-on-login: true + + # Interval (in minutes) to notify players of unread mail. Set to 0 to disable. + notify-interval: 3 + + # Unread mail notification message + # Placeholders: + # - the number of unread mail messages + notify: "(!) You have unread messages. View mail" + + # Successfully sent mail + # Placeholders: + # - the username of the sender + # - the username of the recipient + # - the message + # - whether the mail was read/opened by the recipient (see: https://docs.advntr.dev/minimessage/dynamic-replacements.html#insert-a-choice) + # - whether the recipient has starred the mail (see: https://docs.advntr.dev/minimessage/dynamic-replacements.html#insert-a-choice) + # - the ID of the mail + sent: "(!) Successfully sent mail to ." + + # Received mail while online + # Placeholders: + # - the username of the sender + # - the username of the recipient + # - the message + # - whether the mail was read/opened by the recipient (see: https://docs.advntr.dev/minimessage/dynamic-replacements.html#insert-a-choice) + # - whether the recipient has starred the mail (see: https://docs.advntr.dev/minimessage/dynamic-replacements.html#insert-a-choice) + # - the ID of the mail + received: "(!) You have new mail from . >Click to view" + # Error messages errors: # No permission diff --git a/src/main/resources/ddl/sqlite.sql b/src/main/resources/ddl/sqlite.sql new file mode 100644 index 0000000..901a8aa --- /dev/null +++ b/src/main/resources/ddl/sqlite.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `cloudnodemsg_mail` +( + `id` CHAR(32) COLLATE NOCASE PRIMARY KEY, + `sender` CHAR(32) COLLATE NOCASE NOT NULL, + `recipient` CHAR(32) COLLATE NOCASE NOT NULL, + `message` TEXT NOT NULL, + `seen` BOOLEAN DEFAULT 0, + `starred` BOOLEAN DEFAULT 0 +); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2f88c00..edc366c 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -37,3 +37,7 @@ commands: usage: / [message] aliases: [ "tmsg", "teamchat" , "tm" ] permission: cloudnodemsg.use.team + mail: + description: Send and receive offline messages + usage: / + aliases: [ "post" ]