Reworked auth, added WS for imediate communication, cleaned up architecture.

This commit is contained in:
Mikolaj Wojciech Gorski 2025-07-26 05:43:38 +02:00
parent 19d7ee1dc5
commit f93781931f
12 changed files with 541 additions and 187 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
node_modules
output/
Dockerfile
Dockerfile.dev
docker-compose.yaml
docker-compose.dev.yml
.git
.gitignore
README.md

51
Dockerfile.dev Normal file
View file

@ -0,0 +1,51 @@
# --- Stage 1: Install node_modules ---
FROM node:20-slim as dependencies
WORKDIR /opt/bot
COPY package*.json ./
RUN npm install --no-audit --no-fund
# --- Stage 2: Development Build ---
FROM node:20-slim
# Install system dependencies for Discord and virtual display
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl ca-certificates sudo \
# ── Discord runtime deps ──────────────────
libatomic1 libnotify4 libnspr4 libnss3 libxss1 libgbm1 \
libgconf-2-4 libxtst6 libgtk-3-0 \
# ── GUI / audio / misc ───────────────────
xvfb pulseaudio openbox \
tigervnc-standalone-server tigervnc-common fonts-liberation \
x11vnc \
&& rm -rf /var/lib/apt/lists/*
# Download and install Discord
WORKDIR /opt/preinstall
RUN curl -L https://discord.com/api/download/stable?platform=linux -o discord.deb && \
dpkg -i discord.deb || apt-get -f install -y
# Create a non-root user
RUN useradd --create-home --shell /bin/bash bot && \
adduser bot sudo && \
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# Set up the bot directory
WORKDIR /opt/bot
# Copy pre-installed node_modules from the 'dependencies' stage
COPY --from=dependencies /opt/bot/node_modules ./node_modules
# Copy entrypoint and set permissions
COPY entry.sh .
RUN chmod +x /opt/bot/entry.sh
# The rest of the source code will be mounted as a volume
# Set ownership and user
RUN chown -R bot:bot /opt/bot
USER bot
# Set environment and entrypoint
ENV DISPLAY=:99
RUN mkdir -p /run/dbus
ENTRYPOINT dbus-daemon --system --fork --nofork --address unix:/run/dbus/system_bus_socket & exec /opt/bot/entry.sh "$@"

284
bot.js
View file

@ -4,15 +4,22 @@ import { Client as DiscordClient } from "discord.js-selfbot-v13";
import express from "express";
import session from "express-session";
import path from "path";
import { fileURLToPath } from "url";
import fetch from "node-fetch"; // Import fetch
import { fileURLToPath, pathToFileURL } from "url";
import { WebSocketServer } from "ws";
import authRouter from "./src/routes/authRoutes.js";
import fs from "fs";
// --- CONFIGURATION ---
const { USER_TOKEN, BOT_CLIENT_ID, BOT_CLIENT_SECRET, BOT_REDIRECT_URI } =
process.env;
const {
USER_TOKEN,
BOT_CLIENT_ID,
BOT_CLIENT_SECRET,
BOT_REDIRECT_URI,
COOKIE_SECRET,
} = process.env;
const WEB_PORT = 3000;
// IMPORTANT: Replace with the Discord User IDs of people allowed to access the dashboard.
const ADMIN_USER_IDS = ["339753362297847810"];
const MAX_LOG_SIZE = 20;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -20,217 +27,122 @@ const __dirname = path.dirname(__filename);
// --- IN-MEMORY STATE ---
const activityLog = [];
const actionsLog = [];
const MAX_LOG_SIZE = 20;
const logItem = (logArray, item) => {
logArray.unshift(item);
if (logArray.length > MAX_LOG_SIZE) logArray.pop();
};
// --- WEB SERVER SETUP ---
const app = express();
// Make config and data available to routers/controllers via app.locals
app.locals.config = {
BOT_CLIENT_ID,
BOT_CLIENT_SECRET,
BOT_REDIRECT_URI,
ADMIN_USER_IDS,
};
app.locals.data = {
activityLog,
actionsLog,
};
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.set("trust proxy", 1);
app.set("trust proxy", 1); // Required for secure cookies if behind a proxy
app.use(
session({
secret: "a-very-secret-string-for-teto",
secret: COOKIE_SECRET || "a-very-secret-string-for-teto-is-required",
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
},
}),
);
// --- OAUTH2 AUTHENTICATION & DASHBOARD ROUTES ---
app.get("/auth/discord", (req, res) => {
const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${BOT_CLIENT_ID}&redirect_uri=${encodeURIComponent(BOT_REDIRECT_URI)}&response_type=code&scope=identify`;
res.redirect(authUrl);
});
app.get("/auth/callback", async (req, res) => {
const code = req.query.code;
if (!code) return res.status(400).send("No code provided.");
try {
const params = {
client_id: BOT_CLIENT_ID,
client_secret: BOT_CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: BOT_REDIRECT_URI,
};
console.log("Attempting token exchange with params:", params);
const tokenResponse = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
body: new URLSearchParams(params),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
const tokenData = await tokenResponse.json();
if (tokenData.error) {
console.error("Token Exchange Error:", tokenData); // Log the full error response
return res
.status(500)
.send(
`Error exchanging code for token. Details: ${JSON.stringify(tokenData)}`,
);
}
const userResponse = await fetch("https://discord.com/api/users/@me", {
headers: { Authorization: `Bearer ${tokenData.access_token}` },
});
const userData = await userResponse.json();
if (!ADMIN_USER_IDS.includes(userData.id)) {
return res
.status(403)
.send("You are not authorized to view this dashboard.");
}
req.session.user = {
id: userData.id,
username: userData.username,
discriminator: userData.discriminator,
avatar: userData.avatar,
};
req.session.save((err) => {
if (err) {
console.error("Error saving session:", err);
return res
.status(500)
.send("An error occurred while saving the session.");
}
console.log("Session saved successfully, redirecting.");
res.redirect("/");
});
} catch (error) {
console.error("OAuth2 Callback Error:", error);
res.status(500).send("An error occurred during authentication.");
}
});
app.get("/logout", (req, res, next) => {
req.session.destroy((err) => {
if (err) return next(err);
res.redirect("/");
});
});
const checkAuth = (req, res, next) => {
if (req.session.user) return next();
res.redirect("/auth/discord");
};
// --- STATIC ASSETS & ROUTES ---
app.use(express.static(path.join(__dirname, "public")));
app.use("/", authRouter);
app.get("/", checkAuth, (req, res) => {
const systemResources = {
memory: { percentage: 91, used: 7.3, total: 8 },
vram: { percentage: 69, used: 5.5, total: 8 },
avgResponse: 32,
shutdown: "3d 0h",
sessionEnd: "Jul 24, 2025, 07:21 PM",
};
res.render("index", {
user: req.session.user,
activityLog,
actionsLog,
systemResources,
});
});
// --- DISCORD BOT LOGIC (no changes here) ---
// --- DISCORD BOT & SERVER INITIALIZATION ---
(async () => {
// Puppeteer setup (currently optional)
puppeteer.use(StealthPlugin());
// This part is disabled for now but can be re-enabled if needed for screenshots
/*
const browser = await puppeteer.launch({
executablePath: '/usr/bin/discord',
headless: false,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--start-maximized'],
defaultViewport: null,
});
const [page] = await browser.pages();
console.log("Puppeteer has attached to Discord's browser.");
const browser = await puppeteer.launch({ headless: false });
// ... more puppeteer logic if needed
*/
// Start the web server AND capture the HTTP server instance it creates
const server = app.listen(WEB_PORT, () => {
console.log(`Dashboard server running on http://localhost:${WEB_PORT}`);
});
// Create the WebSocket server and attach it to our existing HTTP server
const wss = new WebSocketServer({ server });
// Function to broadcast data to all connected WebSocket clients
const broadcast = (data) => {
// The 'wss.clients' property is a Set of all connected clients
wss.clients.forEach((client) => {
// Check if the connection is still open before sending
if (client.readyState === client.OPEN) {
// WebSocket only sends strings, so we stringify our object
client.send(JSON.stringify(data));
}
});
};
const logItem = (logArray, item) => {
logArray.unshift(item);
if (logArray.length > MAX_LOG_SIZE) logArray.pop();
if (logArray === activityLog) {
broadcast({ type: "activityLogUpdate", payload: item });
}
};
wss.on("connection", (ws) => {
console.log("Dashboard client connected via WebSocket.");
ws.on("close", () => {
console.log("Dashboard client disconnected.");
});
});
const client = new DiscordClient({ checkUpdate: false });
client.once("ready", () => {
console.log(`Selfbot ready as ${client.user.tag} (${client.user.id})`);
// --- DYNAMIC EVENT HANDLER LOADING ---
const eventsPath = path.join(__dirname, "src", "events");
const eventFiles = fs
.readdirSync(eventsPath)
.filter((file) => file.endsWith(".js"));
// Add dummy data
logItem(actionsLog, {
message: "Responded to Alice in",
channel: "#gaming",
icon: "💬",
});
logItem(actionsLog, {
message: "Joined Voice Chat",
channel: "#general",
icon: "🎤",
});
logItem(actionsLog, {
message: "Analyzed image from Charlie in",
channel: "#memes",
icon: "👁️",
});
const context = {
client,
logItem,
activityLog,
actionsLog,
MAX_LOG_SIZE,
};
const now = new Date();
logItem(activityLog, {
message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Memory consolidation complete`,
});
now.setSeconds(now.getSeconds() - 4);
logItem(activityLog, {
message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Learning: Alice enjoys my French bread jokes`,
});
now.setSeconds(now.getSeconds() - 4);
logItem(activityLog, {
message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Updating conversation context in vector database...`,
});
now.setSeconds(now.getSeconds() - 4);
logItem(activityLog, {
message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Alice reacted with 😊 - response was well received`,
});
now.setSeconds(now.getSeconds() - 4);
logItem(activityLog, {
message: `[${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Monitoring user reactions and engagement...`,
});
});
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = (await import(pathToFileURL(filePath))).default;
client.on("messageCreate", (msg) => {
if (msg.author.id === client.user.id) return;
const message = `[${new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Message from ${msg.author.tag} in #${msg.channel.name || "DM"}: "${msg.content.slice(0, 50)}..."`;
logItem(activityLog, { message });
// Example of logging a bot action
if (msg.content.toLowerCase().includes("hello teto")) {
msg.channel.send("Hello there!");
logItem(actionsLog, {
message: `Responded to ${msg.author.tag}`,
channel: `#${msg.channel.name || "DM"}`,
icon: "💬",
});
if (event.name && event.execute) {
if (event.once) {
client.once(event.name, (...args) => event.execute(context, ...args));
} else {
client.on(event.name, (...args) => event.execute(context, ...args));
}
}
});
}
// End dynamic event handler loading
client.on("disconnect", () => {
const message = `[${new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}] Bot disconnected.`;
logItem(activityLog, { message });
});
client.login(USER_TOKEN);
app.listen(WEB_PORT, () => {
console.log(`Dashboard server running on https://teto.getsilly.org`);
});
try {
await client.login(USER_TOKEN);
} catch (error) {
console.error("Failed to login to Discord. Check your USER_TOKEN.", error);
process.exit(1);
}
})();

32
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,32 @@
version: "3.8"
services:
teto_ai:
build:
context: .
dockerfile: Dockerfile.dev
container_name: teto_ai_dev
cap_add:
- SYS_ADMIN
environment:
# supply the user token at runtime:
USER_TOKEN: "${USER_TOKEN}"
BOT_CLIENT_ID: "${BOT_CLIENT_ID}"
BOT_CLIENT_SECRET: "${BOT_CLIENT_SECRET}"
BOT_REDIRECT_URI: "https://teto.getsilly.org/auth/callback"
volumes:
# live-peek folder so you can grab screenshots outside the container
- ./output:/tmp/output
# Mount the current directory into the container for live-reloading
- .:/opt/bot
# Use a named volume to keep the container's node_modules from being overwritten
- node_modules:/opt/bot/node_modules
ports:
- "5901:5901" # VNC
- "3000:3000" # optional HTTP server if you add one
tmpfs:
- /tmp:size=512M
restart: unless-stopped
volumes:
node_modules:

0
entry.sh Normal file → Executable file
View file

94
public/socket_logic.js Normal file
View file

@ -0,0 +1,94 @@
let socket;
let heartbeatInterval;
function connect() {
// Use wss:// for secure connections (in production) or ws:// for local
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
socket = new WebSocket(`${protocol}://${window.location.host}`);
socket.onopen = () => {
console.log("WebSocket connection established.");
// Start a heartbeat to keep the connection alive
clearInterval(heartbeatInterval); // Clear previous interval if it exists
heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
// Sending a simple ping message. The server's 'ws' library will handle this
// automatically to keep the connection from being marked as idle by proxies.
socket.send(JSON.stringify({ type: "ping" }));
}
}, 30000); // every 30 seconds
};
socket.onmessage = (event) => {
// The server's 'ws' library might automatically respond to pings with pongs.
// We can safely ignore them, along with our own pings if they get echoed.
if (event.data.includes("ping") || event.data.includes("pong")) {
return;
}
const data = JSON.parse(event.data);
// --- This is our client-side message router ---
switch (data.type) {
case "activityLogUpdate":
handleActivityLogUpdate(data.payload);
break;
case "systemResourcesUpdate":
handleSystemResourceUpdate(data.payload);
break;
// Add more cases here for other real-time updates
default:
console.log("Received unknown message type:", data.type);
}
};
socket.onclose = () => {
console.log(
"WebSocket connection closed. Attempting to reconnect in 3 seconds...",
);
clearInterval(heartbeatInterval); // Stop the heartbeat when the connection is lost
// **This is the key part:** Wait 3 seconds, then try to connect again.
setTimeout(connect, 3000);
};
socket.onerror = (error) => {
console.error("WebSocket Error:", error);
// The 'onclose' event will fire automatically after an error,
// which will then trigger our reconnection logic.
socket.close(); // Ensure the socket is closed to trigger onclose
};
}
function handleActivityLogUpdate(payload) {
const activityLog = document.getElementById("activity-log");
if (!activityLog || !payload.message) return;
const newLogItem = document.createElement("p");
const styledMessage = payload.message.replace(
/(\[[^\]]+\])/,
'<span class="timestamp">$1</span>',
);
newLogItem.innerHTML = styledMessage;
activityLog.prepend(newLogItem);
while (activityLog.children.length > 20) {
activityLog.lastChild.remove();
}
}
function handleSystemResourceUpdate(payload) {
// Conceptual: Find the memory percentage element and update its text and style
const memPercentage = document.querySelector(".metric-header strong"); // Simplified selector
const memProgressBar = document.querySelector(".progress-bar.memory"); // Simplified selector
if (memPercentage)
memPercentage.textContent = `${payload.memory.percentage}%`;
if (memProgressBar)
memProgressBar.style.width = `${payload.memory.percentage}%`;
console.log("System resources updated!", payload);
}
// --- Initial Connection ---
connect();

View file

@ -0,0 +1,127 @@
import fetch from "node-fetch";
const authController = {
// --- Route Handlers ---
redirectToDiscord: (req, res) => {
const { BOT_CLIENT_ID, BOT_REDIRECT_URI } = req.app.locals.config;
const authUrl = `https://discord.com/api/oauth2/authorize?client_id=${BOT_CLIENT_ID}&redirect_uri=${encodeURIComponent(
BOT_REDIRECT_URI,
)}&response_type=code&scope=identify`;
res.redirect(authUrl);
},
handleCallback: async (req, res) => {
const code = req.query.code;
if (!code) {
return res.status(400).send("No code provided.");
}
const {
BOT_CLIENT_ID,
BOT_CLIENT_SECRET,
BOT_REDIRECT_URI,
ADMIN_USER_IDS,
} = req.app.locals.config;
try {
const params = new URLSearchParams({
client_id: BOT_CLIENT_ID,
client_secret: BOT_CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: BOT_REDIRECT_URI,
});
const tokenResponse = await fetch(
"https://discord.com/api/oauth2/token",
{
method: "POST",
body: params,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
},
);
const tokenData = await tokenResponse.json();
if (tokenData.error) {
console.error("Token Exchange Error:", tokenData);
return res
.status(500)
.send(
`Error exchanging code for token. Details: ${JSON.stringify(
tokenData,
)}`,
);
}
const userResponse = await fetch("https://discord.com/api/users/@me", {
headers: { Authorization: `Bearer ${tokenData.access_token}` },
});
const userData = await userResponse.json();
if (!ADMIN_USER_IDS.includes(userData.id)) {
return res
.status(403)
.send("You are not authorized to view this dashboard.");
}
req.session.user = {
id: userData.id,
username: userData.username,
discriminator: userData.discriminator,
avatar: userData.avatar,
};
req.session.save((err) => {
if (err) {
console.error("Error saving session:", err);
return res
.status(500)
.send("An error occurred while saving the session.");
}
res.redirect("/");
});
} catch (error) {
console.error("OAuth2 Callback Error:", error);
res.status(500).send("An error occurred during authentication.");
}
},
logout: (req, res, next) => {
req.session.destroy((err) => {
if (err) {
console.error("Error destroying session:", err);
return next(err);
}
res.redirect("/");
});
},
renderDashboard: (req, res) => {
const { activityLog, actionsLog } = req.app.locals.data;
const systemResources = {
memory: { percentage: 91, used: 7.3, total: 8 },
vram: { percentage: 69, used: 5.5, total: 8 },
avgResponse: 32,
shutdown: "3d 0h",
sessionEnd: "Jul 24, 2025, 07:21 PM",
};
res.render("index", {
user: req.session.user,
activityLog,
actionsLog,
systemResources,
});
},
// --- Middleware ---
checkAuth: (req, res, next) => {
if (req.session.user) {
return next();
}
res.redirect("/auth/discord");
},
};
export default authController;

8
src/events/disconnect.js Normal file
View file

@ -0,0 +1,8 @@
// src/events/disconnect.js
export default {
name: "disconnect",
execute({ logItem, activityLog }) {
const message = `[${new Date().toLocaleTimeString()}] Bot disconnected.`;
logItem(activityLog, { message });
},
};

View file

@ -0,0 +1,22 @@
// src/events/messageCreate.js
export default {
name: "messageCreate",
execute({ client, logItem, activityLog, actionsLog, MAX_LOG_SIZE }, msg) {
if (msg.author.id === client.user.id) return;
const message = `[${new Date().toLocaleTimeString()}] Message from ${
msg.author.tag
} in #${msg.channel.name || "DM"}: "${msg.content.slice(0, 50)}..."`;
logItem(activityLog, { message }); // This will now also broadcast
if (msg.content.toLowerCase().includes("hello teto")) {
msg.channel.send("Hello there!");
const action = {
message: `Responded to ${msg.author.tag}`,
channel: `#${msg.channel.name || "DM"}`,
icon: "💬",
};
actionsLog.unshift(action);
if (actionsLog.length > MAX_LOG_SIZE) actionsLog.pop();
}
},
};

23
src/events/ready.js Normal file
View file

@ -0,0 +1,23 @@
export default {
name: "ready",
once: true,
execute({ client, logItem, activityLog, actionsLog }) {
console.log(`Selfbot ready as ${client.user.tag} (${client.user.id})`);
// Add dummy data for demonstration purposes
actionsLog.unshift({
message: "Responded to Alice in",
channel: "#gaming",
icon: "💬",
});
actionsLog.unshift({
message: "Joined Voice Chat",
channel: "#general",
icon: "🎤",
});
const now = new Date();
logItem(activityLog, {
message: `[${now.toLocaleTimeString()}] System initialized and connected.`,
});
},
};

16
src/routes/authRoutes.js Normal file
View file

@ -0,0 +1,16 @@
import express from "express";
import authController from "../controllers/authController.js";
const router = express.Router();
// --- Authentication Routes ---
// These routes handle the OAuth2 flow with Discord.
router.get("/auth/discord", authController.redirectToDiscord);
router.get("/auth/callback", authController.handleCallback);
router.get("/logout", authController.logout);
// --- Dashboard Route ---
// The main dashboard route is protected by the `checkAuth` middleware.
router.get("/", authController.checkAuth, authController.renderDashboard);
export default router;

View file

@ -45,7 +45,7 @@
<div class="widget">
<h3>Recent Actions <span class="timeframe">12 in last hour</span></h3>
<div class="actions-box">
<% let-i = 0; %>
<% let i = 0; %>
<% actionsLog.forEach(action => { %>
<% const time = i === 0 ? '5 min ago' : i === 1 ? '12 min ago' : '30 min ago'; i++; %>
<div class="action-item">
@ -110,5 +110,65 @@
</div>
</main>
</div>
<script>
// 1. Establish the connection
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const socket = new WebSocket(`${protocol}://${window.location.host}`);
function connectWS() {
socket.onopen = () => {
console.log('WebSocket connection established');
};
}
connectWS();
// 2. Listen for messages from the server
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
// --- This is our client-side message router ---
switch (data.type) {
case 'activityLogUpdate':
handleActivityLogUpdate(data.payload);
break;
case 'systemResourcesUpdate':
handleSystemResourceUpdate(data.payload);
break;
// Add more cases here for other real-time updates
default:
console.log('Received unknown message type:', data.type);
}
};
function handleActivityLogUpdate(payload) {
const activityLog = document.getElementById('activity-log');
if (!activityLog || !payload.message) return;
const newLogItem = document.createElement('p');
const styledMessage = payload.message.replace(/(\[[^\]]+\])/, '<span class="timestamp">$1</span>');
newLogItem.innerHTML = styledMessage;
activityLog.prepend(newLogItem);
while (activityLog.children.length > 20) {
activityLog.lastChild.remove();
}
}
function handleSystemResourceUpdate(payload) {
// Conceptual: Find the memory percentage element and update its text and style
const memPercentage = document.querySelector('.metric-header strong'); // Simplified selector
const memProgressBar = document.querySelector('.progress-bar.memory'); // Simplified selector
if(memPercentage) memPercentage.textContent = `${payload.memory.percentage}%`;
if(memProgressBar) memProgressBar.style.width = `${payload.memory.percentage}%`;
console.log('System resources updated!', payload);
}
socket.onclose = () => {
console.log('WebSocket connection closed. Attempting to reconnect...');
connectWS();
};
</script>
</body>
</html>