170 lines
4.2 KiB
JavaScript
170 lines
4.2 KiB
JavaScript
const vm = require("vm");
|
|
const { spawn } = require("child_process");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const {
|
|
prepareJavaScriptCommand,
|
|
preparePythonCommand,
|
|
resolveJavaScriptHandler
|
|
} = require("./command-code");
|
|
|
|
function buildCommandContext({ platform, user, message, args, argsText }) {
|
|
return {
|
|
platform,
|
|
user,
|
|
message,
|
|
args: args || [],
|
|
argsText: argsText || ""
|
|
};
|
|
}
|
|
|
|
async function runAdvancedCommand({ code, language }, ctx) {
|
|
if (language === "python") {
|
|
return await runPythonCommand(code, ctx);
|
|
}
|
|
return await runJsCommand(code, ctx);
|
|
}
|
|
|
|
async function runJsCommand(code, ctx) {
|
|
const logs = [];
|
|
const safeConsole = {
|
|
log: (...args) => logs.push(args.join(" "))
|
|
};
|
|
const sandbox = {
|
|
ctx,
|
|
console: safeConsole,
|
|
module: { exports: {} },
|
|
exports: {}
|
|
};
|
|
const context = vm.createContext(sandbox);
|
|
const script = new vm.Script(prepareJavaScriptCommand(code), { filename: "command.js" });
|
|
script.runInContext(context, { timeout: 1000 });
|
|
|
|
const handler = resolveJavaScriptHandler(context);
|
|
if (typeof handler !== "function") {
|
|
throw new Error("Dynamic command did not create a runnable handler.");
|
|
}
|
|
const result = handler(ctx);
|
|
if (result && typeof result.then === "function") {
|
|
return await promiseWithTimeout(result, 1500);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function runPythonCommand(code, ctx) {
|
|
return await new Promise((resolve, reject) => {
|
|
const payload = JSON.stringify(ctx);
|
|
const encoded = Buffer.from(preparePythonCommand(code), "utf8").toString("base64");
|
|
const script = `
|
|
import base64, json, sys, traceback
|
|
ctx = json.loads(sys.stdin.read() or "{}")
|
|
code = base64.b64decode("${encoded}").decode("utf-8")
|
|
globals_dict = {}
|
|
try:
|
|
exec(code, globals_dict)
|
|
if "run" not in globals_dict:
|
|
raise Exception("Dynamic command did not create a runnable handler.")
|
|
result = globals_dict["run"](ctx)
|
|
if result is None:
|
|
sys.exit(0)
|
|
if isinstance(result, (dict, list)):
|
|
print(json.dumps(result))
|
|
else:
|
|
print(str(result))
|
|
except Exception:
|
|
traceback.print_exc()
|
|
sys.exit(2)
|
|
`;
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-cmd-"));
|
|
const filePath = path.join(tmpDir, "runner.py");
|
|
fs.writeFileSync(filePath, script, "utf8");
|
|
|
|
const child = spawn("python", ["-u", filePath], {
|
|
stdio: ["pipe", "pipe", "pipe"]
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
const timeout = setTimeout(() => {
|
|
child.kill("SIGKILL");
|
|
reject(new Error("Python command timed out."));
|
|
}, 2000);
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk.toString();
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
child.on("error", (error) => {
|
|
clearTimeout(timeout);
|
|
cleanupTemp(tmpDir);
|
|
reject(error);
|
|
});
|
|
child.on("close", (code) => {
|
|
clearTimeout(timeout);
|
|
cleanupTemp(tmpDir);
|
|
if (code && code !== 0) {
|
|
reject(new Error(stderr || "Python command failed."));
|
|
return;
|
|
}
|
|
resolve(stdout.trim());
|
|
});
|
|
|
|
child.stdin.write(payload);
|
|
child.stdin.end();
|
|
});
|
|
}
|
|
|
|
function cleanupTemp(dir) {
|
|
try {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors.
|
|
}
|
|
}
|
|
|
|
function normalizeCommandResult(result) {
|
|
if (result === null || result === undefined) {
|
|
return "";
|
|
}
|
|
if (typeof result === "string") {
|
|
return result;
|
|
}
|
|
if (typeof result === "number" || typeof result === "boolean") {
|
|
return String(result);
|
|
}
|
|
if (typeof result === "object" && result.content) {
|
|
return String(result.content);
|
|
}
|
|
try {
|
|
return JSON.stringify(result);
|
|
} catch {
|
|
return String(result);
|
|
}
|
|
}
|
|
|
|
function promiseWithTimeout(promise, timeoutMs) {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error("Command timed out."));
|
|
}, timeoutMs);
|
|
promise
|
|
.then((value) => {
|
|
clearTimeout(timeout);
|
|
resolve(value);
|
|
})
|
|
.catch((error) => {
|
|
clearTimeout(timeout);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
buildCommandContext,
|
|
runAdvancedCommand,
|
|
normalizeCommandResult
|
|
};
|