import { TeamSpeak } from "ts3-nodejs-library"; import fetch from "node-fetch"; import fs from "fs"; import jsonfile from "jsonfile"; let botUniqueIdentifier = null; // ------------------------------------------------------- // CONFIG / TOKENS // ------------------------------------------------------- // File paths const tokensFile = "./tokens.json"; const configFile = "./config.json"; // Load config (must have TS server info & apiBaseURL) let config; try { config = JSON.parse(fs.readFileSync(configFile, "utf8")); } catch (error) { console.error("Failed to read config.json:", error); process.exit(1); } // Load the existing tokens from tokens.json function loadTokens() { try { return jsonfile.readFileSync(tokensFile); } catch (error) { console.error("Error reading tokens file:", error); return {}; } } // Save the tokens object to tokens.json function saveTokens(tokenObj) { try { jsonfile.writeFileSync(tokensFile, tokenObj, { spaces: 2 }); } catch (error) { console.error("Error saving tokens file:", error); } } // Keep our tokens in memory const tokens = loadTokens(); // ------------------------------------------------------- // TEAM SPEAK CONNECTION // ------------------------------------------------------- const ts3 = new TeamSpeak({ host: config.TS_HOST, queryport: config.TS_QUERY_PORT, serverport: config.TS_SERVER_PORT, username: config.TS_USERNAME, password: config.TS_PASSWORD, nickname: "Linux", // The displayed name of your bot }); // ------------------------------------------------------- // HELPER: REQUEST A TOKEN VIA DM // ------------------------------------------------------- async function requestTokenViaDM(client) { // If the user already has a token, do nothing if (tokens[client.uniqueIdentifier]) return; console.log(`Requesting token from ${client.nickname} via DM...`); try { // Send a private message to the user to request the token await client.message( `Hello ${client.nickname}! You have no token on record. ` + `Please reply to *this private message* with your token (no ! prefix).` ); } catch (error) { console.error(`Failed to send DM to ${client.nickname}:`, error); } } // ------------------------------------------------------- // API REQUEST FUNCTION // ------------------------------------------------------- async function makeApiRequest(endpoint, token, client, method = "GET", body = null) { const url = `${config.apiBaseURL}${endpoint}`; const options = { method, headers: { "Accept": "application/json", "Content-Type": "application/json", "x-ssh-auth": token, // custom header for auth }, }; if (body) { options.body = JSON.stringify(body); } try { console.log(`Making API request to ${url} for user ${client.nickname}`); const response = await fetch(url, options); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { console.error(`Error making API request for ${client.nickname}:`, error); throw error; } } // ------------------------------------------------------- // GET TOKEN (CHECK & REQUEST IF MISSING) // ------------------------------------------------------- async function getTokenForClient(client) { // If we already have the token, return it if (tokens[client.uniqueIdentifier]) { return tokens[client.uniqueIdentifier]; } // Otherwise, request token via DM await requestTokenViaDM(client); return null; } // ------------------------------------------------------- // EVENT: TEXT MESSAGE // ------------------------------------------------------- ts3.on("textmessage", async (event) => { const { targetmode, msg } = event; const message = msg.trim(); const senderNickname = event.invoker.nickname; console.log(`${senderNickname} sent message: "${message}" (targetmode=${targetmode})`); // Attempt to get the client object for more info let client; try { client = await ts3.getClientById(event.invoker.clid); } catch (error) { console.error("Error fetching client object:", error); return; } // // 1) PRIVATE MESSAGES (DM) // // For private messages: if (targetmode === 1) { // If user sends something that doesn't start with "!", treat it as a potential token if (!message.startsWith("!")) { // 1) Skip if it's the bot's own message if (event.invoker.uniqueIdentifier === botUniqueIdentifier) { // It's the bot talking to itself—ignore return; } // Otherwise, handle the incoming token const client = await ts3.getClientById(event.invoker.clid); if (client.nickname == "Linux") return console.log(`Potential token received from ${client.nickname}: "${message}"`); tokens[client.uniqueIdentifier] = message; saveTokens(tokens); // Confirm receipt await client.message("Your token has been saved! You can now use channel commands."); return; } // If it does start with "!", you can handle optional DM commands here. // For example, let’s do a simple DM-only command: const dmCommand = message.slice(1).toLowerCase(); switch (dmCommand) { case "ping": await client.message("Pong! (DM response)"); break; default: await client.message("Unknown DM command. You can just send your token or use `!ping` here."); break; } return; } // // 2) CHANNEL (targetmode=2) OR SERVER (targetmode=3) MESSAGES // if (targetmode === 2 || targetmode === 3) { // We only handle commands that start with "!" if (!message.startsWith("!")) return; // Extract command and arguments const parts = message.slice(1).split(" "); // remove "!" const command = parts[0].toLowerCase(); const args = parts.slice(1); let token = await getTokenForClient(client); // If user doesn't have a token, let them know in the channel, then DM them if (!token) { // Let them know publicly await ts3.sendTextMessage( event.target, // either channel ID or virtual server ID targetmode, `${senderNickname}, you have no token on record! Check your private messages.` ); // The DM request was already done in getTokenForClient, so we’re done return; } // Example: store the working directory per user (for !x command): const userWorkingDirectories = new Map(); // Helper function for "../" logic in directories function removeLastDirectoryPartOf(thePath, levelsUp) { let parts = thePath.split("/").filter(Boolean); for (let i = 0; i < levelsUp; i++) { parts.pop(); } return "/" + parts.join("/"); } // If we do have a token, let's handle the commands try { switch (command) { // // Command: !hello // case "hello": { const helloResponse = await makeApiRequest("/hello", token, client); const reply = `API says: ${helloResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !name // case "name": { const nameResponse = await makeApiRequest("/name", token, client); const reply = `Your API username: ${nameResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !stats // case "stats": { const statsResponse = await makeApiRequest("/stats", token, client); // Example: just stringify or format it how you like const reply = `Stats: ${JSON.stringify(statsResponse)}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !uptime // case "uptime": { const uptimeResponse = await makeApiRequest("/uptime", token, client); const reply = `Uptime: ${uptimeResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !start // case "start": { const startResponse = await makeApiRequest("/start", token, client); const reply = `Start Server:\nStatus: Success\nMessage: ${startResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !stop // case "stop": { const stopResponse = await makeApiRequest("/stop", token, client); const reply = `Stop Server:\nStatus: Success\nMessage: ${stopResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !restart // case "restart": { const restartResponse = await makeApiRequest("/restart", token, client); const reply = `Restart Server:\nStatus: Success\nMessage: ${restartResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !info // case "info": { const infoResponse = await makeApiRequest("/info", token, client); // Safely extract data const containerName = infoResponse.data?.name || "N/A"; const ipAddress = infoResponse.data?.IPAddress || "N/A"; const macAddress = infoResponse.data?.MacAddress || "N/A"; const memory = infoResponse.data?.memory || "N/A"; const cpus = infoResponse.data?.cpus || "N/A"; const restartPolicy = infoResponse.data?.restartPolicy?.Name || "N/A"; const restarts = (infoResponse.data?.restarts !== undefined) ? infoResponse.data.restarts : "N/A"; const status = infoResponse.data?.state?.Status || "Unknown"; const pid = infoResponse.data?.state?.Pid || "N/A"; const startedAt = infoResponse.data?.state?.StartedAt || "N/A"; const createdAt = infoResponse.data?.created || "N/A"; const reply = [ "Container Info:", `Name: ${containerName}`, `IP Address: ${ipAddress}`, `MAC Address: ${macAddress}`, `Memory: ${memory}`, `CPUs: ${cpus}`, `Restart Policy: ${restartPolicy}`, `Restarts: ${restarts}`, `Status: ${status}`, `PID: ${pid}`, `Started At: ${startedAt}`, `Created At: ${createdAt}` ].join("\n"); await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !time // case "time": { const timeResponse = await makeApiRequest("/time", token, client); const reply = `Container Expire Time:\nExpire Date: ${timeResponse.expireDate}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !root-password // case "root-password": { const rootPassResponse = await makeApiRequest("/rootpass", token, client); const reply = `Root Password:\nNew Root Password: ${rootPassResponse.newRootPass}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !new-api-key // case "new-api-key": { const newKeyResponse = await makeApiRequest("/new-key", token, client); const reply = `New API Key: ${newKeyResponse.newAPIKey}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !key-expire-time // case "key-expire-time": { const keyTimeResponse = await makeApiRequest("/key-time", token, client); const reply = `API Key Expire Time:\nExpire Date: ${keyTimeResponse.expireDate}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !x // Handles directory changes and command execution // case "x": { // Combine args into a single string (e.g., user typed: !x cd /root) const userCommand = args.join(" "); // Identify user by their unique TS3 ID const sshSurfID = event.invoker.uid; // or event.invoker.clid if (!userWorkingDirectories.has(sshSurfID)) { userWorkingDirectories.set(sshSurfID, "/"); // Default to root } let userPWD = userWorkingDirectories.get(sshSurfID) || "/"; if (userCommand.startsWith("cd")) { const argscmd = userCommand.replace("cd ", "").trim(); // cd .. if (argscmd === "..") { if (userPWD !== "/") { const newPWD = userPWD.split("/").slice(0, -1).join("/") || "/"; userPWD = newPWD; userWorkingDirectories.set(sshSurfID, newPWD); const reply = `Directory changed to: ${newPWD}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); } else { const reply = `Already at the root directory: ${userPWD}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); } break; } // cd ~ if (argscmd === "~") { userPWD = "/root"; userWorkingDirectories.set(sshSurfID, userPWD); const reply = `Directory changed to: ${userPWD}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // cd /some/absolute/path or cd relativePath let newPWD; if (argscmd.startsWith("/")) { newPWD = argscmd; } else { newPWD = `${userPWD}/${argscmd}`; } // Remove double slashes newPWD = newPWD.replace(/([^:]\/)\/+/g, "$1"); // If user typed "cd ../" or multiple "../" if (argscmd.includes("../")) { const numDirsBack = argscmd.split("../").length - 1; newPWD = removeLastDirectoryPartOf(userPWD, numDirsBack); } userPWD = newPWD; userWorkingDirectories.set(sshSurfID, newPWD); const reply = `Directory changed to: ${newPWD}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // Otherwise, execute the command in the container via /exec const execResponse = await makeApiRequest("/exec", token, client, "post", { cmd: userCommand, pwd: userPWD }); // Handle large output by truncating or chunking for TS3 let stdoutMsg = execResponse.stdout || "No output"; let stderrMsg = execResponse.stderr || ""; // const MAX_TS3_MESSAGE_LENGTH = 800; // if (stdoutMsg.length > MAX_TS3_MESSAGE_LENGTH) { // stdoutMsg = stdoutMsg.slice(0, MAX_TS3_MESSAGE_LENGTH) + "...[truncated]"; // } // if (stderrMsg.length > MAX_TS3_MESSAGE_LENGTH) { // stderrMsg = stderrMsg.slice(0, MAX_TS3_MESSAGE_LENGTH) + "...[truncated]"; // } let reply = `Executed: ${userCommand}\n\nOutput:\n\`\`\`bash\n${stdoutMsg}\n\`\`\``; if (stderrMsg.trim()) { reply += `\n\nError:\n${stderrMsg}`; } await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Command: !notify // case "notify": { // Combine args as the notification message const messageText = args.join(" "); const notifyResponse = await makeApiRequest("/notify", token, client, "post", { message: messageText }); const reply = `Notification:\nStatus: Success\nMessage: ${notifyResponse.message}`; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } // // Unrecognized command // default: { const reply = "Command not recognized."; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); break; } } } catch (error) { console.error("Command error:", error); const reply = "An error occurred while processing your request."; await ts3.sendTextMessage(event.target, targetmode, `${senderNickname}, ${reply}`); } } }); // ------------------------------------------------------- // EVENT: CLIENT CONNECT // ------------------------------------------------------- ts3.on("clientconnect", async (event) => { try { const client = await ts3.getClientById(event.client.clid); console.log(`${client.nickname} connected.`); // Optionally request their token in DM if missing await requestTokenViaDM(client); } catch (error) { console.error("Failed to handle clientconnect event:", error); } }); // ------------------------------------------------------- // EVENT: CLIENT DISCONNECT // ------------------------------------------------------- ts3.on("clientdisconnect", (event) => { console.log(`A client disconnected: ${event.client?.nickname || "Unknown"}`); }); // ------------------------------------------------------- // EVENT: ERROR // ------------------------------------------------------- ts3.on("error", (error) => { console.error("TeamSpeak error event:", error.message); }); // ------------------------------------------------------- // ON READY // ------------------------------------------------------- ts3.on("ready", async () => { console.log("TeamSpeak bot is ready and connected."); try { // whoami() gives information about this query client (the bot itself) const whoAmI = await ts3.whoami(); botUniqueIdentifier = whoAmI.virtualserverUniqueIdentifier; console.log("Bot's unique ID is:", botUniqueIdentifier); } catch (error) { console.error("Error retrieving bot ID:", error); } }); // ------------------------------------------------------- // GRACEFUL SHUTDOWN // ------------------------------------------------------- process.on("SIGINT", () => { ts3.quit().then(() => { console.log("Disconnected from TeamSpeak server."); process.exit(0); }); });