first commit
This commit is contained in:
commit
7ac55c7f42
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
config.json
|
||||
tokens.json
|
||||
node_modules
|
||||
package-lock.json
|
161
README.md
Normal file
161
README.md
Normal 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. It’s 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 bot’s 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 don’t 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 user’s 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
12
config.json.dist
Normal 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
8
package.json
Normal 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
548
teamspeak.js
Normal 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, 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);
|
||||
});
|
||||
});
|
2
tokens.json.dist
Normal file
2
tokens.json.dist
Normal file
@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user