diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 2047703..7bca7ba 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -9,6 +9,7 @@ on: branches: - master - main + - dev jobs: build: @@ -27,15 +28,15 @@ jobs: - name: Set up Maven uses: stCarolas/setup-maven@v4.5 with: - maven-version: 3.9.1 + maven-version: 3.9.9 - name: build application shell: bash run: | - mvn clean install + mvn clean package - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: ${{ github.event.repository.name }}-artifact - path: target/*.jar \ No newline at end of file + name: artifacts + path: target/*.jar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70c055d..5269456 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,9 +36,10 @@ jobs: - name: Create GitHub Release if: ${{ !endsWith(env.PROJECT_VERSION, '-SNAPSHOT') }} - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ env.PROJECT_VERSION }} + body_path: changelogs/${{ env.PROJECT_VERSION }}.md files: | target/*.jar env: diff --git a/LICENSE b/LICENSE index ab5a771..6dc18d1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 RetroMC Development Group +Copyright (c) 2025 RetroHaven Development Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LICENSE-retromc b/LICENSE-retromc new file mode 100644 index 0000000..ab5a771 --- /dev/null +++ b/LICENSE-retromc @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 RetroMC Development Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d2b175d..983918b 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,27 @@ -# Poseidon-Plugin-Template +# SimpleNotes -This repository serves as a template to assist with creating plugins for Project Poseidon. +This plugin aims to provide a note/warning system implemented easily for beta 1.7.3. No other Minecraft versions are targeted and no support is provided for those other versions. -It includes examples of: -- A configuration file. -- A listener. -- A command. +This plugin currently assumes all players are paid accounts. Adding offline players support isn't planned for now. -## Steps to Use This Template +Tested with Project Poseidon. Plain Bukkit should work since this project doesn't use any of Poseidon's APIs. -1. **Clone the Repository** - - Clone this repository to your local machine. +## Commands -2. **Modify `pom.xml`** - - Update the following fields to reflect your plugin: - - `name` - - `version` - - `description` - - **Note:** Removing `-SNAPSHOT` from the version will trigger the `release.yml` GitHub Action to create a GitHub release. +The plugin uses one command, /note (with aliases /notes, /warn, /warns, /warning and /warnings). -3. **Refactor Package Structure** - - Refactor the package `org.retromc.templateplugin` to a unique package name for your plugin to avoid conflicts. +This command is disabled by default to make you look through the config (it's located in plugins/SimpleNotes/config.yml!). -4. **Update `plugin.yml`** - - Update the `plugin.yml` file to match the refactored package name and plugin metadata. +### Subcommands -5. **Modify the Code** - - Customize the code as required for your plugin. - - **Important:** - - Remove the player greeting example in the listener. - - Remove the test command. +- /note help: list the subcommands you have access to +- /note list: list your own notes/warnings (permission: simplenotes.see.self.notes/warns) +- /note list [player]: list another player's notes/warnings (permission: simplenotes.see.others.notes/warns) +- /note add [player] [content]: add a note to a player (permission: simplenotes.addnotes) +- /warn add [player] [content]: add a warn to a player (permission: simplenotes.addnotes) + - This is the only case where the alias matter. You can use any of the warn* aliases for this. +- /note remove [player] [id]: remove a note from a player (permission: simplenotes.removenotes) -## GitHub Actions +## Compiling -This repository includes a pre-configured GitHub Action: - -1. **`build-and-test.yml`**: - - Runs tests on every push to ensure code quality. - - Uploads an artifact for each commit, allowing others to download the plugin for testing. - -2. **`release.yml`**: - - Automatically creates a GitHub release if the `-SNAPSHOT` suffix is removed from the version in `pom.xml`. - -With this template, you can kickstart your plugin development for Project Poseidon quickly and efficiently. +Clone the repository and run `mvn clean package`. The resulting jar should be in the `target` folder. \ No newline at end of file diff --git a/changelogs/1.0.0.md b/changelogs/1.0.0.md new file mode 100644 index 0000000..1936fc8 --- /dev/null +++ b/changelogs/1.0.0.md @@ -0,0 +1,3 @@ +First release. + +Adds /note add, /warn add, /note remove, /note list and /note help. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 643ddfa..d29e639 100644 --- a/pom.xml +++ b/pom.xml @@ -4,10 +4,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.example - Poseidon-Plugin-Template - 1.0.0-SNAPSHOT - A template for creating plugins for the Poseidon server software. + org.retrohaven.mc.notes + SimpleNotes + 1.0.0 + A note system for moderators and admins, with the objective to be light on the system and on the codebase. 8 @@ -57,18 +57,66 @@ 1.8 + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + + com.opencsv:opencsv + org.json:json + com.google.guava:* + + + + + com.google.common + org.retrohaven.shaded.com.google.common + + + + + com.google.guava:guava + + com/google/common/** + + + + + + + package + + shade + + + + - - com.legacyminecraft.poseidon poseidon-craftbukkit - 1.1.8 + 1.1.10-250328-1731-f67a8e3 + provided + + + org.json + json + 20250107 + + + com.opencsv + opencsv + 5.11 + + + com.google.guava + guava + 33.4.8-jre - - - \ No newline at end of file + diff --git a/src/main/java/org/retromc/templateplugin/TemplateConfig.java b/src/main/java/org/retrohaven/mc/notes/NoteConfig.java similarity index 71% rename from src/main/java/org/retromc/templateplugin/TemplateConfig.java rename to src/main/java/org/retrohaven/mc/notes/NoteConfig.java index 072c1d8..1322802 100644 --- a/src/main/java/org/retromc/templateplugin/TemplateConfig.java +++ b/src/main/java/org/retrohaven/mc/notes/NoteConfig.java @@ -1,7 +1,6 @@ -package org.retromc.templateplugin; +package org.retrohaven.mc.notes; import org.bukkit.util.config.Configuration; -import org.jetbrains.annotations.Nullable; import java.io.File; @@ -10,19 +9,18 @@ * Extends the {@link Configuration} class to provide additional utility methods for * reading and writing configuration options with defaults. */ -public class TemplateConfig extends Configuration { - private final int configVersion = 1; +public class NoteConfig extends Configuration { + private final int configVersion = 0; - - private TemplatePlugin plugin; + private NotePlugin plugin; /** - * Constructs a new TemplateConfig instance. + * Constructs a new NoteConfig instance. * * @param plugin The plugin instance associated with this configuration. * @param configFile The configuration file to be managed. */ - public TemplateConfig(TemplatePlugin plugin, File configFile) { + public NoteConfig(NotePlugin plugin, File configFile) { super(configFile); this.plugin = plugin; this.reload(); @@ -44,24 +42,20 @@ private void write() { generateConfigOption("config-version", configVersion); // Plugin options - generateConfigOption("settings.test-command.enabled.value", true); - generateConfigOption("settings.test-command.enabled.info", "Whether the test command is enabled."); // Informational comment + generateConfigOption("settings.plugin.enabled.value", false); + generateConfigOption("settings.plugin.enabled.info", "Whether the command is enabled."); - generateConfigOption("settings.test-command.response.value", "This is the response sent to players when they execute the test command."); - generateConfigOption("settings.test-command.response.info", "The response sent to players when they execute the test command."); // Informational comment + generateConfigOption("settings.warns.showonlogin.value", true); + generateConfigOption("settings.warns.showonlogin.info", "Whether one's warns should be shown on login. Requires the player to have simplenotes.see.self.warns."); - generateConfigOption("settings.welcome-message.value", "Welcome to the server, %player%!"); - generateConfigOption("settings.welcome-message.info", "The message sent to players when join the server."); // Informational comment + generateConfigOption("settings.notes.showonlogin.value", false); + generateConfigOption("settings.notes.showonlogin.info", "Whether one's notes should be shown on login. Requires the player to have simplenotes.see.self.notes."); } private void convertToNewConfig() { // Convert old configuration keys to new keys - - // Convert from old config version 0 to new config version 1 - if(this.getString("config-version") == null || Integer.valueOf(this.getString("config-version")) < 1) { - convertToNewAddress("settings.test-command-response.value", "settings.test-command.response.value", true); - convertToNewAddress("settings.test-command.enabled", "settings.test-command.enabled.value", true); - } + // Currently unused + ; } /** diff --git a/src/main/java/org/retrohaven/mc/notes/NoteListener.java b/src/main/java/org/retrohaven/mc/notes/NoteListener.java new file mode 100644 index 0000000..bc6a462 --- /dev/null +++ b/src/main/java/org/retrohaven/mc/notes/NoteListener.java @@ -0,0 +1,28 @@ +package org.retrohaven.mc.notes; + +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerListener; +import org.retrohaven.mc.notes.commands.NoteCommand; + +public class NoteListener extends PlayerListener { + private NotePlugin plugin; + private NoteConfig config; + + // Constructor to link the plugin instance + public NoteListener(NotePlugin plugin) { + this.plugin = plugin; + this.config = plugin.getConfig(); + } + + @Override + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + if ((!player.hasPermission("simplenotes.see.self.warns") && !player.isOp() && !config.getConfigBoolean("settings.warns.showonlogin.value")) + && (!player.hasPermission("simplenotes.see.self.notes") && !player.isOp() && !config.getConfigBoolean("settings.notes.showonlogin.value"))) { + return; + } + NoteCommand commands = new NoteCommand(plugin); + boolean b = commands.NoteList(player, new String[] {""}, true); + } +} diff --git a/src/main/java/org/retrohaven/mc/notes/NotePlugin.java b/src/main/java/org/retrohaven/mc/notes/NotePlugin.java new file mode 100644 index 0000000..e11cbf4 --- /dev/null +++ b/src/main/java/org/retrohaven/mc/notes/NotePlugin.java @@ -0,0 +1,59 @@ +package org.retrohaven.mc.notes; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; +import org.retrohaven.mc.notes.commands.NoteCommand; + +import java.io.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NotePlugin extends JavaPlugin { + private JavaPlugin plugin; + private Logger log; + private String pluginName; + private PluginDescriptionFile pdf; + + private NoteConfig configuration; + + @Override + public void onEnable() { + plugin = this; + log = this.getServer().getLogger(); + pdf = this.getDescription(); + pluginName = pdf.getName(); + log.info("[" + pluginName + "] is loading, version: " + pdf.getVersion()); + + // Load configuration + configuration = new NoteConfig(this, new File(getDataFolder(), "config.yml")); // Load the configuration file from the plugin's data folder + + // Register the command and the aliases + getCommand("note").setExecutor(new NoteCommand(this)); + + // Register the listeners + if (configuration.getConfigBoolean("settings.warns.showonlogin.value") || configuration.getConfigBoolean("settings.notes.showonlogin.value")) { + NoteListener listener = new NoteListener(this); + getServer().getPluginManager().registerEvent(Event.Type.PLAYER_JOIN, listener, Event.Priority.Monitor, this); + } + + log.info("[" + pluginName + "] Plugin loaded!"); + } + + @Override + public void onDisable() { + // Save configuration + //config.save(); // Save the configuration file to disk. This should only be necessary if the configuration cam be modified during runtime. + + log.info("[" + pluginName + "] Plugin unloaded!"); + } + + public void logger(Level level, String message) { + Bukkit.getLogger().log(level, "[" + plugin.getDescription().getName() + "] " + message); + } + + public NoteConfig getConfig() { + return configuration; + } +} diff --git a/src/main/java/org/retrohaven/mc/notes/commands/NoteCommand.java b/src/main/java/org/retrohaven/mc/notes/commands/NoteCommand.java new file mode 100644 index 0000000..029952b --- /dev/null +++ b/src/main/java/org/retrohaven/mc/notes/commands/NoteCommand.java @@ -0,0 +1,492 @@ +package org.retrohaven.mc.notes.commands; + +import com.opencsv.CSVParserBuilder; +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.CSVWriter; +import com.opencsv.exceptions.CsvException; +import com.opencsv.exceptions.CsvValidationException; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.retrohaven.mc.notes.NoteConfig; +import org.retrohaven.mc.notes.NotePlugin; +import org.json.JSONObject; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class NoteCommand implements CommandExecutor { + + private final NotePlugin plugin; + private final NoteConfig config; + private final String errorColorCode = "§e"; + private final String permissionColorCode = "§3"; + + public NoteCommand(NotePlugin plugin) { + this.plugin = plugin; + this.config = plugin.getConfig(); + } + + public boolean CheckFileExists(File file) { + if (file.exists()) { + return true; + } else { + // we create the folders above + List parentListInit = Arrays.asList(file.getParentFile()); + List parentList = new ArrayList(parentListInit); + while (parentList.get(parentList.size() - 1).getParentFile() != null && !parentList.get(parentList.size() - 1).getParentFile().exists()) { + parentList.add(parentList.get(parentList.size() - 1).getParentFile()); + } + for (int i = parentList.size()-1; i > -1; i--) { + // create all parents, going through the list in reverse + try { + boolean b = parentList.get(i).mkdirs(); + if (!b) { + throw new RuntimeException(); + } + } catch (Exception e) { + // probably a permission issue, failed to create the dirs + return false; + } + } + // then we return false, now that all parents exist. + return false; + } + } + + // caching for UUIDs, to prevent unnecessary requests to the API + private final Cache uuidCache = CacheBuilder.newBuilder() + .expireAfterWrite(30, TimeUnit.DAYS) + .build(); + public Object getUUIDFromName(String playerName) { + try { + return uuidCache.get(playerName, () -> { + try { + String urlString = "https://api.mojang.com/users/profiles/minecraft/" + playerName; + URL url = URI.create(urlString).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + + int responseCode = conn.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + StringBuilder content = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + + in.close(); + conn.disconnect(); + + JSONObject jsonobj = new JSONObject(content.toString()); + if (jsonobj.has("errorMessage")) { + // couldn't find a profile with this name + return null; + } + return jsonobj.getString("id"); + } catch (Exception e) { + // probably network error. nothing we can do. + // unlikely to happen realistically though + return null; + } + }); + } catch (ExecutionException e) { + return null; + } + } + + public boolean NoteAdd(CommandSender sender, String[] args, String type) { + if (!sender.hasPermission("simplenotes.addnotes") && !sender.isOp()) { + sender.sendMessage(permissionColorCode+"You do not have permission to use this subcommand."); + return false; + } + if (args.length == 1) { + sender.sendMessage(errorColorCode+"Please provide a player name."); + return false; + } + String requestSubject = args[1]; + Object subjectUUID = this.getUUIDFromName(requestSubject); + if (subjectUUID == null) { + sender.sendMessage(errorColorCode+"Player doesn't exist."); + return false; + } + subjectUUID = subjectUUID.toString(); + + String filename = plugin.getDataFolder()+File.separator+"data"+File.separator+subjectUUID+".csv"; + File dataFile = new File(filename); + if (!this.CheckFileExists(dataFile)) { + try { + if (!dataFile.createNewFile()) { + throw new IOException("this didn't work"); + } + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to create "+dataFile.getAbsolutePath()); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + } + + // the following is copied from opencsv's documentation + CSVReader reader = null; + try { + reader = new CSVReaderBuilder(new FileReader(filename)).build(); + } catch (FileNotFoundException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + String [] nextLine; + int i = 1; + try { + while (true) { + if ((nextLine = reader.readNext()) != null) { + i = Integer.parseInt(nextLine[0]) + 1; + } else { + break; + } + } + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } catch (CsvValidationException e) { + throw new RuntimeException(e); + } + try { + reader.close(); + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + + Integer id = i; // we use the last ID + 1 + String id_str = id.toString(); + type = type.substring(0,4).toUpperCase(); // we keep the first 4 char, so that we have note or warn + + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + Date date = new Date(); + String dateStr = dateFormat.format(date); + + String author; + if (sender instanceof Player) { + author = sender.getName(); + } else { + author = "CONSOLE"; + } + try { + CSVWriter writer = new CSVWriter(new FileWriter(filename, true), + CSVWriter.DEFAULT_SEPARATOR, + '\"', + CSVWriter.DEFAULT_ESCAPE_CHARACTER, + CSVWriter.DEFAULT_LINE_END); + String[] line = { + id_str, + type, + dateStr, + author, + String.join(" ", + Arrays.copyOfRange(args, 2, args.length) + ) + }; + writer.writeNext(line, true); + writer.close(); + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + // success! + sender.sendMessage(type.charAt(0) + type.substring(1).toLowerCase()+" added."); + return true; + } + + public boolean NoteRemove(CommandSender sender, String[] args) { + if (!sender.hasPermission("simplenotes.removenotes") && !sender.isOp()) { + sender.sendMessage(permissionColorCode+"You do not have permission to use this subcommand."); + return false; + } + if (args.length <= 2) { + sender.sendMessage(errorColorCode+"Please provide a player name and an id."); + return true; + } + + String requestSubject = args[1]; + Object subjectUUID = this.getUUIDFromName(requestSubject); + if (subjectUUID == null) { + sender.sendMessage(errorColorCode+"Player doesn't exist."); + return true; + } + subjectUUID = subjectUUID.toString(); + + String filename = plugin.getDataFolder()+File.separator+"data"+File.separator+subjectUUID+".csv"; + File dataFile = new File(filename); + if (!this.CheckFileExists(dataFile)) { + // we do not try to create the file, since if it doesn't exist, the warn/note doesn't exist either + sender.sendMessage(errorColorCode+"Note/warn not found."); + return false; + } + + // We dump the entire CSV file into allElements + CSVReader reader = null; + try { + reader = new CSVReaderBuilder(new FileReader(filename)) + .withCSVParser(new CSVParserBuilder().build()) + .build(); + } catch (FileNotFoundException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + List allElements = null; + try { + allElements = reader.readAll(); + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } catch (CsvException e) { + throw new RuntimeException(e); + } + try { + reader.close(); + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + + /* + we delete the file, since we recreate the contents entirely later. + this is the recommended way to remove a specific line in a CSV file + */ + try { + if (!dataFile.delete() || !dataFile.createNewFile()) { + // something went wrong. we do not do data loss here + // so everything is printed to console + this.plugin.logger(Level.INFO,"Content of "+filename+":"); + this.plugin.logger(Level.INFO,allElements.stream().map(Arrays::toString).reduce((a, b) -> a + "\n" + b).orElse("")); + throw new IOException("this didn't work"); + } + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + int rowNumber = -1; + for (int i = 0; i < allElements.size(); i++) { + // we iterate through the list, and check the id of each row + if (Integer.parseInt(allElements.get(i)[0]) == Integer.parseInt(args[2])) { + rowNumber = i; + break; + } + } + if (rowNumber == -1) { + sender.sendMessage(errorColorCode+"Note/warn not found."); + return false; + } + allElements.remove(rowNumber); + + CSVWriter writer = null; + try { + writer = new CSVWriter(new FileWriter(filename), + CSVWriter.DEFAULT_SEPARATOR, + '\"', + CSVWriter.DEFAULT_ESCAPE_CHARACTER, + CSVWriter.DEFAULT_LINE_END); + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + writer.writeAll(allElements); + try { + writer.close(); + } catch (IOException e) { + sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + // success! + sender.sendMessage("Note/warn removed."); + return true; + } + + public boolean NoteList(CommandSender sender, String[] args, boolean isListener) { + String RequestSubject; + if (args.length == 1) { + if (!(sender instanceof Player)) { + sender.sendMessage("You need to provide a player name when running this in the console."); + return true; + } + RequestSubject = sender.getName(); + } + else RequestSubject = args[1]; + if (RequestSubject.equals(sender.getName()) && !sender.hasPermission("simplenotes.see.self.notes") && !sender.hasPermission("simplenotes.see.self.warns") && !sender.isOp()) { + if (!isListener) sender.sendMessage(permissionColorCode+"You do not have permission to check your own notes/warns."); + return true; + } + if (!RequestSubject.equals(sender.getName()) && !sender.hasPermission("simplenotes.see.others.notes") && !sender.hasPermission("simplenotes.see.others.warns") && !sender.isOp()) { + if (!isListener) sender.sendMessage(permissionColorCode+"You do not have permission to check other people's notes/warns."); + return true; + } + + Object subjectUUID = this.getUUIDFromName(RequestSubject); + if (subjectUUID == null) { + if (!isListener) sender.sendMessage(errorColorCode+"Player doesn't exist."); + return true; + } + subjectUUID = subjectUUID.toString(); + + String filename = plugin.getDataFolder()+File.separator+"data"+File.separator+subjectUUID+".csv"; + File dataFile = new File(filename); + if (!this.CheckFileExists(dataFile)) { + // we do not try to create the file, since if it doesn't exist, there are no warns/notes + if (!isListener) sender.sendMessage(errorColorCode+"No notes or warns to show."); + return true; + } + + CSVReader reader = null; + try { + reader = new CSVReaderBuilder(new FileReader(filename)) + .withCSVParser(new CSVParserBuilder().build()) + .build(); + } catch (FileNotFoundException e) { + if (!isListener) sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + List allElements = null; + try { + allElements = reader.readAll(); + } catch (IOException e) { + if (!isListener) sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } catch (CsvException e) { + throw new RuntimeException(e); + } + try { + reader.close(); + } catch (IOException e) { + if (!isListener) sender.sendMessage(errorColorCode+"Internal error. Ask your local sys-admin to check the console."); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] ERROR: Failed to access "+filename); + this.plugin.logger(Level.SEVERE,"["+this.plugin.getDescription().getName()+"] Check the permissions of the folders."); + return false; + } + + int shownCounter = 0; + for (String[] line : allElements) { + // some transformation first + if (Objects.equals(line[1], "NOTE")) line[1] = "§eNOTE"; + if (Objects.equals(line[1], "WARN")) line[1] = "§cWARN"; + line[2] = "§9"+line[2]; + + if (!RequestSubject.equals(sender.getName()) && (sender.hasPermission("simplenotes.see.others.notes") || sender.isOp()) && Objects.equals(line[1].substring(line[1].length() -4), "NOTE")) { + shownCounter = shownCounter + 1; + sender.sendMessage("§8| " + String.join(" §8|§r ", line)); + } else if (RequestSubject.equals(sender.getName()) && (sender.hasPermission("simplenotes.see.self.notes") || sender.isOp()) && Objects.equals(line[1].substring(line[1].length() -4), "NOTE") && (!isListener || config.getConfigBoolean("settings.notes.showonlogin.value"))) { + shownCounter = shownCounter + 1; + sender.sendMessage("§8| " + String.join(" §8|§r ", line)); + } else if (!RequestSubject.equals(sender.getName()) && (sender.hasPermission("simplenotes.see.others.warns") || sender.isOp()) && Objects.equals(line[1].substring(line[1].length() -4), "WARN")) { + shownCounter = shownCounter + 1; + sender.sendMessage("§8| " + String.join(" §8|§r ", line)); + } else if (RequestSubject.equals(sender.getName()) && (sender.hasPermission("simplenotes.see.self.warns") || sender.isOp()) && Objects.equals(line[1].substring(line[1].length() -4), "WARN") && (!isListener || config.getConfigBoolean("settings.warns.showonlogin.value"))) { + shownCounter = shownCounter + 1; + sender.sendMessage("§8| " + String.join(" §8|§r ", line)); + } + } + if (shownCounter == 0) { + sender.sendMessage("No notes or warns to show."); + } + return true; + } + + // the following should be used in all cases. + // the only reason we have isListener is because of the NoteListener + public boolean NoteList(CommandSender sender, String[] args) { + return NoteList(sender, args, false); + } + + public boolean NoteHelp(CommandSender sender, String alias) { + int removedCommands = 4; + sender.sendMessage("=== Command list ==="); + sender.sendMessage("- /"+alias+" help: show this"); + if ((sender.hasPermission("simplenotes.see.self.notes") && sender.hasPermission("simplenotes.see.self.warns")) || sender.isOp()) { + sender.sendMessage("- /"+alias+" list: show your notes/warns"); + removedCommands = removedCommands - 1; + } else if (sender.hasPermission("simplenotes.see.self.notes")) { + sender.sendMessage("- /"+alias+" list: show your notes"); + removedCommands = removedCommands - 1; + } else if (sender.hasPermission("simplenotes.see.self.warns")) { + sender.sendMessage("- /"+alias+" list: show your warns"); + removedCommands = removedCommands - 1; + } + if (sender.hasPermission("simplenotes.see.others.notes") || sender.hasPermission("simplenotes.see.others.warns") || sender.isOp()) { + sender.sendMessage("- /"+alias+" list : show another player's notes"); + removedCommands = removedCommands - 1; + } + if (sender.hasPermission("simplenotes.addnotes") || sender.isOp()) { + sender.sendMessage("- /"+alias+" add : add a "+alias.substring(0, 4)); + removedCommands = removedCommands - 1; + } + if (sender.hasPermission("simplenotes.removenotes") || sender.isOp()) { + sender.sendMessage("- /"+alias+" remove : remove a note/warn (you can get the id via /"+alias+" list!)"); + removedCommands = removedCommands - 1; + } + if (removedCommands > 0) { + sender.sendMessage(permissionColorCode+"You do not have access to any other subcommand."); + } + return true; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Check if the command is enabled + Boolean isEnabled = config.getConfigBoolean("settings.plugin.enabled.value"); + if (!isEnabled) { + sender.sendMessage("This command is currently disabled. Please check the config."); + return true; + } + // objects.equals is used to shut the IDE. it's not gonna be null let's be realistic. + if (args.length == 0 || Objects.equals(args[0], "help")) { + return this.NoteHelp(sender, label); + } else if (Objects.equals(args[0], "add")) { + // label is needed because of the differentiation between notes and warnings + return this.NoteAdd(sender, args, label); + } else if (Objects.equals(args[0], "remove")) { + return this.NoteRemove(sender, args); + } else if (Objects.equals(args[0], "list")) { + return this.NoteList(sender, args); + } else { + sender.sendMessage(errorColorCode+"Unrecognized argument."); + return this.NoteHelp(sender, label); + } + } +} diff --git a/src/main/java/org/retromc/templateplugin/TemplateListener.java b/src/main/java/org/retromc/templateplugin/TemplateListener.java deleted file mode 100644 index 846827e..0000000 --- a/src/main/java/org/retromc/templateplugin/TemplateListener.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.retromc.templateplugin; - -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; - -public class TemplateListener implements Listener { - private TemplatePlugin plugin; - private TemplateConfig config; - - // Constructor to link the plugin instance - public TemplateListener(TemplatePlugin plugin) { - this.plugin = plugin; - this.config = plugin.getConfig(); - } - - // Handle player join event - @EventHandler(priority = Event.Priority.Normal) - public void onPlayerJoin(PlayerJoinEvent event) { - // Example action: Send a welcome message configured in the plugin's configuration file to the player when they join the server - - String welcomeMessage = config.getConfigString("settings.welcome-message.value"); - welcomeMessage = welcomeMessage.replace("%player%", event.getPlayer().getName()); - - event.getPlayer().sendMessage(welcomeMessage); - } -} diff --git a/src/main/java/org/retromc/templateplugin/TemplatePlugin.java b/src/main/java/org/retromc/templateplugin/TemplatePlugin.java deleted file mode 100644 index 5f06a40..0000000 --- a/src/main/java/org/retromc/templateplugin/TemplatePlugin.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.retromc.templateplugin; - -import org.bukkit.Bukkit; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.plugin.java.JavaPlugin; -import org.retromc.templateplugin.commands.TemplateTestCommand; - -import java.io.File; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class TemplatePlugin extends JavaPlugin { - private JavaPlugin plugin; - private Logger log; - private String pluginName; - private PluginDescriptionFile pdf; - - private TemplateConfig configuration; - - - @Override - public void onEnable() { - plugin = this; - log = this.getServer().getLogger(); - pdf = this.getDescription(); - pluginName = pdf.getName(); - log.info("[" + pluginName + "] Is Loading, Version: " + pdf.getVersion()); - - // Load configuration - configuration = new TemplateConfig(this, new File(getDataFolder(), "config.yml")); // Load the configuration file from the plugin's data folder - - // Register the commands - getCommand("testcommand").setExecutor(new TemplateTestCommand(this)); - - // Register the listeners - TemplateListener listener = new TemplateListener(this); - getServer().getPluginManager().registerEvents(listener, this); - - log.info("[" + pluginName + "] Is Loaded, Version: " + pdf.getVersion()); - } - - @Override - public void onDisable() { - log.info("[" + pluginName + "] Is Unloading, Version: " + pdf.getVersion()); - - // Save configuration - //config.save(); // Save the configuration file to disk. This should only be necessary if the configuration cam be modified during runtime. - - log.info("[" + pluginName + "] Is Unloaded, Version: " + pdf.getVersion()); - } - - public void logger(Level level, String message) { - Bukkit.getLogger().log(level, "[" + plugin.getDescription().getName() + "] " + message); - } - - public TemplateConfig getConfig() { - return configuration; - } -} diff --git a/src/main/java/org/retromc/templateplugin/commands/TemplateTestCommand.java b/src/main/java/org/retromc/templateplugin/commands/TemplateTestCommand.java deleted file mode 100644 index b23c08e..0000000 --- a/src/main/java/org/retromc/templateplugin/commands/TemplateTestCommand.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.retromc.templateplugin.commands; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.retromc.templateplugin.TemplateConfig; -import org.retromc.templateplugin.TemplatePlugin; - -public class TemplateTestCommand implements CommandExecutor { - - private final TemplatePlugin plugin; - - private final TemplateConfig config; - - public TemplateTestCommand(TemplatePlugin plugin) { - this.plugin = plugin; - this.config = plugin.getConfig(); - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - // Check if the sender is a player - if (!(sender instanceof Player)) { - sender.sendMessage("This command can only be executed by players."); - return true; - } - - if (!sender.hasPermission("myplugin.testcommand") && !sender.isOp()) { - sender.sendMessage("You do not have permission to execute this command."); - return true; - } - - // Check if the command is enabled - Boolean isEnabled = config.getConfigBoolean("settings.test-command.enabled.value"); - if (!isEnabled) { - sender.sendMessage("This command is currently disabled."); - return true; - } - - // Get the response message from the config - String response = config.getConfigString("settings.test-command.response.value"); - - // Send the response message to the player - sender.sendMessage(response); - return true; - } -} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ab373a5..09d1590 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,22 +1,52 @@ name: ${project.name} description: ${project.description} -main: org.retromc.templateplugin.TemplatePlugin +main: org.retrohaven.mc.notes.NotePlugin version: ${project.version} authors: - - JohnyMuffin -softdepend: - - Essentials + - eleanorsilly commands: - testcommand: - description: A test command for the plugin. - usage: / [args] - aliases: [tc] - permission: myplugin.testcommand - permission-message: You do not have permission to use this command. + note: + description: Add, remove or see notes or warnings. /note help for more details. + usage: /note [subcommand] [player] [additional arguments] + aliases: [notes, warn, warns, warning, warnings] permissions: - myplugin.*: - description: Gives access to all myplugin commands. + simplenotes.*: + description: Gives access to all commands of the plugin. Should be used sparingly, e.g. for admins. children: - myplugin.testcommand: true \ No newline at end of file + simplenotes.addnotes: true + simplenotes.removenotes: true + simplenotes.see.self.notes: true + simplenotes.see.self.warns: true + simplenotes.see.others.notes: true + simplenotes.see.others.warns: true + simplenotes.addnotes: + description: Gives the permission to add notes/warnings. + simplenotes.removenotes: + description: Gives the permission to remove notes/warnings. + simplenotes.see.*: + description: Gives the permission to see the notes/warnings of all players. + children: + simplenotes.see.self.notes: true + simplenotes.see.self.warns: true + simplenotes.see.others.notes: true + simplenotes.see.others.warns: true + simplenotes.see.self.*: + description: Gives the permission to see your own notes/warnings + children: + simplenotes.see.self.notes: true + simplenotes.see.self.warns: true + simplenotes.see.self.notes: + description: Gives the permission to see your own notes + simplenotes.see.self.warns: + description: Gives the permission to see your own warnings + simplenotes.see.others.*: + description: "Gives the permission to see other people's notes/warnings" + children: + simplenotes.see.others.notes: true + simplenotes.see.others.warns: true + simplenotes.see.others.notes: + description: "Gives the permission to see other people's notes" + simplenotes.see.others.warns: + description: "Gives the permission to see other people's warnings"