import java.io.*; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; import org.luaj.vm2.*; import org.luaj.vm2.compiler.*; import org.luaj.vm2.lib.*; import org.luaj.vm2.lib.jse.*; import org.pircbotx.Channel; import org.pircbotx.PircBotX; import org.pircbotx.User; import pl.shockah.Delegate; import pl.shockah.Helper; import pl.shockah.ZeroInputStream; import pl.shockah.shocky.Cache; import pl.shockah.shocky.Data; import pl.shockah.shocky.Module; import pl.shockah.shocky.ScriptModule; import pl.shockah.shocky.Utils; import pl.shockah.shocky.cmds.Command; import pl.shockah.shocky.cmds.CommandCallback; import pl.shockah.shocky.cmds.Parameters; import pl.shockah.shocky.cmds.Command.EType; import pl.shockah.shocky.interfaces.ILua; import pl.shockah.shocky.sql.Factoid; import pl.shockah.shocky.threads.*; public class ModuleLua extends ScriptModule implements ResourceFinder { public static final File binary = new File(Data.lastSave, "luastate.bin").getAbsoluteFile(); public static final File scripts = new File("data", "lua").getAbsoluteFile(); public static final String luaHash = "lua"; public static final String cmdFuncHash = "cmdfunc"; public static final String channelHash = "luachannel"; protected Command cmd, reset; private final SandboxThreadGroup sandboxGroup = new SandboxThreadGroup("lua"); private final ThreadFactory sandboxFactory = new SandboxThreadFactory(sandboxGroup); LuaTable env = null; LuaValue envMeta = null; @Override public String name() { return "lua"; } @Override public String identifier() { return "lua"; } @Override public void onEnable(File dir) { Command.addCommands(this, cmd = new CmdLua(), reset = new CmdLuaReset()); initLua(); } @Override public void onDisable() { Command.removeCommands(cmd, reset); } public File[] getReadableFiles() { return new File[] { binary, scripts }; } @Override public void onDataSave(File dir) { if (env == null) return; File file = new File(dir, "luastate.bin"); LuaValue value = env.get("irc"); if (!value.istable()) return; File temp = null; try { try { temp = File.createTempFile("shocky", ".tmp"); } catch (IOException e1) { throw new RuntimeException(e1); } System.out.printf("File: %s Temp: %s", file.getAbsolutePath(), temp.getAbsolutePath()).println(); DataOutputStream os = new DataOutputStream(new FileOutputStream(temp)); writeValue(os, value); os.close(); if (file.exists()) file.delete(); temp.renameTo(file); } catch (Exception e) { e.printStackTrace(); } finally { if (temp != null && temp.exists()) temp.delete(); } } private void initLua() { BaseLib.FINDER = this; env = new LuaTable(); env.load(new JseBaseLib()); env.load(new PackageLib()); env.load(new TableLib()); env.load(new StringLib()); env.load(new JseMathLib()); env.load(new JseOsLib()); env.load(new BotLib()); env.load(new JSONLib()); env.load(new BitLib()); env.rawset("print", LuaValue.NIL); env.rawset("pcall", LuaValue.NIL); env.rawset("xpcall", LuaValue.NIL); env.rawset("sleep", new SleepFunction()); env.rawset("cmd", new CmdData()); for (Module module : Module.getModules()) { if (module instanceof ILua) ((ILua)module).setupLua(env); } LuaBoolean.s_metatable = LuaImmutableTable.immutableOf(LuaBoolean.s_metatable); LuaFunction.s_metatable = LuaImmutableTable.immutableOf(LuaFunction.s_metatable); LuaNil.s_metatable = LuaImmutableTable.immutableOf(LuaNil.s_metatable); LuaNumber.s_metatable = LuaImmutableTable.immutableOf(LuaNumber.s_metatable); LuaString.s_metatable = LuaImmutableTable.immutableOf(LuaString.s_metatable); LuaThread.s_metatable = LuaImmutableTable.immutableOf(LuaThread.s_metatable); DataInputStream is = null; LuaValue table = new LuaTable(); try { Class.forName("org.luaj.vm2.lib.LuaState"); Class.forName("ModuleLua$CmdFunction"); Class.forName("pl.shockah.Delegate$Instance"); if (!scripts.exists()) scripts.mkdirs(); if (binary.exists()) { is = new DataInputStream(new FileInputStream(binary)); table = readValue(is); } } catch (Exception e) { e.printStackTrace(); } finally { env.rawset("irc", table); try { if (is != null) is.close(); } catch (IOException e) {} } LuaThread.setGlobals(env); LuaC.install(); BaseLib.instance.STDIN = new ZeroInputStream(); envMeta = new LuaTable(); envMeta.rawset(LuaValue.INDEX, env); envMeta = LuaImmutableTable.immutableOf(envMeta); } @Override public InputStream findResource(String filename) { File[] files = scripts.listFiles(); for (int i = 0; i < files.length; i++) { try { String name = files[i].getName(); if (filename.indexOf('.') == -1) name = name.split("\\.")[0]; if (name.equalsIgnoreCase(filename)) { return new FileInputStream(files[i]); } } catch (FileNotFoundException e) { } } return null; } public static void writeTable(DataOutputStream os, LuaValue value) throws IOException { LuaTable table = value.checktable(); LuaValue[] keys = table.keys(); writeEncodedInt(os, keys.length); for (LuaValue value2 : keys) { writeValue(os, value2); writeValue(os, table.get(value2)); } } public static void writeValue(DataOutputStream os, LuaValue value) throws IOException { if (value.type() == LuaValue.TFUNCTION && !value.isclosure()) { writeEncodedInt(os, LuaValue.TNIL); } else { writeEncodedInt(os, value.type()); } switch (value.type()) { case LuaValue.TBOOLEAN: os.writeBoolean(value.checkboolean()); break; case LuaValue.TINT: writeEncodedInt(os, value.checkint()); break; case LuaValue.TTABLE: writeTable(os, value); break; case LuaValue.TNUMBER: os.writeDouble(value.checkdouble()); break; case LuaValue.TSTRING: byte[] bytes = value.checkjstring().getBytes(Helper.utf8); writeEncodedInt(os, bytes.length); os.write(bytes); break; case LuaValue.TFUNCTION: if (value.isclosure()) { LuaClosure closure = value.checkclosure(); DumpState.dump(closure.p, os, true); } break; default: case LuaValue.TNIL: case LuaValue.TNONE: break; } } public LuaValue readTable(DataInputStream is) throws IOException { LuaTable table = new LuaTable(); int size = readEncodedInt(is); for (int i = 0; i < size; i++) { LuaValue key = readValue(is); LuaValue value = readValue(is); if (key.type() != LuaValue.TNIL && key.type() != LuaValue.TNONE) table.set(key, value); } return table; } public LuaValue readValue(DataInputStream is) throws IOException { int type = readEncodedInt(is); switch (type) { case LuaValue.TBOOLEAN: return LuaValue.valueOf(is.readBoolean()); case LuaValue.TINT: return LuaValue.valueOf(readEncodedInt(is)); case LuaValue.TTABLE: return readTable(is); case LuaValue.TNUMBER: return LuaValue.valueOf(is.readDouble()); case LuaValue.TSTRING: int length = readEncodedInt(is); byte[] bytes = new byte[length]; is.read(bytes); return LuaValue.valueOf(new String(bytes, Helper.utf8)); case LuaValue.TFUNCTION: return LoadState.load(is, "script", env); default: case LuaValue.TNIL: return LuaNil.NIL; case LuaValue.TNONE: return LuaNil.NONE; } } public static void writeEncodedInt(OutputStream os, int value) throws IOException { long num; for (num = value & 0xFFFFFFFFL; num >= 0x80; num >>= 7) os.write((int) ((num & 0x7F) | 0x80)); os.write((int) num); } public static int readEncodedInt(InputStream is) throws IOException { int num = 0; int shift = 0; while (shift < 35) { int b = is.read(); num |= (b & 0x7F) << shift; shift += 7; if ((b & 0x80) == 0) return num; } return Integer.MAX_VALUE; } private LuaValue valueOf(Object obj) { if (obj == null) { return LuaValue.NIL; } else if (obj.getClass().isArray()) { Object[] a = (Object[])obj; LuaTable t = new LuaTable(); for (int i = 0; i < a.length; ++i) t.rawset(i+1, valueOf(a[i])); return t; } else if (obj instanceof Map) { LuaTable t = new LuaTable(); for (Entry<?,?> entry : ((Map<?,?>)obj).entrySet()) t.rawset(valueOf(entry.getKey()), valueOf(entry.getValue())); return t; } else if (obj instanceof String) { return LuaValue.valueOf((String)obj); } else if (obj instanceof Double) { return LuaValue.valueOf((Double)obj); } else if (obj instanceof Integer) { return LuaValue.valueOf((Integer)obj); } else if (obj instanceof Long) { return LuaValue.valueOf((Long)obj); } else if (obj instanceof Boolean) { return LuaValue.valueOf((Boolean)obj); } else if (obj instanceof LuaValue) { return (LuaValue)obj; } else { return null; } } @Override public String parse(Cache cache, PircBotX bot, Channel channel, User sender, Factoid factoid, String code, String message) { if (code == null) return ""; String output = null; LuaState state = new LuaState(bot, channel, sender, cache); LuaTable subTable = new LuaTable(); subTable.setmetatable(envMeta); Map<String,Object> params = getParams(bot, channel, sender, message, factoid); params.put("channel", ChannelData.getChannelData(state, channel)); for (Map.Entry<String,Object> pair : params.entrySet()) subTable.rawset(pair.getKey(),valueOf(pair.getValue())); LuaClosure func = null; Object obj = state.get(luaHash, code); if (obj instanceof LuaClosure) { func = (LuaClosure) obj; func.setfenv(subTable); } final ExecutorService service = Executors.newSingleThreadExecutor(sandboxFactory); Print print = null; try { if (func == null) { func = LuaC.instance.load(new ByteArrayInputStream(code.getBytes(Helper.utf8)), "script", subTable).checkclosure(); state.put(luaHash, code, func); } LuaRunner r = new LuaRunner(func,state); print = r.printer; Future<String> f = service.submit(r); output = f.get(30, TimeUnit.SECONDS); } catch (LuaError e) { output = e.getMessage(); } catch (TimeoutException e) { output = "Script timed out"; } catch (Exception e) { throw new RuntimeException(e); } finally { service.shutdown(); LuaState.clearState(state); if (print != null) print.dispose(); } if (output == null || output.isEmpty()) return null; return Utils.mungeAllNicks(channel, 2, output); } public class CmdLua extends ScriptCommand { public String command() {return "lua";} public String help(Parameters params) { return "lua\nlua {code} - runs Lua code"; } } public class CmdLuaReset extends Command { public String command() { return "resetlua"; } public String help(Parameters params) { return "resetlua\nresetlua - resets Lua environment"; } public void doCommand(Parameters params, CommandCallback callback) { callback.type = EType.Notice; try { initLua(); callback.append("Done."); } catch (Throwable e) { callback.append(e.getMessage()); } } } public class LuaRunner implements Callable<String> { private final LuaClosure func; private final LuaState state; public final Print printer; public LuaRunner(LuaClosure f, LuaState s) { func = f; state = s; printer = new Print(f.getfenv()); f.getfenv().rawset("print", printer); } @Override public String call() throws Exception { try { /* * LuaValue[] stack = new LuaValue[func.p.maxstacksize]; * System.arraycopy(LuaValue.NILS, 0, stack, 0, * func.p.maxstacksize); Varargs out = (Varargs) * closureExecute.invoke(func, stack, LuaValue.NONE); */ LuaState.setState(state); Varargs out = func.invoke(); if (printer.hasOutput()) return printer.getOutput(); if (out != LuaValue.NONE) return out.eval().tojstring(); return null; /* * StringBuilder sb = new StringBuilder(stack.length*20); for * (int i = 0; i < stack.length;i++) { LuaValue v = stack[i]; if * (v == LuaValue.NIL || v == LuaValue.NONE) continue; * sb.append(v); sb.append('('); sb.append(v.typename()); * sb.append(')'); sb.append('\n'); } return sb.toString(); */ /*} catch (LuaError ex) { ex.printStackTrace(System.out); return ex.getMessage();*/ } finally { LuaState.clearState(); printer.dispose(); } } } public static class Print extends VarArgFunction { private final LuaValue env; private final ByteArrayOutputStream array; private final PrintStream stream; private boolean disposed = false; public Print(LuaValue luaValue) { this.env = luaValue; array = new ByteArrayOutputStream(); stream = new PrintStream(array); } @Override public Varargs invoke(Varargs args) { if (disposed) throw new LuaError("Print used while disposed."); LuaValue tostring = env.get("tostring"); for (int i = 1, n = args.narg(); i <= n; i++) { if (i > 1) stream.write('\t'); LuaString s = tostring.call(args.arg(i)).strvalue(); int z = s.indexOf((byte) 0, 0); stream.write(s.m_bytes, s.m_offset, z >= 0 ? z : s.m_length); } stream.println(); return NONE; } public boolean hasOutput() { return array.size() > 0; } public String getOutput() throws UnsupportedEncodingException { return array.toString("UTF-8"); } public void dispose() { disposed = true; stream.close(); } } public static class CmdData extends LuaValue { @Override public int type() { return LuaValue.TUSERDATA; } @Override public String typename() { return "userdata"; } @Override public LuaValue get(LuaValue key) { return CmdFunction.create(key.checkjstring()); } } private static class CmdFunction extends OneArgFunction { final Command cmd; private CmdFunction(Command factoid) { this.cmd = factoid; } public synchronized static LuaValue create(String cmd) { if (cmd == null || cmd.isEmpty()) return NIL; LuaState state = LuaState.getState(); if (state == null) return NIL; Command cmdobj = Command.getCommand(state.bot, state.user, state.chan, EType.Channel, new CommandCallback(), cmd); if (cmdobj == null) return NIL; Object obj = state.get(cmdFuncHash, cmdobj.command()); if (obj instanceof CmdFunction) return (CmdFunction) obj; CmdFunction function = new CmdFunction(cmdobj); state.put(cmdFuncHash, cmdobj.command(), function); return function; } @Override public LuaValue call(LuaValue arg) { LuaState state = LuaState.getState(); if (state == null) return NIL; String args = arg.optjstring(""); Parameters params = new Parameters(state.bot, EType.Channel, state.chan, state.user, args); CommandCallback callback = new CommandCallback(); //try { cmd.doCommand(params, callback); if (callback.type != EType.Channel) return NIL; return valueOf(callback.output.toString()); /*} catch (Exception e) { throw new LuaError(e); }*/ } } private static class ChannelData extends LuaValue { public final Channel channel; public LuaFunction isOp; public LuaFunction isVoiced; private static final Delegate<Channel,Boolean> opMethod = Delegate.create(Channel.class, "isOp", User.class); private static final Delegate<Channel,Boolean> voiceMethod = Delegate.create(Channel.class, "hasVoice", User.class); public static LuaValue getChannelData(LuaState state, Channel channel) { if (state == null || channel == null) return NIL; if (state.containsKey(channelHash, channel)) return (LuaValue) state.get(channelHash, channel); ChannelData data = new ChannelData(channel); state.put(channelHash,channel, data); return data; } private ChannelData(Channel channel) { this.channel = channel; } @Override public int type() { return LuaValue.TUSERDATA; } @Override public String typename() { return "userdata"; } @Override public LuaValue get(LuaValue key) { String name = key.checkjstring(); if (name.equals("topic")) return valueOf(channel.getTopic()); else if (name.equals("name")) return valueOf(channel.getName()); else if (name.equals("isop")) { if (isOp == null) isOp = new ChannelFunction(channel, opMethod); return isOp; } else if (name.equals("isvoiced")) { if (isVoiced == null) isVoiced = new ChannelFunction(channel, voiceMethod); return isVoiced; } else if (name.equals("users")) return listOfUsers(channel.getUsers()); else if (name.equals("ops")) return listOfUsers(channel.getOps()); else if (name.equals("voiced")) return listOfUsers(channel.getVoices()); return super.get(key); } private static LuaValue listOfUsers(Collection<User> users) { LuaValue[] values = new LuaValue[users.size()]; int i = 0; for (User user : users) values[i++] = valueOf(user.getNick()); return listOf(values); } } private static class ChannelFunction extends OneArgFunction { public final Channel channel; public final Delegate<Channel,Boolean>.Instance method; public ChannelFunction(Channel channel, Delegate<Channel,Boolean> method) { this.channel = channel; this.method = method.instance(channel); } @Override public LuaValue call(LuaValue arg) { User user = channel.getBot().getUser(arg.checkjstring()); if (user == null) return NIL; Boolean ret = method.invoke(user); if (ret != null) return valueOf(ret.booleanValue()); return NIL; } } private static class SleepFunction extends OneArgFunction { @Override public LuaValue call(LuaValue arg) { try { Thread.sleep(arg.checkint()*1000); } catch (InterruptedException e) { } return NIL; } } }