package org.royaldev.royalbot; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.ArrayUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.pircbotx.User; import org.royaldev.royalbot.auth.Auth; import org.royaldev.royalbot.auth.AuthResponse; import org.royaldev.royalbot.commands.ChannelCommand; import org.royaldev.royalbot.commands.IRCCommand; import org.royaldev.royalbot.configuration.ConfigurationSection; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.apache.commons.lang3.Validate.notNull; /** * Utility class for commonly-used methods to be used in the bot. All of the methods in this class throw a * {@link java.lang.NullPointerException} if any argument is null. */ public class BotUtils { private final static ObjectMapper om = new ObjectMapper(); private BotUtils() {} // this is a thing that should never be done /** * Gets the appropriate string to send to a user if an exception is encountered. * * @param t Exception to format * @return Message to send user; never null * @throws java.lang.NullPointerException If any argument is null */ public static String formatException(Throwable t) { //noinspection ThrowableResultOfMethodCallIgnored notNull(t, "t was null"); return "Exception! " + t.getClass().getSimpleName() + ": " + t.getMessage(); } /** * Converts a Throwable's stack trace into a String. * * @param t Throwable * @return Stack trace as string * @throws java.lang.NullPointerException If any argument is null */ public static String getStackTrace(Throwable t) { //noinspection ThrowableResultOfMethodCallIgnored notNull(t, "t was null"); final StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); return sw.toString(); } /** * Pastes something to Hastebin. * * @param paste String to paste * @return Hastebin URL or null if error encountered * @throws java.lang.NullPointerException If any argument is null */ public static String pastebin(String paste) { notNull(paste, "paste was null"); final CloseableHttpClient hc = HttpClients.createDefault(); final HttpPost hp = new HttpPost("http://hastebin.com/documents"); try { hp.setEntity(new StringEntity(paste, "UTF-8")); } catch (UnsupportedEncodingException ignored) { return null; } HttpResponse hr; try { hr = hc.execute(hp); } catch (IOException ex) { return null; } HttpEntity he = hr.getEntity(); if (he == null) return null; String json; try { try (BufferedReader br = new BufferedReader(new InputStreamReader(he.getContent()))) { json = br.readLine(); } finally { hc.close(); } } catch (IOException ex) { return null; } JsonNode jn; try { jn = om.readTree(json); } catch (Exception e) { return null; } json = jn.path("key").asText(); return json.isEmpty() ? null : "http://hastebin.com/" + json; } /** * Convenience method to get a stack trace from an Exception, send it to Hastebin, and shorten the resulting link * with is.gd. * <br/> * <strong>Note:</strong> If <em>any</em> errors occur, this will simply return null, and you will get no feedback * of the error. * * @param t Throwable to do this with * @return Shortened link to the stack trace or null * @throws java.lang.NullPointerException If any argument is null */ public static String linkToStackTrace(Throwable t) { //noinspection ThrowableResultOfMethodCallIgnored notNull(t, ""); String pastebin = BotUtils.pastebin(BotUtils.getStackTrace(t)); if (pastebin != null) { pastebin += ".txt"; String url = null; try { url = BotUtils.shortenURL(pastebin); } catch (Exception ignored) { } if (url != null) return url; } return null; } /** * Gets the contents of an external URL. * * @param url URL to get contents of * @return Contents * @throws IOException * @throws java.lang.NullPointerException If any argument is null */ public static String getContent(String url) throws IOException { notNull(url, "url was null"); final URL u = new URL(url); final BufferedReader br = new BufferedReader(new InputStreamReader(u.openConnection().getInputStream())); final StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) sb.append(line).append("\n"); return sb.substring(0, sb.length() - 1); // remove last newline } /** * Shortens a URL with is.gd. * * @param url URL to shorten * @return Shortened URL * @throws IOException If an exception occurs encoding or shortening * @throws java.lang.NullPointerException If any argument is null */ public static String shortenURL(String url) throws IOException { notNull(url, "url was null"); final URL shorten = new URL("http://is.gd/create.php?format=simple&url=" + URLEncoder.encode(url, "UTF-8")); return getContent(shorten.toString()); } /** * Gets a string to send a user if help is requested for a command. * * @param ic Command to get help for * @return String to send user * @throws java.lang.NullPointerException If any argument is null */ public static String getHelpString(IRCCommand ic) { notNull(ic, "ic was null"); return ic.getName() + " / Description: " + ic.getDescription() + " / Usage: " + ic.getUsage().replaceAll("(?i)<command>", ic.getName()) + " / Aliases: " + Arrays.toString(ic.getAliases()) + " / Type: " + ic.getCommandType().getDescription(); } /** * Creates a channel-specific command based on JSON. * * @param commandJson JSON of command * @param channel Channel for command * @return ChannelCommand * @throws RuntimeException If there is any error * @throws java.lang.NullPointerException If any argument is null */ public static ChannelCommand createChannelCommand(String commandJson, final String channel) throws RuntimeException { notNull(commandJson, "commandJson was null"); notNull(channel, "channel was null"); final JsonNode jn; try { jn = om.readTree(commandJson); } catch (Exception ex) { String paste = BotUtils.linkToStackTrace(ex); throw new RuntimeException("An error occurred reading that!" + ((paste != null) ? " (" + paste + ")" : "")); } final String name = jn.path("name").asText().trim(); final String desc = jn.path("description").asText().trim(); final String usage = jn.path("usage").asText().trim(); final String auth = jn.path("auth").asText().trim(); final String script = jn.path("script").asText().trim(); final List<String> aliases = new ArrayList<>(); for (String alias : jn.path("aliases").asText().trim().split(",")) { alias = alias.trim(); if (alias.isEmpty()) continue; aliases.add(alias + ":" + channel); } if (name.isEmpty() || desc.isEmpty() || usage.isEmpty() || auth.isEmpty() || script.isEmpty()) { throw new RuntimeException("Invalid JSON."); } final IRCCommand.AuthLevel al; try { al = IRCCommand.AuthLevel.valueOf(auth.toUpperCase()); } catch (IllegalArgumentException e) { throw new RuntimeException("Invalid auth level."); } return new ChannelCommand() { @Override public String getBaseName() { return name; } @Override public String getChannel() { return channel; } @Override public String getJavaScript() { return script; } @Override public String getUsage() { return usage; } @Override public String getDescription() { return desc; } @Override public String[] getAliases() { return aliases.toArray(new String[aliases.size()]); } @Override public AuthLevel getAuthLevel() { return al; } }; } /** * Checks if a hostmask matches a pattern. This replaces "*" with ".+" prior to checking, and it does use regex, as * one would assume. * * @param hostmask Hostmask of a user (regex, * = .+) * @param checkAgainst Hostmask pattern to check against * @return true if hostmask matches checkAgainst, false if otherwise * @throws java.lang.NullPointerException If any argument is null */ public static boolean doesHostmaskMatch(String hostmask, String checkAgainst) { notNull(hostmask, "hostmask was null"); notNull(checkAgainst, "checkAgainst was null"); checkAgainst = checkAgainst.replace("*", ".+"); return hostmask.matches(checkAgainst); } /** * Checks to see if a hostmask is ignored by the bot. * * @param hostmask Hostmask to check * @param ignores List of hostmasks to check against * @return true if hostmask is ignored, false if not * @throws java.lang.NullPointerException If any argument is null */ public static boolean isIgnored(String hostmask, List<String> ignores) { notNull(hostmask, "hostmask was null"); notNull(ignores, "ignores was null"); for (String ignore : ignores) { if (ignore.equals(hostmask)) return true; if (doesHostmaskMatch(hostmask, ignore)) return true; } return false; } /** * Checks to see if a hostmask is ignored by the bot. * * @param hostmask Hostmask to check * @return true if hostmask is ignored, false if not * @throws java.lang.NullPointerException If any argument is null */ public static boolean isIgnored(String hostmask) { notNull(hostmask, "hostmask was null"); return isIgnored(hostmask, RoyalBot.getInstance().getConfig().getIgnores()); } /** * Temporary workaround for PircBotX not returning the right value for {@link org.pircbotx.User#getHostmask()}. * * @param user User to get hostmask of * @return Real hostmask * @throws java.lang.NullPointerException If any argument is null */ public static String generateHostmask(User user) { notNull(user, "user was null"); return user.getNick() + "!" + user.getLogin() + "@" + user.getHostmask(); } /** * Checks to see if a User is authorized in a channel or is a superadmin. * * @param u User to check * @param chan Channel of user * @return true if authorized or superadmin, false if not * @throws java.lang.NullPointerException If any argument is null */ public static boolean isAuthorized(User u, String chan) { notNull(u, "u was null"); notNull(chan, "chan was null"); AuthResponse ar = Auth.checkAuth(u); boolean loggedIn = ar.isLoggedIn() && ar.isValid(); boolean isChanOp = u.getChannelsOpIn().contains(u.getBot().getUserChannelDao().getChannel(chan)); boolean isSuperAdmin = RoyalBot.getInstance().getConfig().getSuperAdmin().equalsIgnoreCase(u.getNick()); return (loggedIn && isChanOp) || isSuperAdmin; } /** * Flips a string upside-down in accordance to the table from the configuration. * * @param toFlip String to flip * @return Flipped string * @throws java.lang.NullPointerException If any argument is null */ public static String flip(String toFlip) { notNull(toFlip, "toFlip was null"); final ConfigurationSection flips = RoyalBot.getInstance().getConfig().getFlipTable(); final StringBuilder sb = new StringBuilder(); for (char c : toFlip.toCharArray()) { final String ch = String.valueOf(c); sb.append(flips.getString(ch, ch)); } return sb.toString(); } /** * Gets an array of all indices of a string. * * @param string Search string * @param of Delimiter * @return Array of ints * @throws java.lang.NullPointerException If any argument is null */ public static int[] indicesOf(String string, String of) { notNull(string, "string was null"); notNull(of, "of was null"); List<Integer> indices = new ArrayList<>(); for (int i = string.indexOf(of); i >= 0; i = string.indexOf(of, i + 1)) indices.add(i); return ArrayUtils.toPrimitive(indices.toArray(new Integer[indices.size()])); } /** * Returns a string of less than or equal to the maximum length provided. If the provided string is more than the * maximum length, the rest of the contents will be removed. * * @param text Text to truncate * @param maxLength Maximum length to allow; must be 0+ * @return Truncated string * @throws java.lang.IllegalArgumentException If maxLength is less than zero * @throws java.lang.NullPointerException If any argument is null */ public static String truncate(String text, int maxLength) { notNull(text, "text was null"); if (maxLength < 0) throw new IllegalArgumentException("maxLength was less than zero"); return text.length() < maxLength ? text : text.substring(0, maxLength); } }