sshsurf-ts3-bot/teamspeak.js
2025-01-20 17:13:07 -05:00

587 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, lets 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 were 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: !help
//
case "help": {
// Construct a help message listing each command
const helpMessage = [
"Here are the available commands:",
"• !hello - returns a 'hello' from the API",
"• !name - shows your API username",
"• !stats - shows container stats",
"• !uptime - shows container uptime",
"• !start / !stop / !restart - controls the container",
"• !info - shows container info (IP, status, etc.)",
"• !time - shows container expire time",
"• !root-password - generates a new root password",
"• !new-api-key - generates a new API key",
"• !key-expire-time - shows when current key expires",
"• !x <command> - execute a command in the container shell",
"• !notify <message> - send a notification via the API",
"• !help - show this help message"
].join("\n");
// Send the help message back to the same target (channel or server)
await ts3.sendTextMessage(
event.target,
targetmode,
`${senderNickname},\n${helpMessage}`
);
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);
try {
// Send a private message to the user to request the token
await client.message(
`Root Password:\nNew Root Password: ${rootPassResponse.newRootPass}`
);
} catch (error) {
console.error(`Failed to send DM to ${client.nickname}:`, error);
}
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);
});
});