import java.util.Deque; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.pircbotx.Channel; import org.pircbotx.Colors; import org.pircbotx.ShockyBot; import org.pircbotx.hooks.events.MessageEvent; import pl.shockah.StringTools; import pl.shockah.shocky.Data; import pl.shockah.shocky.Module; import pl.shockah.shocky.Shocky; import pl.shockah.shocky.interfaces.IRollback; import pl.shockah.shocky.interfaces.ILinePredicate; import pl.shockah.shocky.lines.LineAction; import pl.shockah.shocky.lines.LineMessage; import pl.shockah.shocky.lines.LineWithUsers; public class ModuleRegexReplace extends Module { public static enum Type {SUB, MATCH, TRANSLATE}; public static final Pattern sedPattern = Pattern.compile("^([sm]|tr)/(.*?(?<!\\\\))/(?:(.*?(?<!\\\\))/)?([a-z]*)"); public static String[] groupColors = new String[] {"01,14","03,09","10,11","05,04","06,13","07,08","02,12","15,00"}; @Override public String name() {return "regexreplace";} public boolean isListener() {return true;} @Override public void onMessage(MessageEvent<ShockyBot> event) throws Exception { if (Data.isBlacklisted(event.getUser())) return; String output = run(event.getChannel(), event.getMessage()); if (output != null) Shocky.sendChannel(event.getBot(), event.getChannel(), output); } public String run(Channel channel, String s) throws InterruptedException, ExecutionException { IRollback module = (IRollback) Module.getModule("rollback"); if (module == null) return null; int start = 0; List<StringProcess> list = new LinkedList<StringProcess>(); Matcher m = sedPattern.matcher(s); while(start < s.length()) { while(start < s.length() && Character.isWhitespace(s.charAt(start))) ++start; m.region(start, s.length()); if (!m.find()) break; String stype = m.group(1); String pattern = m.group(2); String replacement = m.group(3); String params = m.group(4); if (pattern.isEmpty()) return null; Type type; if (stype.contentEquals("s")) type = Type.SUB; else if (stype.contentEquals("m")) type = Type.MATCH; else if (stype.contentEquals("tr")) type = Type.TRANSLATE; else continue; if (type != Type.MATCH && replacement==null) return null; if (type == Type.MATCH && channel.getMode().contains("c")) return null; if (type == Type.TRANSLATE) { try { list.add(new Translate(StringTools.build_translate(pattern), StringTools.build_translate(replacement))); } catch (RuntimeException e) { return StringTools.deleteWhitespace(e.getMessage()); } } else { int flags = 0; boolean single = true; for (int i = 0;i < params.length();++i) { switch (params.charAt(i)) { case 'g':single = false;break; case 'd':flags |= Pattern.UNIX_LINES;break; case 'i':flags |= Pattern.CASE_INSENSITIVE;break; case 'm':flags |= Pattern.MULTILINE;break; case 's':flags |= Pattern.DOTALL;break; case 'u':flags |= Pattern.UNICODE_CASE;break; case 'x':flags |= Pattern.COMMENTS;break; } } try { list.add(new Regex(Pattern.compile(pattern, flags), single, type, type != Type.MATCH ? replacement : null)); } catch (PatternSyntaxException e) { return StringTools.deleteWhitespace(e.getMessage()); } } start = m.end()+1; } if (list.isEmpty()) return null; String user = null; if (start < s.length()) user = s.substring(start).trim(); final ExecutorService service = Executors.newFixedThreadPool(1); try { Future<String> run = service.submit(new Run(module, channel.getName(), user, s, list)); return run.get(10, TimeUnit.SECONDS); } catch (TimeoutException e) { return null; } finally { service.shutdown(); } } private interface StringProcess { boolean matches(String text); void init(CharSequence input); boolean run(boolean useLine, StringBuffer output); } private static class Translate implements StringProcess { public final String search; public final String replacement; private CharSequence text; public Translate(String search, String replacement) { this.search = search; this.replacement = replacement; } @Override public boolean matches(String text) { for (int i = text.length() - 1; i >= 0; --i) { if (search.indexOf(text.charAt(i)) >= 0) { this.text = text; return true; } } this.text = null; return false; } @Override public void init(CharSequence input) { this.text = input; } @Override public boolean run(boolean useLine, StringBuffer output) { this.text = StringTools.translate(search, replacement, text); output.append(text); return false; } } private static class Regex implements StringProcess { public final Pattern pattern; public final boolean single; public final Type type; public final String replacement; private Matcher matcher; public Regex(Pattern pattern, boolean single, Type type, String replacement) { this.pattern = pattern; this.single = single; this.type = type; this.replacement = replacement; } @Override public boolean matches(String text) { if (type == Type.MATCH) text = Colors.removeFormattingAndColors(text); return matcher.reset(text).find(); } @Override public void init(CharSequence input) { matcher = pattern.matcher(input); } @Override public boolean run(boolean useLine, StringBuffer output) { if (!useLine && !matcher.find()) return true; do { matcher.appendReplacement(output, (type == Type.SUB) ? replacement : coloredGroups()); if (single) break; } while (matcher.find()); matcher.appendTail(output); return false; } private String coloredGroups() { String capture = matcher.group(); if (capture.isEmpty()) return capture; StringBuilder sb = new StringBuilder(); Deque<Integer> color = new LinkedList<Integer>(); int last = -1; for (int o = 0; o <= capture.length(); ++o) { for (int p = 0; p <= matcher.groupCount(); ++p) { int s = matcher.start(p) - matcher.start(); int e = matcher.end(p) - matcher.start(); if (o == s) color.push(p); else if (o == e) color.pop(); } if (color.isEmpty() && last != -1) { last = -1; sb.append(Colors.NORMAL); } else if (!color.isEmpty() && last != color.peek()) { last = color.peek(); sb.append('\3').append(groupColors[last%groupColors.length]); } if (o < capture.length()) sb.append(capture.charAt(o)); } return sb.toString(); } } private static class Run implements Callable<String>, ILinePredicate<LineWithUsers> { private final IRollback module; private final String channel; private final String user; private final String message; private final Iterable<StringProcess> regex; private StringProcess current; public Run(IRollback module, String channel, String user, String message, Iterable<StringProcess> regex) { this.module = module; this.channel = channel; this.user = user; this.message = message; this.regex = regex; } @Override public boolean accepts(LineWithUsers line) { String text; if (line instanceof LineMessage) text = ((LineMessage) line).text; else if (line instanceof LineAction) text = ((LineAction) line).text; else return false; return current.matches(text); } @Override public String call() throws Exception { LineWithUsers line = null; boolean useLine = true; StringBuffer sb = new StringBuffer(); Iterator<StringProcess> iter = regex.iterator(); while(iter.hasNext()) { current = iter.next(); current.init(sb); if (useLine) { line = module.getRollbackLine(this, LineWithUsers.class, channel, user, null, message, true, 10, 0); if (line == null) return null; if (current.run(true, sb)) return null; useLine = false; } else { sb = new StringBuffer(); if (current.run(false, sb)) return null; } } if (line instanceof LineAction) { sb.insert(0, "\001ACTION "); sb.append('\001'); } return StringTools.limitLength(sb); } } }