Compare commits

...

5 Commits

Author SHA1 Message Date
8ebd9b78ec add bcrypt module 2025-08-17 17:56:41 +03:00
a98e09129d add new logger functions 2025-08-17 17:56:13 +03:00
dd9bc64f1e add new methods code in frontend 2025-08-17 17:55:55 +03:00
9544bc70d6 add database code 2025-08-17 17:55:23 +03:00
8471e5177c add .db files on gitigore 2025-08-17 17:53:34 +03:00
17 changed files with 2278 additions and 319 deletions

5
server/.gitignore vendored
View File

@ -117,6 +117,9 @@ dist
# DynamoDB Local files
.dynamodb/
# DB local files
.db
# TernJS port file
.tern-port
@ -128,4 +131,4 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*

View File

@ -31,14 +31,11 @@ class APIMethods {
info (con, req, cb) {
if (!req.fields) req.fields = "*";
if (!req.fieldsExtSource) req.fieldsExtSource = "*";
const cache = this.info.cache[
JSON.stringify({
f: req.fields, e: req.fieldsExtSource
})
];
const cacheKey = `${req.fields}\u0000${req.fieldsExtSource}`;
const cache = this.info.cache[cacheKey];
if (cache) {
console.debug("CACHE!");
return cb({ result: cache, trace_id: req.trace_id, ended: true });
}
@ -55,11 +52,7 @@ class APIMethods {
if (!fields[k]) serverInfo.extSource[k] = undefined;
});
}
this.info.cache[
JSON.stringify({
f: req.fields, e: req.fieldsExtSource
})
] = serverInfo;
this.info.cache[cacheKey] = serverInfo;
cb({ result: serverInfo, trace_id: req.trace_id, ended: true });
}
@ -104,6 +97,35 @@ class APIMethods {
});
}
async characters (con, req, cb) {
const authedMethod = this.authed;
const isAuthed = await (new Promise((rs, rj) => {
authedMethod(con, req, ({ result, ended }) => {
if (ended)
return rs(result);
});
}));
if (!isAuthed) {
return cb({
error: "require authorize",
trace_id: req.trace_id,
ended: true
});
}
// getting characters
const user = global.authed.get(con);
const result = (await user.getCharacters())
.map(character => {
return character;
});
cb({
result, trace_id: req.trace_id, ended: true
});
}
/*setSession (isEncrypted, address, { key, counter }, cb) {
if (!global.config.server.secureMode)
return cb({ error: "Encryption is off by configuration file into server" });

View File

@ -13,7 +13,7 @@ module.exports.registerToken = function (user) {
token = module.exports();
} while (global.authed.tokens[token] !== undefined);
global.authed.tokens[token] = user;
// (!) Потом добавить регистрацию в БД!
// TODO: Потом добавить регистрацию в БД!
return token;
};

View File

@ -9,26 +9,46 @@
* * 5 - root
*/
class User {
constructor(name, accessLevel) {
if (
typeof accessLevel !== "number" ||
{
"-1": true,
0: true,
1: true,
2: true,
3: true,
4: true,
5: true,
}["" + accessLevel] !== true
) {
const ACCESS_LEVELS = {
"-1": "banned",
0: "user",
1: "helper",
2: "moderator",
3: "admin",
4: "superadmin",
5: "root",
};
class AccessLevel {
static validate (value) {
const access = Object.fromEntries(Object.keys(ACCESS_LEVELS)
.map(level => [level, true]));
return (
Number.isInteger(value) &&
access["" + value] === true
);
}
constructor (accessLevel) {
if (!this.__proto__.constructor.validate(accessLevel))
throw new Error(
`Invalid access level: ${accessLevel} (must be ranged from -1 to 5)`,
`Invalid access level: ${accessLevel}`,
);
}
this.code = accessLevel;
}
get accessName () {
return ACCESS_LEVELS[""+this.code]
}
}
class User {
constructor (name, accessLevel) {
this.name = name;
this.accessLevel = accessLevel;
if (accessLevel instanceof AccessLevel)
this.accessLevel = accessLevel;
else
this.accessLevel = new AccessLevel(accessLevel);
}
static async getUserByName(name) {
@ -41,8 +61,15 @@ class User {
User.cache[name] = user;
return user;
}
// Потом добавить чтение из БД
return null;
// TODO: Потом добавить чтение из БД
//return null;
const userdata = await global.database.getUserRowByName(name);
if (!userdata)
return null;
const user = new User(userdata.username, userdata.accessLevel);
User.cache[name] = user;
return user;
}
static getByToken(token) {
@ -50,13 +77,27 @@ class User {
return user ?? null;
}
getUserForAPI(getFullInfo = false) {
return {
getUserForAPI(getFullInfo = false, fields = "*") {
const getted = {
username: this.name,
accessLevel: this.accessLevel,
accessLevel: this.accessLevel.code,
};
if (fields !== "*") {
const selected = Object.fromEntries(fields.split(/\s*,\s*/g).map(field => [field, true]));
for (let field in getted) {
if (!selected[field])
getted[field] = undefined;
}
}
return getted;
}
async getCharacters () {
return [];
}
}
User.cache = {};
module.exports = { User };
module.exports = { User, AccessLevel };

View File

@ -18,7 +18,8 @@
},
"database": {
"dialect": "sqlite",
"path": "./data"
"storage": "./data.db",
"logging": false
},
"ai": {
"apiType": "kobold"

124
server/database/dbModel.js Normal file
View File

@ -0,0 +1,124 @@
const { DataTypes } = require("sequelize");
const { AccessLevel } = require("../api/utils/user");
const logger = require("../logger");
const bcrypt = require("bcrypt");
/*const SALT_ROUNDS = Math.round(Math.r) + 6;
const BCRYPT_SALT = bcrypt.genSaltSync(SALT_ROUNDS);*/
function getSalt () {
//return bcrypt.genSaltSync(10);
return bcrypt.genSaltSync(Math.round(Math.random() * 4) + 6);
}
module.exports = async function (sequelize, dbObj) {
const dbModel = {
users: sequelize.define("users", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true,
comment: "ID of user",
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: "Username of user",
validate: {
not: /null/iu,
},
},
accessLevel: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: false,
comment: "access level of user",
get: function () {
return new AccessLevel(this.getDataValue("accessLevel"));
},
set: function (value) {
if (value instanceof AccessLevel)
return this.setDataValue("accessLevel", value.code);
if (Number.isInteger(value)) {
if (AccessLevel.validate(value))
return this.setDataValue("accessLevel", value);
}
throw new TypeError("accessLevel must be AccessLevel object or integer");
//this.setDataValue('inventory', JSON.stringify(value));
},
},
password: {
type: DataTypes.STRING,
allowNull: false,
unique: false,
comment: "Hash code of password",
set: function (value) {
const hash = bcrypt.hashSync(value, getSalt());
this.setDataValue('password', hash);
},
get: function () {
const checkHash = this.getDataValue("password");
return async function (password) {
return await new Promise((rs, rj) => {
bcrypt.compare(password, checkHash, (err, res) => {
if (err)
return rs(false);
logger.debug("isRes is", res);
rs(res);
});
});
}
},
},
isActivate: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: "Is user activated",
},
}),
userCharacters: sequelize.define("userCharacters", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true,
comment: "ID of character",
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: "Name of character",
},
ownerId: {
type: DataTypes.INTEGER,
allowNull: false,
comment: "ID of character's owner",
},
}),
};
dbModel.userCharacters.belongsTo(dbModel.users, {
foreignKey: "ownerId", // field in userCharacters
targetKey: "id", // field in users
onDelete: "CASCADE", // If delete user, characters will be deleted
onUpdate: "CASCADE",
});
dbModel.users.hasMany(dbModel.userCharacters, {
foreignKey: "ownerId", // field ownerId
sourceKey: "id",
});
await sequelize.authenticate();
const initPromise = Object.entries(dbModel).map(async ([tableName, table]) => {
logger.log(`* init table: ${tableName}`);
await table.sync({ alter: true });
const rows = await table.findAll();
await dbObj.cache[tableName]?.(rows, table);
logger.log(`* inited: ${tableName}`);
});
await Promise.all(initPromise);
};

89
server/database/index.js Normal file
View File

@ -0,0 +1,89 @@
const { Sequelize } = require("sequelize");
const dbModel = require("./dbModel");
const logger = require("../logger");
class DatabaseCache {
async users (rows, table) {
// Add root user only where root user is activated
if (global.config.rootuser.activated) {
const isRootHasOnDB = rows.find(x => x.username === global.config.rootuser.login);
const rootLevel = 5; // TODO: replace it to AccessLevel.root in future
if (!isRootHasOnDB) {
await table.create({
username: global.config.rootuser.login,
accessLevel: rootLevel,
password: "",
});
return await this.users((
await table.findAll()
), table);
} else if (!rows.find(x => x.username === global.config.rootuser.login && x.accessLevel.code === rootLevel)) {
throw new Error(`Username ${
global.config.rootuser.login
} is busy`);
}
}
this.users.rows = rows;
this.users.byUsername = new Object();
this.users.byId = new Object();
for (let row of rows) {
this.users.byUsername[row.username] = row;
this.users.byId[row.id] = row;
}
// Add user info
this.users.add = async (row, isCreate = false) => {
if (isCreate)
await table.create(row);
let addedData = null;
if (row.username)
addedData = await table.findAll({
where: {
username: row.username
}
})[0];
else if (row.id)
addedData = await table.findAll({
where: {
id: row.id
}
})[0];
if (!addedData) throw new Error("Invalid row information");
this.users.rows.push(addedData);
// By keys
this.users.byUsername[addedData.username] = addedData;
this.users.byId[addedData.id] = addedData;
}
}
}
class Database {
constructor(sequlizeData) {
this.sequlize = new Sequelize(sequlizeData);
}
async init(fromHandler = false) {
global.server.toggleLock(true);
this.cache = new DatabaseCache();
await dbModel(this.sequlize, this);
global.server.toggleLock(false);
if (!fromHandler)
global.server.on("config.update", async ({
oldConfig, newConfig
}) => {
if (JSON.stringify(oldConfig.database) !== JSON.stringify(newConfig.database))
await this.init(true);
});
}
// Return ROW of 'users' table by username
async getUserRowByName (name) {
return this.cache.users.byUsername[name];
}
}
module.exports = Database;

View File

@ -5,31 +5,31 @@ module.exports = function (req, res) {
if (req.query.encrypted === undefined || req.query.server === undefined)
return res.status(400).send(`<!DOCTYPE html>
<html>
<head>
<title>Error</title>
<meta charset="utf-8">
</head>
<body class="bg text-white">
<h1>[400] Error</h1><hr/><p>unuse required params</p>
</body>
<head>
<title>Error</title>
<meta charset="utf-8">
</head>
<body class="bg text-white">
<h1>[400] Error</h1><hr/><p>unuse required params</p>
</body>
</html>`);
res.send(`<!DOCTYPE html>
<html>
<head>
<title>Connection: ${serverAddress}</title>
<meta charset="utf-8">
</head>
<body class="bg text-white">
<div id="js-required">
<h1>Error: Javascript off</h1><hr/>
<p>Javascript is required for the site to work, enable it in your browser settings</p>
</div>
<script>
document.getElementById("js-required").innerHTML = "<p>Loading...</p>";
</script>
<head>
<title>Connection: ${serverAddress}</title>
<meta charset="utf-8">
</head>
<body class="bg text-white">
<div id="js-required">
<h1>Error: Javascript off</h1><hr/>
<p>Javascript is required for the site to work, enable it in your browser settings</p>
</div>
<script>
document.getElementById("js-required").innerHTML = "<p>Loading...</p>";
</script>
<script src="/bootstrap/bootstrap.bundle.min.js" defer></script>
<script src="/js/jquery-3.7.1.min.js" defer></script>
<script src="/js/connect/main.js" type="module" defer></script>
</body>
</body>
</html>`);
}
};

View File

@ -4,17 +4,20 @@ const path = require("path");
const router = express.Router();
router.get("/", (req, res) => {
res.send(
fs.readFileSync(path.join(__dirname, "pages/index.html")).toString("utf-8")
);
res.send(
fs.readFileSync(path.join(__dirname, "pages/index.html")).toString("utf-8"),
);
});
router.get("/connect", (req, res) => {
res.send(
fs.readFileSync(path.join(__dirname, "pages/connect.html")).toString("utf-8")
);
res.send(
fs.readFileSync(path.join(__dirname, "pages/connect.html"))
.toString("utf-8"),
);
});
router.get("/*", (req, res) => res.status(404).send("<p><b>404:</b> Not finded.</p>"));
router.get("/*", (req, res) =>
res.status(404).send("<p><b>404:</b> Not finded.</p>"),
);
module.exports = router;

View File

@ -7,48 +7,51 @@ const connectArea = require("./connect-router");
const api = require("./web-api/api-router");
module.exports = (address, port) => {
const app = express();
logger.log("Static in", path.join(__dirname, "public"));
const app = express();
logger.log("Static in", path.join(__dirname, "public"));
app.use((req, res, next) => {
// logger.log("HTTP headers:", req.headers);
if (global.server.isLocked) {
res.set("Content-Type", "plain/text");
return res.send("Server temporary on reload");
}
next();
});
app.use(express.static(path.join(__dirname, "public")));
app.use("/fl_dir", flDir);
app.use("/api", api);
app.use(express.static(path.join(__dirname, "public")));
app.use("/fl_dir", flDir);
app.use("/api", api);
app.get("/connect-area", connectArea);
app.use("/*", (req, res) => {
res.send(`<!DOCTYPE html>
app.use("/*", (req, res) => {
res.send(`<!DOCTYPE html>
<html>
<head>
<title>AI Adventure Labs</title>
<meta charset="utf-8">
</head>
<body class="bg text-white">
<div id="js-required">
<h1>Error: Javascript off</h1><hr/>
<p>Javascript is required for the site to work, enable it in your browser settings</p>
</div>
<script>
document.getElementById("js-required").innerHTML = "<p>Loading...</p>";
</script>
<script src="/bootstrap/bootstrap.bundle.min.js" defer></script>
<head>
<title>AI Adventure Labs</title>
<meta charset="utf-8">
</head>
<body class="bg text-white">
<div id="js-required">
<h1>Error: Javascript off</h1><hr/>
<p>Javascript is required for the site to work, enable it in your browser settings</p>
</div>
<script>
document.getElementById("js-required").innerHTML = "<p>Loading...</p>";
</script>
<script src="/bootstrap/bootstrap.bundle.min.js" defer></script>
<script src="/fl_framework/index.js" defer></script>
<script src="/js/translate.js" defer></script>
<script src="/js/jsencrypt.js" defer></script>
<script src="/js/main.js" defer></script>
</body>
</body>
</html>`);
});
});
app.listen(port, address, (err) => {
if (err)
throw err;
logger.log(`HTTP page successful runned at port ${port}`);
});
app.listen(port, address, (err) => {
if (err) throw err;
logger.log(`HTTP page successful runned at port ${port}`);
});
};

View File

@ -83,6 +83,12 @@ class ApiMethods extends ProtoApiMethods {
gtf
});
}
async characters () {
return await super._protoMethod({
method: "characters"
});
}
}
class ApiHTML {
@ -102,6 +108,9 @@ class ApiHTML {
document.getElementById("server.area").innerHTML = '';
$(document.getElementById("server.area")).append(ServerAuth.mainMenuForm);
// JS-Events
const charactersEvents = await this.api.methods.characters();
console.debug({ charactersEvents });
}
async renderAuth (authMode, bgUrl = null, favicon = null) {

View File

@ -1,26 +1,29 @@
// cookie parser function
function getCookies () {
return Object.fromEntries(document.cookie.split(/\s{0,};\s{0,}/gmiu).map(i => i.split(/\s{0,}=\s{0,}/gmiu)));
function getCookies() {
return Object.fromEntries(
document.cookie
.split(/\s{0,};\s{0,}/gimu)
.map((i) => i.split(/\s{0,}=\s{0,}/gimu)),
);
}
// translate function
function translate (page) {
[...document.getElementsByClassName("translate")].forEach(translateEl => {
function translate(page) {
[...document.getElementsByClassName("translate")].forEach((translateEl) => {
// console.log(translateEl.id);
const result = tr(page, translateEl.id);
// console.log(result);
if (result)
translateEl.innerHTML = result;
const result = tr(page, translateEl.id);
// console.log(result);
if (result) translateEl.innerHTML = result;
});
[...document.getElementsByClassName("linked-btn")].forEach(el => {
[...document.getElementsByClassName("linked-btn")].forEach((el) => {
el.style.cursor = "pointer";
});
}
// Get random integer number function
function randint(min, max) {
return Math.ceil((Math.random() * (max - min)) + min);
return Math.ceil(Math.random() * (max - min) + min);
}
// Get traceId for requests to server
@ -32,10 +35,12 @@ function getTraceId() {
}
// load styles
document.head.innerHTML += '\\n <link href="/bootstrap/bootstrap.min.css" rel="stylesheet">';
document.head.innerHTML +=
'\\n <link href="/bootstrap/bootstrap.min.css" rel="stylesheet">';
document.head.innerHTML += '\\n <link href="/css/main.css" rel="stylesheet">';
document.head.innerHTML += '\\n <link href="/font-awesome/css/all.css" rel="stylesheet">';
document.head.innerHTML += '\\n <link rel="icon" href="/favicon.png">'
document.head.innerHTML +=
'\\n <link href="/font-awesome/css/all.css" rel="stylesheet">';
document.head.innerHTML += '\\n <link rel="icon" href="/favicon.png">';
// load main page
document.body.innerHTML = `<nav id="navbar-main" class="navbar navbar-expand-lg navbar-dark bg-dark"><div class="container-fluid">
@ -59,13 +64,16 @@ document.body.innerHTML = `<nav id="navbar-main" class="navbar navbar-expand-lg
</div>`;
function checkContentWarning (successfulHandler) {
const warnModal = new bootstrap.Modal(document.getElementById('warningModal'), {});
document.getElementById('accept-denial-btn').onclick = () => {
function checkContentWarning(successfulHandler) {
const warnModal = new bootstrap.Modal(
document.getElementById("warningModal"),
{},
);
document.getElementById("accept-denial-btn").onclick = () => {
document.cookie = "cw=1";
successfulHandler?.();
};
const cookies = getCookies();
if (!cookies.cw) {
warnModal.show();
@ -77,17 +85,17 @@ function checkContentWarning (successfulHandler) {
// Fl Handlers
fl.bindLoad("/connect", () => {
translate("/connect");
function initNewConnectionOpen () {
function initNewConnectionOpen() {
if (initNewConnectionOpen.interval)
clearInterval(initNewConnectionOpen.interval);
document.getElementById("custom-conn-address").value = "";
document.getElementById("custom-conn-port").value = 80;
document.getElementById("custom-conn-encryption").checked = false;
document.getElementById("custom-connect-btn").disabled = true;
const connButton = document.getElementById("custom-connect-btn");
initNewConnectionOpen.interval = setInterval(() => {
const address = document.getElementById("custom-conn-address");
// address.value
@ -96,7 +104,7 @@ fl.bindLoad("/connect", () => {
const isWSS = document.getElementById("custom-conn-encryption");
// isWSS.checked
// connButton.disabled
if (address.value.match(/^wss:\/\//)) {
address.value = address.value.slice(6);
isWSS.checked = true;
@ -106,25 +114,30 @@ fl.bindLoad("/connect", () => {
isWSS.checked = false;
}
if (address.value.match(/:[0-9]{1,5}$/)) {
const portSelected = +(address.value.match(/:[0-9]{1,5}$/).at(0).slice(1,));
address.value = address.value.slice(0, -1 * (":"+portSelected).length);
const portSelected = +address.value
.match(/:[0-9]{1,5}$/)
.at(0)
.slice(1);
address.value = address.value.slice(
0,
-1 * (":" + portSelected).length,
);
port.value = portSelected;
document.getElementById("custom-conn-port").focus();
}
if (!port.value) {
port.value = 80;
}
if (+port.value < 0)
port.value = 0;
if (+port.value > 65535)
port.value = 65535;
if (+port.value < 0) port.value = 0;
if (+port.value > 65535) port.value = 65535;
connButton.disabled = !address.value;
}, 50);
connButton.onclick = () => {
const serverlist = JSON.parse(localStorage.getItem("savedServers") ?? "null") ?? [];
const serverlist =
JSON.parse(localStorage.getItem("savedServers") ?? "null") ?? [];
serverlist.push({
address: document.getElementById("custom-conn-address").value,
port: +document.getElementById("custom-conn-port").value,
@ -132,40 +145,54 @@ fl.bindLoad("/connect", () => {
});
localStorage.setItem("savedServers", JSON.stringify(serverlist));
connectServers();
}
};
}
function connectServers () {
document.getElementById("loading-servers").innerHTML = `<center><p>${tr("/connect", "loading") ?? "Loading..."}</p></center>`;
function connectServers() {
document.getElementById("loading-servers").innerHTML =
`<center><p>${tr("/connect", "loading") ?? "Loading..."}</p></center>`;
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/active-servers.json", true);
xhr.onerror = () => {
document.getElementById("loading-servers").innerHTML = `<center>
<h3>${tr("/connect", "connection-failed") ?? "Connection failed"}</h3><hr>
<button type="button" class="btn btn-primary translate" id="retry-connecting">${tr("/connect", "retry-connecting") ?? "Retry"}</button>
</center>`;
document.getElementById("retry-connecting").onclick = () => connectServers();
}
document.getElementById("retry-connecting").onclick = () =>
connectServers();
};
xhr.onload = async () => {
if (xhr.status === 200) {
const localServers = (JSON.parse(localStorage.getItem("savedServers") ?? "null") ?? []).map(x => { x.fromLocal = true; return x });
const servers = localServers.concat(JSON.parse(xhr.response).result.map(x => { x.fromLocal = false; return x }));
const localServers = (
JSON.parse(localStorage.getItem("savedServers") ?? "null") ?? []
).map((x) => {
x.fromLocal = true;
return x;
});
const servers = localServers.concat(
JSON.parse(xhr.response).result.map((x) => {
x.fromLocal = false;
return x;
}),
);
const serversByItemID = new Object();
const serversByAddress = new Object();
const items = [];
const itemIDs = [];
const mp = servers.map(async serverItem => {
const mp = servers.map(async (serverItem) => {
const promise = new Promise((resolve, reject) => {
console.log("serverItem:", serverItem);
const trace_id = getTraceId();
const socket = new WebSocket(`${!serverItem.tls ? "ws" : "wss"}://${serverItem.address}:${serverItem.port}`);
const socket = new WebSocket(
`${!serverItem.tls ? "ws" : "wss"}://${serverItem.address}:${serverItem.port}`,
);
let ping = new Date();
socket.onopen = () => {
ping = (new Date()) - ping;
ping = new Date() - ping;
socket.onmessage = (ev) => {
const data = JSON.parse(ev.data).result;
console.debug("data:", data);
@ -176,22 +203,31 @@ fl.bindLoad("/connect", () => {
deleteID: `remove-con:${serverItem.address}:${serverItem.port}`,
address: `${serverItem.address}:${serverItem.port}`,
});
serversByItemID[`con-btn-to:${data.tag}:${serverItem.address}:${serverItem.port}`] = { data, serverItem };
serversByAddress[`${serverItem.address}:${serverItem.port}`] = { data, serverItem };
resolve(items.push(`<tr${!data.tls ? ' style="background-color: #7F6003"' : ""}>
serversByItemID[
`con-btn-to:${data.tag}:${serverItem.address}:${serverItem.port}`
] = { data, serverItem };
serversByAddress[`${serverItem.address}:${serverItem.port}`] = {
data,
serverItem,
};
resolve(
items.push(`<tr${!data.tls ? ' style="background-color: #7F6003"' : ""}>
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}>${data.name}</td>
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}>${serverItem.address}:${serverItem.port}</td>
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}>${!data.tls ? '<font color="red"><i class="fas fa-exclamation-triangle"></i></font>' : '<font color="green"><i class="fas fa-lock"></i></font>'}</td>
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}>${ping}</td>
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}><button class="btn btn-secondary" id="con-btn-to:${data.tag}:${serverItem.address}:${serverItem.port}">${tr("/connect", "connect-to-server-btn") ?? "Connect"}</button></td>
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}><button class="btn btn-outline-danger" id="remove-con:${serverItem.address}:${serverItem.port}"${!serverItem.fromLocal ? ' disabled' : ''}><i class="fas fa-trash"></i></button></td>
</tr>`));
<td${!data.tls ? ' style="background-color: #7F6003"' : ""}><button class="btn btn-outline-danger" id="remove-con:${serverItem.address}:${serverItem.port}"${!serverItem.fromLocal ? " disabled" : ""}><i class="fas fa-trash"></i></button></td>
</tr>`),
);
};
socket.send(JSON.stringify({
method: "info",
fields: "name,tag",
trace_id
}));
socket.send(
JSON.stringify({
method: "info",
fields: "name,tag",
trace_id,
}),
);
};
socket.onerror = (err) => {
console.error(err);
@ -199,23 +235,30 @@ fl.bindLoad("/connect", () => {
deleteID: `remove-con:${serverItem.address}:${serverItem.port}`,
address: `${serverItem.address}:${serverItem.port}`,
});
serversByAddress[`${serverItem.address}:${serverItem.port}`] = { serverItem };
resolve(items.push(`<tr>
serversByAddress[`${serverItem.address}:${serverItem.port}`] = {
serverItem,
};
resolve(
items.push(`<tr>
<td style="background-color: #7F0000"><font color="red">${tr("/connect", "offline-server") ?? "Offline"}</font></td>
<td style="background-color: #7F0000">${serverItem.address}:${serverItem.port}</td>
<td style="background-color: #7F0000"><font color="red"><i class="fas fa-exclamation-triangle"></i></font></td>
<td style="background-color: #7F0000"><font color="red">${tr("/connect", "offline-server") ?? "Offline"}</font></td>
<td style="background-color: #7F0000"><button class="btn btn-danger" disabled>${tr("/connect", "offline-server") ?? "Offline"}</button></td>
<td style="background-color: #7F0000"><button class="btn btn-outline-danger" id="remove-con:${serverItem.address}:${serverItem.port}"${!serverItem.fromLocal ? ' disabled' : ''}><i class="fas fa-trash"></i></button></td>
</tr>`));
<td style="background-color: #7F0000"><button class="btn btn-outline-danger" id="remove-con:${serverItem.address}:${serverItem.port}"${!serverItem.fromLocal ? " disabled" : ""}><i class="fas fa-trash"></i></button></td>
</tr>`),
);
};
});
return await promise;
});
await Promise.all(mp);
items.push(`<tr><td colspan="6"><center><button class="btn btn-primary" id="conn-to-custom">${tr("/connect", "add-custon-server") ?? "Connect to custom server"}</button></center></td></tr>`);
document.getElementById("loading-servers").innerHTML = `<table class="table table-dark">
items.push(
`<tr><td colspan="6"><center><button class="btn btn-primary" id="conn-to-custom">${tr("/connect", "add-custon-server") ?? "Connect to custom server"}</button></center></td></tr>`,
);
document.getElementById("loading-servers").innerHTML =
`<table class="table table-dark">
<thead>
<th scope="col">${tr("/connect", "server-name-tbl") ?? "Name"}</th>
<th scope="col">${tr("/connect", "server-address") ?? "Address"}</th>
@ -228,92 +271,111 @@ fl.bindLoad("/connect", () => {
${items.join("\n")}
</tbody>
</table>`;
itemIDs.forEach(btn => {
itemIDs.forEach((btn) => {
//console.log("btn:", btn);
if (btn.itemID)
document.getElementById(btn.itemID).onclick = () => {
checkContentWarning(() => {
const serverInfo = serversByItemID[btn.itemID].serverItem;
window.open(`/connect-area?server=${serverInfo.address}:${serverInfo.port}&encrypted=${serverInfo.tls ? "true" : "false"}`);
window.open(
`/connect-area?server=${serverInfo.address}:${serverInfo.port}&encrypted=${serverInfo.tls ? "true" : "false"}`,
);
});
};
document.getElementById(btn.deleteID).onclick = () => {
const serverInfo = serversByAddress[btn.address].serverItem;
console.log("serverInfo:", serverInfo);
const savedServers = (JSON.parse(localStorage.getItem("savedServers") ?? "null") ?? [])
.filter(server => {
return !(server.address === serverInfo.address && server.port === serverInfo.port)
});
const savedServers = (
JSON.parse(localStorage.getItem("savedServers") ?? "null") ?? []
).filter((server) => {
return !(
server.address === serverInfo.address &&
server.port === serverInfo.port
);
});
localStorage.setItem("savedServers", JSON.stringify(savedServers));
connectServers();
};
});
document.getElementById("conn-to-custom").onclick = function () {
const connectModal = new bootstrap.Modal(document.getElementById('connectModal'), {});
const connectModal = new bootstrap.Modal(
document.getElementById("connectModal"),
{},
);
initNewConnectionOpen();
connectModal.show();
}
};
}
};
xhr.send();
}
const cookies = getCookies();
console.log(cookies);
const cookiesModal = new bootstrap.Modal(document.getElementById('cookiesModal'), {});
const cookiesModal = new bootstrap.Modal(
document.getElementById("cookiesModal"),
{},
);
if (!cookies.accept_cookies) {
cookiesModal.show();
} else {
connectServers();
}
document.getElementById("accept-cookie-btn").onclick = () => {
document.cookie = "accept_cookies=1";
connectServers();
};
/*document.getElementById("enable-cookie-cnt").onclick = () => {
cookiesModal.show();
};*/
});
fl.bindLoad("", () => {
translate("/");
translate("/");
// carousel
function onMouse (condition) {
function onMouse(condition) {
return;
[...document.getElementsByClassName("card-img")].forEach(img => {
[...document.getElementsByClassName("card-img")].forEach((img) => {
img.style.filter = `blur(${!condition ? 5 : 0}px)`;
});
}
onMouse(false);
[...document.getElementsByClassName("carousel-item")].forEach(item => {
[...document.getElementsByClassName("carousel-item")].forEach((item) => {
item.onmouseout = () => onMouse(false);
item.onmouseover = () => onMouse(true);
});
[...document.getElementsByClassName("btn-card")].forEach(item => {
[...document.getElementsByClassName("btn-card")].forEach((item) => {
item.onmouseout = () => onMouse(true);
item.onmouseover = () => onMouse(false);
});
const interval = setInterval(() => {
const navbarHeight = +(document.getElementById("navbar-main")?.offsetHeight);
const navbarHeight = +document.getElementById("navbar-main")?.offsetHeight;
/*if (document.getElementById('main_img1')?.src) {
document.getElementById('main_img1').src = window.screen.availWidth / window.screen.availHeight > 1.45 ? "/assets/hello/1.png" : "/assets/hello/m/1.png";
document.getElementById('main_img2').src = window.screen.availWidth / window.screen.availHeight > 1.45 ? "/assets/hello/2.png" : "/assets/hello/m/2.png";
document.getElementById('main_img3').src = window.screen.availWidth / window.screen.availHeight > 1.45 ? "/assets/hello/3.png" : "/assets/hello/m/3.png";
}*/
const selectedCSS = Object.entries(document.styleSheets).filter(([key, cssFileObject]) => cssFileObject.href === `${location.origin}/css/main.css`)[0][1];
Object.entries(selectedCSS.rules).filter(([key, rule]) => rule.selectorText == '.carousel > .carousel-inner > .carousel-item > img')[0][1].style.height = `calc(100vh - ${navbarHeight}px)`;
const selectedCSS = Object.entries(document.styleSheets).filter(
([key, cssFileObject]) =>
cssFileObject.href === `${location.origin}/css/main.css`,
)[0][1];
Object.entries(selectedCSS.rules).filter(
([key, rule]) =>
rule.selectorText ==
".carousel > .carousel-inner > .carousel-item > img",
)[0][1].style.height = `calc(100vh - ${navbarHeight}px)`;
// console.log('ok');
}, 1);

View File

@ -12,57 +12,69 @@ const metaTranslate = {
};
const translationTree = {
"/": {
"en-US": {
"intro-1": "Welcome to Adventure AI Labs!",
"description-1": "Embark on an unforgettable adventure with your favorite characters and friends! Visit familiar and unfamiliar locations..",
"button-1": '<i class="fas fa-users"></i> Start the adventure',
"/": {
"en-US": {
"intro-1": "Welcome to Adventure AI Labs!",
"description-1":
"Embark on an unforgettable adventure with your favorite characters and friends! Visit familiar and unfamiliar locations..",
"button-1": '<i class="fas fa-users"></i> Start the adventure',
"intro-2": "Become a gamemaster",
"description-2": "Or you can become the gamemaster of the party and lead the plot of the characters and players like a creator god who tells an amazing story.",
"button-2": '<i class="fas fa-book"></i> Documentation',
"intro-2": "Become a gamemaster",
"description-2":
"Or you can become the gamemaster of the party and lead the plot of the characters and players like a creator god who tells an amazing story.",
"button-2": '<i class="fas fa-book"></i> Documentation',
"intro-3": "Needed a more gold..",
"description-3": "The project exists purely on a voluntary basis and the enthusiasm of the author, so if you want to help financially, then it will be very cool (:",
"button-3": '<i class="fab fa-bitcoin"></i> Donate',
},
"ru-RU": {
"intro-1": "Добро пожаловать в Adventure AI Labs!",
"description-1": "Отправьтесь в незабываемое приключение вместе со своими любимыми персонажами и друзьями! Посетите знакомые и незнакомые вам локации..",
"button-1": '<i class="fas fa-users"></i> Начать приключение',
"intro-3": "Needed a more gold..",
"description-3":
"The project exists purely on a voluntary basis and the enthusiasm of the author, so if you want to help financially, then it will be very cool (:",
"button-3": '<i class="fab fa-bitcoin"></i> Donate',
},
"ru-RU": {
"intro-1": "Добро пожаловать в Adventure AI Labs!",
"description-1":
"Отправьтесь в незабываемое приключение вместе со своими любимыми персонажами и друзьями! Посетите знакомые и незнакомые вам локации..",
"button-1": '<i class="fas fa-users"></i> Начать приключение',
"intro-2": "Стань мастером партии",
"description-2": "Либо же ты можешь стать мастером партии и вести сюжет персонажей и игроков словно бог-творец, который рассказывает удивительную историю.",
"button-2": '<i class="fas fa-book"></i> Документация',
"intro-2": "Стань мастером партии",
"description-2":
"Либо же ты можешь стать мастером партии и вести сюжет персонажей и игроков словно бог-творец, который рассказывает удивительную историю.",
"button-2": '<i class="fas fa-book"></i> Документация',
"intro-3": "Нужно больше золота..",
"description-3": "Проект существует сугубо на добровольном начале и энтузиазме автора, поэтому если вы захотите помочь финансово, то это будет очень круто (:",
"button-3": '<i class="fab fa-bitcoin"></i> Задонатить',
}
},
"/connect": {
"en-US": {
title: "Server list",
"intro-3": "Нужно больше золота..",
"description-3":
"Проект существует сугубо на добровольном начале и энтузиазме автора, поэтому если вы захотите помочь финансово, то это будет очень круто (:",
"button-3": '<i class="fab fa-bitcoin"></i> Задонатить',
},
},
"/connect": {
"en-US": {
title: "Server list",
"enable-cookie-cnt": "Enable Cookie!",
"enable-cookie-text-cnt": "To work with servers, accept the cookie agreement",
"enable-cookie-text-cnt":
"To work with servers, accept the cookie agreement",
"accept-cookie-show-btn": "Show",
exampleModalLabel: "<i class=\"fas fa-cookie\"></i> Accept cookies",
cookiesText: "We are obliged to warn you that the AI Adventure Labs protocol client uses cookies for: authorization, storage of session keys and user consent to the use of cookies",
"accept-cookie-btn": "Accept",
exampleModalLabel: '<i class="fas fa-cookie"></i> Accept cookies',
cookiesText:
"We are obliged to warn you that the AI Adventure Labs protocol client uses cookies for: authorization, storage of session keys and user consent to the use of cookies",
"accept-cookie-btn": "Accept",
loading: "Loading of servers...",
titleDenial: '<i class="fas fa-exclamation-triangle"></i> Denial of responsibility',
denialText: 'The server data does not belong to a centrally moderated foundation, community or individual, as well as to the developer of the software, program interface (protocol client), or protocol developer. Any content posted on servers is posted by the server administrator and can be absolutely anything. <b>You assume all risks for the content and connect to the server at your own peril and risk!</b>',
titleDenial:
'<i class="fas fa-exclamation-triangle"></i> Denial of responsibility',
denialText:
"The server data does not belong to a centrally moderated foundation, community or individual, as well as to the developer of the software, program interface (protocol client), or protocol developer. Any content posted on servers is posted by the server administrator and can be absolutely anything. <b>You assume all risks for the content and connect to the server at your own peril and risk!</b>",
"accept-denial-btn": "Accept",
titleCustomServerConnect: '<i class="fas fa-wifi"></i> Connect to remote server',
titleCustomConnect: 'Connection Data',
addressCustomConnect: 'Enter address of server (without ws(s):// and port)',
portCustomConnect: 'Enter port of server',
tlsCustomConnect: 'Check this if server use TLS (wss:// connection):',
titleCustomServerConnect:
'<i class="fas fa-wifi"></i> Connect to remote server',
titleCustomConnect: "Connection Data",
addressCustomConnect:
"Enter address of server (without ws(s):// and port)",
portCustomConnect: "Enter port of server",
tlsCustomConnect: "Check this if server use TLS (wss:// connection):",
"custom-connect-btn": "Connect",
"server-name-tbl": "Name",
"server-address": "Address",
"server-encryption": "Encryprtion",
@ -74,28 +86,33 @@ const translationTree = {
"connection-failed": "Connection failed",
"retry-connecting": "Retry",
},
"ru-RU": {
title: "Список серверов",
"ru-RU": {
title: "Список серверов",
"enable-cookie-cnt": "Примите соглашение о Cookie!",
"enable-cookie-text-cnt": "Для работы с серверами, примите соглашение об использовании файлов Cookie",
"enable-cookie-text-cnt":
"Для работы с серверами, примите соглашение об использовании файлов Cookie",
"accept-cookie-show-btn": "Посмотреть",
exampleModalLabel: "<i class=\"fas fa-cookie\"></i> Вопрос с печеньками",
cookiesText: "Мы обязаны вас предупредить, что клиент протокола AI Adventure Labs использует файлы куки для: авторизации, хранения ключей сессии и согласия пользователя на использование куки",
"accept-cookie-btn": "Принять",
exampleModalLabel: '<i class="fas fa-cookie"></i> Вопрос с печеньками',
cookiesText:
"Мы обязаны вас предупредить, что клиент протокола AI Adventure Labs использует файлы куки для: авторизации, хранения ключей сессии и согласия пользователя на использование куки",
"accept-cookie-btn": "Принять",
loading: "Загрузка серверов...",
titleDenial: '<i class="fas fa-exclamation-triangle"></i> Отказ от отвественности',
denialText: 'Данные сервера не принадлежат централизованно-модерируемому фонду, сообществу или частному лицу, а также разработчику программного обеспечения, програмного интерфейса (клиента протокола), разработчику протокола. Любой размещённый на серверах контент размещается администратором сервера и может быть абсолютно любым. <b>Все риски за контент вы берёте полностью на себя и подключаетесь к серверу на свой страх и риск!</b>',
titleDenial:
'<i class="fas fa-exclamation-triangle"></i> Отказ от отвественности',
denialText:
"Данные сервера не принадлежат централизованно-модерируемому фонду, сообществу или частному лицу, а также разработчику программного обеспечения, програмного интерфейса (клиента протокола), разработчику протокола. Любой размещённый на серверах контент размещается администратором сервера и может быть абсолютно любым. <b>Все риски за контент вы берёте полностью на себя и подключаетесь к серверу на свой страх и риск!</b>",
"accept-denial-btn": "Принять",
titleCustomServerConnect: '<i class="fas fa-wifi"></i> Прямое подключение к серверу',
titleCustomConnect: 'Данные подключения',
addressCustomConnect: 'Адрес сервера (без порта и ws(s)://)',
portCustomConnect: 'Порт сервера',
tlsCustomConnect: 'Использовать защищённое соединение (TLS) (wss://):',
titleCustomServerConnect:
'<i class="fas fa-wifi"></i> Прямое подключение к серверу',
titleCustomConnect: "Данные подключения",
addressCustomConnect: "Адрес сервера (без порта и ws(s)://)",
portCustomConnect: "Порт сервера",
tlsCustomConnect: "Использовать защищённое соединение (TLS) (wss://):",
"custom-connect-btn": "Подключиться",
"server-name-tbl": "Имя сервера",
"server-address": "Адрес",
"server-encryption": "Шифрование",
@ -106,33 +123,40 @@ const translationTree = {
"connect-to-server-btn": "Подключиться",
"connection-failed": "Соединение оборвалось",
"retry-connecting": "Повторить попытку",
}
},
},
},
"/connect-area": {
"en-US": {},
"ru-RU": {}
}
}
"ru-RU": {},
},
};
Object.entries(translationTree).forEach(([page, langs]) => {
Object.entries(langs).forEach(([lang]) => {
translationTree[page][lang] = {...translationTree[page][lang], ...metaTranslate[lang]};
translationTree[page][lang] = {
...translationTree[page][lang],
...metaTranslate[lang],
};
});
});
console.log(translationTree);
const simpleLangTags = {
"ru": "ru-RU",
"en": "en-US",
ru: "ru-RU",
en: "en-US",
};
function tr (page, id) {
let lang = navigator.language || navigator.userLanguage;
lang = lang.split("-").length === 1 ? (simpleLangTags[lang] ?? "en-US") : lang;
function tr(page, id) {
let lang = navigator.language || navigator.userLanguage;
lang =
lang.split("-").length === 1 ? (simpleLangTags[lang] ?? "en-US") : lang;
// Britains sucks, Sir Keir Starmer (Sir Dicksucker) is faggot, he is sucking a muslim's dicks.
if (lang === "en-UK")
lang = "en-US";
// console.log("lang is", lang);
return (translationTree[page]?.[lang]?.[id] ?? translationTree[page]?.["en-US"][id]) ?? null
if (lang === "en-UK") lang = "en-US";
// console.log("lang is", lang);
return (
translationTree[page]?.[lang]?.[id] ??
translationTree[page]?.["en-US"][id] ??
null
);
}

View File

@ -1,6 +1,12 @@
class Logger {
static log (...p) {
console.log("[", new Date(), "]:", ...p);
}
static warn (...p) {
console.warn("[", new Date(), "]:", ...p);
}
static debug (...p) {
console.debug("[", new Date(), "]:", ...p);
}
static wslog (...p) {
// return;

1597
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,17 @@
{
"dependencies": {
"aes-js": "^3.1.2",
"bcrypt": "^6.0.0",
"crypto": "^1.0.1",
"express": "^4.19.2",
"fs": "^0.0.1-security",
"https": "^1.0.0",
"md5": "^2.3.0",
"net": "^1.0.2",
"path": "^0.12.7",
"selfsigned": "^2.4.1",
"sequelize": "^6.37.7",
"sqlite3": "^5.1.7",
"ws": "^7.5.10"
},
"name": "ai-adventure-labs-server",

View File

@ -4,6 +4,8 @@ const parser = require("./packet-parser");
const frontend = require("./frontend");
const websocket = require("./ws-handler");
const crypt = require("./crypt");
const Database = require("./database");
const EventEmitter = require("events");
const net = require("net");
const fs = require("fs");
@ -11,10 +13,29 @@ const path = require("path");
const { API } = require("./api");
// So.. what part of the WeakMap/Server class made you think
// Oh! Developer of this AI client agrees that
// [Object Server].isLocked can be writeable?
const isLockedServer = new WeakMap();
class Server extends EventEmitter {
constructor() {
super();
this.on("config.update", ({ oldConfig, newConfig }) => {});
//this.isLocked = false;
isLockedServer.set(this, false);
}
set isLocked (_) {
throw new Error("isLocked not writeable");
}
get isLocked () {
return isLockedServer.get(this);
}
toggleLock (mode = true) {
isLockedServer.set(this, !!mode);
}
run(address, port) {
@ -185,7 +206,8 @@ const defaultConfigData = {
},
database: {
dialect: "sqlite",
path: "./data",
storage: "./data.db",
logging: false
},
ai: {
apiType: "kobold",
@ -199,63 +221,12 @@ const defaultConfigData = {
global.server = new Server();
cfgHandle(global.server, defaultConfigData, logger);
logger.log("Current config:", global.config);
global.server.run(global.config.server.address, global.config.server.port);
/*
if (global.config.server.secureMode) {
async function initEncryption () {
const datadir = global.config.server["ssl-path"] ?? path.join(__dirname, "/encryption");
function generate () {
logger.log("Generate new keys...");
global.openSSH = crypt.generateOpenSSH({
commonName: global.config.server.info.name
}, {
days: 365,
pkcs7: true,
clientCertificate: true,
clientCertificateCN: "AI Adventure Labs",
algorithm: 'sha256',
keySize: 2048,
});
fs.writeFileSync(path.join(datadir, "public.pem"), global.openSSH.public_key.bytes);
fs.writeFileSync(path.join(datadir, "private.pem"), global.openSSH.private_key.bytes);
fs.writeFileSync(path.join(datadir, "cert.crt"), global.openSSH.cert);
}
const readFilePromise = new Promise((resolve, reject) => {
fs.readFile(path.join(datadir, "public.pem"), (err, public_key) => {
if (err) {
return resolve(generate());
}
try {
public_key = new crypt.CryptKey(public_key);
const private_key = new crypt.CryptKey(fs.readFileSync(path.join(datadir, "private.pem")));
const cert = fs.readFileSync(path.join(datadir, "cert.crt"));
global.openSSH = new crypt.OpenSSH({
public_key, private_key, cert
});
resolve();
} catch (_) {
resolve(generate());
}
});
});
await readFilePromise;
logger.log(`loaded from ${datadir}`);
};
initEncryption().then(() => {
server.run(global.config.server.address, global.config.server.port);
}).catch(e => {
throw e;
global.database = new Database(global.config.database);
global.database.init()
.then(() => {
logger.log("Current config:", global.config);
global.server.run(global.config.server.address, global.config.server.port);
})
.catch((err) => {
throw err;
});
}
else
server.run(global.config.server.address, global.config.server.port);
*/