Lumi/src/services/commands.js
2026-07-17 22:10:00 +02:00

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
};