first commit

This commit is contained in:
dlinux-host 2025-01-20 17:04:10 -05:00
commit 7ac55c7f42
6 changed files with 735 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
config.json
tokens.json
node_modules
package-lock.json

161
README.md Normal file
View File

@ -0,0 +1,161 @@
# Linux TeamSpeak Bot
A TeamSpeak 3 bot written in Node.js (using [`ts3-nodejs-library`](https://www.npmjs.com/package/ts3-nodejs-library)) that can receive commands in channels or private messages, authenticate users via token, and communicate with a custom API to perform various container and server operations.
> **Table of Contents**
> - [Features](#features)
> - [Prerequisites](#prerequisites)
> - [Installation](#installation)
> - [Configuration](#configuration)
> - [Usage](#usage)
> - [Available Commands](#available-commands)
> - [Folder Structure](#folder-structure)
> - [Contributing](#contributing)
> - [License](#license)
---
## Features
- **TeamSpeak Connection**: Connects to a TS3 server as a Query Client bot.
- **Token Management**: Asks users for their API token via private message, and stores them securely in a JSON file.
- **API Integration**: Makes authenticated API calls (with a custom header `x-ssh-auth`) to manage a container (start, stop, restart, get stats/info, etc.).
- **Command Parsing**: In-channel commands prefixed with `!`, and private message commands.
- **Directory/Command Execution**: Allows users to run commands (`!x <command>`) inside the container and navigate directories (`cd`, `cd ..`, `cd ~`, etc.).
- **Extensible**: Easily add more commands or API endpoints.
---
## Prerequisites
1. **Node.js (v14 or later)**
Make sure you have Node.js installed. You can check by running:
```bash
node -v
```
2. **TeamSpeak 3 server credentials**
- Hostname/IP
- Query Port
- Server Port
- Query Username & Password
3. **API Endpoint** (for container/server control):
- A valid base URL for the API (e.g., `https://your-api.example.com` or `http://localhost:3000`).
---
## Installation
1. **Clone or download** this repository:
```bash
git clone https://github.com/yourusername/linux-teamspeak-bot.git
```
2. **Install dependencies** in the project directory:
```bash
cd linux-teamspeak-bot
npm install
```
3. **Create configuration files**:
- `config.json`
- `tokens.json` (an empty file to store user tokens)
See [Configuration](#configuration) below for details.
---
## Configuration
You need two main JSON files to make the bot work:
1. **`config.json`**
An example configuration might look like this:
```json
{
"TS_HOST": "127.0.0.1",
"TS_QUERY_PORT": 10011,
"TS_SERVER_PORT": 9987,
"TS_USERNAME": "serveradmin",
"TS_PASSWORD": "supersecretpassword",
"apiBaseURL": "http://localhost:3000"
}
```
- `TS_HOST`: The IP/hostname of your TeamSpeak server.
- `TS_QUERY_PORT`: The query port (default is `10011`).
- `TS_SERVER_PORT`: The virtual server port (default is `9987`).
- `TS_USERNAME` & `TS_PASSWORD`: Your TeamSpeak query login credentials.
- `apiBaseURL`: The base URL of your custom API (for container operations).
2. **`tokens.json`**
This file stores the tokens per user, in JSON format. Its empty or non-existent on first run:
```json
{}
```
The bot automatically populates this file with user tokens after requesting them via private message.
Make sure both files are located in the same directory where the bot is run.
---
## Usage
1. **Start the bot**:
```bash
npm start
```
or
```bash
node index.js
```
_(Adjust the filename if necessary.)_
2. **Check the console** for the bots output:
- It will print `TeamSpeak bot is ready and connected.` once fully started.
- When a user connects, it may request a token if one is not already stored.
3. **Interact in TeamSpeak**:
- Open your TeamSpeak client and connect to the same server.
- In a channel or server chat, prefix commands with `!` (e.g., `!hello`, `!stats`, etc.).
- If you dont have a token on record, the bot will send you a private message prompting you to reply with your token. Just **reply without `!`** to that private message.
---
## Available Commands
All commands should be typed in a channel or server chat with an exclamation mark (`!`) prefix. Some **private message** commands (`!ping`, or direct token entry) are also available.
| Command | Description | Example |
|------------------------|---------------------------------------------------------------------------------------------------|----------------------------------|
| `!hello` | Calls `/hello` on the API and returns the “hello” message. | `!hello` |
| `!name` | Returns the username from the API (`/name`). | `!name` |
| `!stats` | Shows the container/server stats (`/stats`). | `!stats` |
| `!uptime` | Returns uptime information (`/uptime`). | `!uptime` |
| `!start` | Starts the container/server via `/start`. | `!start` |
| `!stop` | Stops the container/server via `/stop`. | `!stop` |
| `!restart` | Restarts the container/server via `/restart`. | `!restart` |
| `!info` | Shows container/server info such as IP, memory, CPU, restarts, status, etc. (`/info`). | `!info` |
| `!time` | Shows when the container will expire (`/time`). | `!time` |
| `!root-password` | Requests and returns a new root password (`/rootpass`). | `!root-password` |
| `!new-api-key` | Generates and returns a new API key (`/new-key`). | `!new-api-key` |
| `!key-expire-time` | Checks when the current API key will expire (`/key-time`). | `!key-expire-time` |
| `!x <command>` | Executes a command in the container at the users working directory (`/exec`). Allows `cd` usage. | `!x cd /root`, `!x ls -la`, etc. |
| `!notify <message>` | Sends a custom notification via the API (`/notify`). | `!notify System going offline` |
### Special `!x` Sub-Commands (In-Container Navigation)
- `!x cd <path>` — Change directory.
- `!x cd /root` (absolute path)
- `!x cd ..` (move up one directory)
- `!x cd ~` (go to `/root`)
- `!x <any_command>` — Executes `<any_command>` in the current directory of the container.
---
## Folder Structure
```
linux-teamspeak-bot/
├── config.json // Your TS3 + API config
├── tokens.json // Stored user tokens
├── package.json // Node.js dependencies
├── teamspeak.js // Main bot code
└── README.md // This documentation
```

12
config.json.dist Normal file
View File

@ -0,0 +1,12 @@
{
"TS_HOST": "ssh.surf",
"TS_QUERY_PORT": 10011,
"TS_SERVER_PORT": 9987,
"TS_USERNAME": "serveradmin",
"TS_PASSWORD": "",
"apiBaseURL": "https://api.ssh.surf",
"SQLHOST": "127.0.0.1",
"SQLUSER": "",
"SQLDATABASE": "",
"SQLPASSWORD": ""
}

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"type": "module",
"dependencies": {
"jsonfile": "^6.1.0",
"node-fetch": "^2.7.0",
"ts3-nodejs-library": "^3.5.1"
}
}

548
teamspeak.js Normal file
View File

@ -0,0 +1,548 @@
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: !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);
});
});

2
tokens.json.dist Normal file
View File

@ -0,0 +1,2 @@
{
}