diff --git a/src/main/java/me/chayapak1/chomens_bot/Bot.java b/src/main/java/me/chayapak1/chomens_bot/Bot.java
index 3305fc2..4f2958d 100644
--- a/src/main/java/me/chayapak1/chomens_bot/Bot.java
+++ b/src/main/java/me/chayapak1/chomens_bot/Bot.java
@@ -69,6 +69,7 @@ public class Bot {
public TPSPlugin tps;
public EvalPlugin eval;
public TrustedPlugin trusted;
+ public GrepLogPlugin grepLog;
public BruhifyPlugin bruhify;
public CloopPlugin cloop;
public ExploitsPlugin exploits;
@@ -123,6 +124,7 @@ public class Bot {
this.tps = new TPSPlugin(this);
this.eval = new EvalPlugin(this);
this.trusted = new TrustedPlugin(this);
+ this.grepLog = new GrepLogPlugin(this);
this.bruhify = new BruhifyPlugin(this);
this.cloop = new CloopPlugin(this);
this.exploits = new ExploitsPlugin(this);
diff --git a/src/main/java/me/chayapak1/chomens_bot/commands/GrepLogCommand.java b/src/main/java/me/chayapak1/chomens_bot/commands/GrepLogCommand.java
new file mode 100644
index 0000000..4dfc3f1
--- /dev/null
+++ b/src/main/java/me/chayapak1/chomens_bot/commands/GrepLogCommand.java
@@ -0,0 +1,93 @@
+package me.chayapak1.chomens_bot.commands;
+
+import me.chayapak1.chomens_bot.Bot;
+import me.chayapak1.chomens_bot.command.Command;
+import me.chayapak1.chomens_bot.command.CommandContext;
+import me.chayapak1.chomens_bot.command.CommandException;
+import me.chayapak1.chomens_bot.command.TrustLevel;
+import me.chayapak1.chomens_bot.util.ColorUtilities;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+
+public class GrepLogCommand extends Command {
+ private Thread thread;
+
+ public GrepLogCommand() {
+ super(
+ "greplog",
+ "Queries the bot's logs",
+ new String[] { "", "...-ignorecase...", "...-regex...", "stop" },
+ new String[] { "logquery", "findlog" },
+ TrustLevel.PUBLIC,
+ false
+ );
+ }
+
+ @Override
+ public Component execute(CommandContext context) throws CommandException {
+ final Bot bot = context.bot;
+
+ if (bot.discord.jda == null) throw new CommandException(Component.text("The bot's Discord integration has to be enabled to use the command."));
+
+ boolean ignoreCase = false;
+ boolean regex = false;
+
+ String firstInput = context.getString(false, true);
+
+ // run 2 times. for example `*greplog -ignorecase -regex test` will be both accepted
+ for (int i = 0; i < 2; i++) {
+ if (firstInput.equals("-ignorecase")) {
+ ignoreCase = true;
+ firstInput = context.getString(false, true);
+ } else if (firstInput.equals("-regex")) {
+ regex = true;
+ firstInput = context.getString(false, true);
+ }
+ }
+
+ // interesting code
+ final String input = (firstInput + " " + context.getString(true, false)).trim();
+
+ if (input.equals("stop")) {
+ if (thread == null) throw new CommandException(Component.text("There is no query process running"));
+
+ bot.grepLog.running = false;
+
+ thread.interrupt(); // ? should i interrupt it this way?
+
+ thread = null;
+
+ return Component.text("Stopped querying the logs").color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor));
+ }
+
+ if (thread != null) throw new CommandException(Component.text("Another query is already running"));
+
+ context.sendOutput(
+ Component
+ .translatable("Started querying the logs for %s")
+ .color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor))
+ .arguments(
+ Component
+ .text(input)
+ .color(ColorUtilities.getColorByString(bot.config.colorPalette.string))
+ )
+ );
+
+ final boolean finalIgnoreCase = ignoreCase;
+ final boolean finalRegex = regex;
+
+ thread = new Thread(() -> {
+ try {
+ bot.grepLog.search(context, input, finalIgnoreCase, finalRegex);
+ } catch (CommandException e) {
+ context.sendOutput(e.message.color(NamedTextColor.RED));
+ }
+
+ thread = null;
+ });
+
+ thread.start();
+
+ return null;
+ }
+}
diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/CommandHandlerPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/CommandHandlerPlugin.java
index 51bda2e..c7074e3 100644
--- a/src/main/java/me/chayapak1/chomens_bot/plugins/CommandHandlerPlugin.java
+++ b/src/main/java/me/chayapak1/chomens_bot/plugins/CommandHandlerPlugin.java
@@ -57,6 +57,7 @@ public class CommandHandlerPlugin {
registerCommand(new SeenCommand());
registerCommand(new IPFilterCommand());
registerCommand(new StopCommand());
+ registerCommand(new GrepLogCommand());
}
public boolean disabled = false;
diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/GrepLogPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/GrepLogPlugin.java
new file mode 100644
index 0000000..c8ee474
--- /dev/null
+++ b/src/main/java/me/chayapak1/chomens_bot/plugins/GrepLogPlugin.java
@@ -0,0 +1,168 @@
+package me.chayapak1.chomens_bot.plugins;
+
+import me.chayapak1.chomens_bot.Bot;
+import me.chayapak1.chomens_bot.command.CommandContext;
+import me.chayapak1.chomens_bot.command.CommandException;
+import me.chayapak1.chomens_bot.util.ColorUtilities;
+import me.chayapak1.chomens_bot.util.FileLoggerUtilities;
+import me.chayapak1.chomens_bot.util.StringUtilities;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.utils.FileUpload;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.event.HoverEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Stream;
+import java.util.zip.GZIPInputStream;
+
+public class GrepLogPlugin {
+ private final Bot bot;
+
+ private Pattern pattern;
+
+ private int count = 0;
+
+ public boolean running = false;
+
+ public GrepLogPlugin (Bot bot) {
+ this.bot = bot;
+ }
+
+ public void search (CommandContext context, String input, boolean ignoreCase, boolean regex) throws CommandException {
+ running = true;
+
+ try (final Stream files = Files.list(FileLoggerUtilities.logDirectory)) {
+ final Path[] fileList = files.toArray(Path[]::new);
+
+ Arrays.sort(fileList, Comparator.comparing(a -> a.getFileName().toString()));
+
+ final StringBuilder result = new StringBuilder();
+
+ for (Path filePath : fileList) {
+ if (!running) return;
+
+ if (count > 1_000_000) break;
+
+ final String fileName = filePath.getFileName().normalize().toString();
+ final String absolutePath = filePath.toAbsolutePath().normalize().toString();
+
+ if (fileName.endsWith(".txt.gz")) {
+ try (
+ final GZIPInputStream gzipInputStream = new GZIPInputStream(new FileInputStream(absolutePath));
+ final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8))
+ ) {
+ result.append(process(bufferedReader, input, ignoreCase, regex));
+ }
+ } else if (fileName.endsWith(".txt")) {
+ try (
+ final FileInputStream fileInputStream = new FileInputStream(absolutePath);
+ final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, StandardCharsets.UTF_8))
+ ) {
+ result.append(process(bufferedReader, input, ignoreCase, regex));
+ }
+ }
+ }
+
+ // ? should this be here? (see `process` function below)
+ pattern = null;
+
+ count = 0;
+
+ final String stringifiedResult = result.toString();
+
+ final long matches = stringifiedResult.lines().count();
+
+ if (matches == 0) throw new CommandException(Component.text("No matches has been found"));
+
+ final String channelId = bot.discord.servers.get(bot.host + ":" + bot.port);
+ final TextChannel logChannel = bot.discord.jda.getTextChannelById(channelId);
+
+ if (logChannel == null) return;
+
+ logChannel
+ .sendMessage("Greplog result:")
+ .addFiles(
+ FileUpload.fromData(
+ // as of the time writing this, discord has an 8 MB file size limit for bots
+ StringUtilities.truncateToFitUtf8ByteLength(stringifiedResult, 8 * 1000 * 1000).getBytes(),
+ String.format("result-%d.txt", System.currentTimeMillis() / 1000)
+ )
+ )
+ .queue(message -> {
+ final String url = message.getAttachments().get(0).getUrl();
+
+ final Component component = Component.translatable("Found %s matches for %s. You can see the results by clicking %s or in the Discord server.")
+ .color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor))
+ .arguments(
+ Component.text(matches).color(ColorUtilities.getColorByString(bot.config.colorPalette.number)),
+ Component.text(input).color(ColorUtilities.getColorByString(bot.config.colorPalette.string)),
+ Component
+ .text("here")
+ .color(NamedTextColor.GREEN)
+ .hoverEvent(
+ HoverEvent.showText(
+ Component
+ .text("Click! :D")
+ .color(ColorUtilities.getColorByString(bot.config.colorPalette.secondary))
+ )
+ )
+ .clickEvent(
+ ClickEvent.openUrl(url)
+ )
+ );
+
+ context.sendOutput(component);
+ });
+ } catch (FileNotFoundException e) {
+ running = false;
+ throw new CommandException(Component.text("File not found"));
+ } catch (NotDirectoryException e) {
+ running = false;
+ throw new CommandException(Component.text("Logger directory is not a directory"));
+ } catch (PatternSyntaxException e) {
+ running = false;
+ throw new CommandException(Component.text("Pattern is invalid"));
+ } catch (IOException e) {
+ running = false;
+ throw new CommandException(Component.text("An I/O error has occurred"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ running = false;
+ }
+
+ private StringBuilder process (BufferedReader bufferedReader, String input, boolean ignoreCase, boolean regex) throws IOException, PatternSyntaxException {
+ if (regex && pattern == null) {
+ if (ignoreCase) pattern = Pattern.compile(input, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS);
+ else pattern = Pattern.compile(input, Pattern.UNICODE_CHARACTER_CLASS);
+ }
+
+ final StringBuilder result = new StringBuilder();
+
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ if (
+ (regex && pattern.matcher(line).find()) || // *greplog -regex text OR *greplog -ignorecase -regex text
+ (!ignoreCase && !regex && line.contains(input)) || // *greplog text
+ (ignoreCase && StringUtilities.containsIgnoreCase(line, input)) // *greplog -ignorecase
+ ) {
+ result.append(line).append("\n");
+
+ count++;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/me/chayapak1/chomens_bot/util/StringUtilities.java b/src/main/java/me/chayapak1/chomens_bot/util/StringUtilities.java
new file mode 100644
index 0000000..af8ff81
--- /dev/null
+++ b/src/main/java/me/chayapak1/chomens_bot/util/StringUtilities.java
@@ -0,0 +1,53 @@
+package me.chayapak1.chomens_bot.util;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+
+public class StringUtilities {
+ // https://stackoverflow.com/a/35148974/18518424
+ public static String truncateToFitUtf8ByteLength(String s, int maxBytes) {
+ if (s == null) {
+ return null;
+ }
+ Charset charset = StandardCharsets.UTF_8;
+ CharsetDecoder decoder = charset.newDecoder();
+ byte[] sba = s.getBytes(charset);
+ if (sba.length <= maxBytes) {
+ return s;
+ }
+ // Ensure truncation by having byte buffer = maxBytes
+ ByteBuffer bb = ByteBuffer.wrap(sba, 0, maxBytes);
+ CharBuffer cb = CharBuffer.allocate(maxBytes);
+ // Ignore an incomplete character
+ decoder.onMalformedInput(CodingErrorAction.IGNORE);
+ decoder.decode(bb, cb, true);
+ decoder.flush(cb);
+ return new String(cb.array(), 0, cb.position());
+ }
+
+ // https://stackoverflow.com/a/25379180/18518424
+ public static boolean containsIgnoreCase(String src, String what) {
+ final int length = what.length();
+ if (length == 0)
+ return true; // Empty string is contained
+
+ final char firstLo = Character.toLowerCase(what.charAt(0));
+ final char firstUp = Character.toUpperCase(what.charAt(0));
+
+ for (int i = src.length() - length; i >= 0; i--) {
+ // Quick check before calling the more expensive regionMatches() method:
+ final char ch = src.charAt(i);
+ if (ch != firstLo && ch != firstUp)
+ continue;
+
+ if (src.regionMatches(true, i, what, 0, length))
+ return true;
+ }
+
+ return false;
+ }
+}