package org.royaldev.royalbot.commands; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.mozilla.javascript.ClassShutter; import org.mozilla.javascript.Context; import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.NativeJavaObject; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrapFactory; import org.pircbotx.hooks.events.MessageEvent; import org.pircbotx.hooks.types.GenericMessageEvent; import org.royaldev.royalbot.BotUtils; import org.royaldev.royalbot.RoyalBot; import java.util.HashMap; import java.util.Map; public abstract class ChannelCommand extends NoticeableCommand { private final RoyalBot rb = RoyalBot.getInstance(); private final ObjectMapper om = new ObjectMapper(); static { ContextFactory.initGlobal(new SandboxContextFactory()); } /** * Gets the name of the command without the channel appended. * * @return Name of the command */ public abstract String getBaseName(); /** * Gets the channel for this command to execute in. * * @return Channel to execute in */ public abstract String getChannel(); /** * Gets the JavaScript for the command. * * @return JavaScript in String */ public abstract String getJavaScript(); /** * This executes the command. In ChannelCommands, this will set up a Context for Rhino to use to execute the * command's JavaScript, which will be obtained from {@link #getJavaScript()}. Two global variables will be passed * to the script: <code>event</code> and <code>args</code>, the same that are used in this method. * <code>event</code> will always be a {@link MessageEvent}. If any exceptions occur while the JavaScript is being * processed, they will be caught and pasted. * * @param event Event of receiving command * @param callInfo Information received at the calling of this command * @param args Arguments passed to the command */ @Override public final void onCommand(GenericMessageEvent event, CallInfo callInfo, String[] args) { if (!(event instanceof MessageEvent)) return; // these commands should only be channel messages final MessageEvent me = (MessageEvent) event; final Context c = ContextFactory.getGlobal().enterContext(); c.setClassShutter(new ClassShutter() { @Override public boolean visibleToScripts(String className) { if (className.equals("org.royaldev.royalbot.BotUtils")) return true; // allow BotUtils else if (className.equals("org.pircbotx.PircBotX")) return false; // no bot access else if (className.startsWith("org.royaldev.royalbot")) return false; // no package access return true; } }); final Scriptable s = c.initStandardObjects(); ScriptableObject.putProperty(s, "event", Context.javaToJS(me, s)); // provide message event for ease ScriptableObject.putProperty(s, "args", Context.javaToJS(args, s)); // supply arguments try { c.evaluateString(s, getJavaScript(), getName(), 1, null); } catch (Throwable t) { if (t instanceof OutOfMemoryError) { rb.getLogger().warning("Channel command (\"" + getName() + "\") produced OutOfMemoryError! Removing."); rb.getCommandHandler().unregister(getName()); } final String url = BotUtils.linkToStackTrace(t); notice(event, "Exception!" + ((url != null) ? " (" + url + ")" : "")); } finally { Context.exit(); } } @Override public final CommandType getCommandType() { return CommandType.MESSAGE; } @Override public final String getName() { return getBaseName() + ":" + getChannel(); } /** * Writes the command out in JSON, ready for use with a command maker. * * @return Command as a JSON string */ @Override public final String toString() { final Map<String, Object> data = new HashMap<>(); data.put("name", getBaseName()); final StringBuilder aliases = new StringBuilder(); for (String alias : getAliases()) { String[] split = alias.split(":#"); aliases.append(StringUtils.join(split, ":#", 0, split.length - 1)).append(","); } if (aliases.length() > 0) data.put("aliases", aliases.substring(0, aliases.length() - 1)); data.put("description", getDescription()); data.put("usage", getUsage()); data.put("auth", getAuthLevel().name()); data.put("script", getJavaScript()); try { return om.writeValueAsString(data); } catch (Exception e) { return "{}"; } } private static class SandboxContextFactory extends ContextFactory { @Override protected Context makeContext() { TimedContext tcx = new TimedContext(this); tcx.setOptimizationLevel(-1); tcx.setInstructionObserverThreshold(500); tcx.setWrapFactory(new SandboxWrapFactory()); return tcx; } @Override protected void observeInstructionCount(Context cx, int instructionCount) { TimedContext tcx = (TimedContext) cx; long currentTime = System.currentTimeMillis(); if (currentTime - tcx.startTime > 7500L) throw new Error("Command ran for too long (longer than 7.5 seconds)!"); } @Override protected Object doTopCall(org.mozilla.javascript.Callable callable, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { TimedContext tcx = (TimedContext) cx; tcx.startTime = System.currentTimeMillis(); return super.doTopCall(callable, cx, scope, thisObj, args); } private static class TimedContext extends Context { long startTime; private TimedContext(SandboxContextFactory scf) { super(scf); } } } private static class SandboxWrapFactory extends WrapFactory { @Override public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType) { return new SandboxNativeJavaObject(scope, javaObject, staticType); } } private static class SandboxNativeJavaObject extends NativeJavaObject { public SandboxNativeJavaObject(Scriptable scope, Object javaObject, Class staticType) { super(scope, javaObject, staticType); } @Override public Object get(String name, Scriptable start) { if (name.equals("getClass")) return NativeJavaObject.NOT_FOUND; // no reflection else if (name.equals("getBot")) return NativeJavaObject.NOT_FOUND; // no bot access return super.get(name, start); } } }