mirror of
https://github.com/lovasoa/whitebophir
synced 2024-11-10 06:24:17 +00:00
Reformat all the server code with prettier
This commit is contained in:
parent
a76bbeced3
commit
53c61ec16e
11 changed files with 1225 additions and 1085 deletions
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* WHITEBOPHIR SERVER
|
||||
*********************************************************
|
||||
* @licstart The following is the entire license notice for the
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page.
|
||||
*
|
||||
* Copyright (C) 2013-2014 Ophir LOJKINE
|
||||
|
@ -25,10 +25,10 @@
|
|||
* @module boardData
|
||||
*/
|
||||
|
||||
var fs = require('./fs_promises.js')
|
||||
, log = require("./log.js").log
|
||||
, path = require("path")
|
||||
, config = require("./configuration.js");
|
||||
var fs = require("./fs_promises.js"),
|
||||
log = require("./log.js").log,
|
||||
path = require("path"),
|
||||
config = require("./configuration.js");
|
||||
|
||||
/**
|
||||
* Represents a board.
|
||||
|
@ -37,21 +37,24 @@ var fs = require('./fs_promises.js')
|
|||
* @param {string} name
|
||||
*/
|
||||
var BoardData = function (name) {
|
||||
this.name = name;
|
||||
/** @type {{[name: string]: {[object_id:string]: any}}} */
|
||||
this.board = {};
|
||||
this.file = path.join(config.HISTORY_DIR, "board-" + encodeURIComponent(name) + ".json");
|
||||
this.lastSaveDate = Date.now();
|
||||
this.users = new Set();
|
||||
this.name = name;
|
||||
/** @type {{[name: string]: {[object_id:string]: any}}} */
|
||||
this.board = {};
|
||||
this.file = path.join(
|
||||
config.HISTORY_DIR,
|
||||
"board-" + encodeURIComponent(name) + ".json"
|
||||
);
|
||||
this.lastSaveDate = Date.now();
|
||||
this.users = new Set();
|
||||
};
|
||||
|
||||
/** Adds data to the board */
|
||||
BoardData.prototype.set = function (id, data) {
|
||||
//KISS
|
||||
data.time = Date.now();
|
||||
this.validate(data);
|
||||
this.board[id] = data;
|
||||
this.delaySave();
|
||||
//KISS
|
||||
data.time = Date.now();
|
||||
this.validate(data);
|
||||
this.board[id] = data;
|
||||
this.delaySave();
|
||||
};
|
||||
|
||||
/** Adds a child to an element that is already in the board
|
||||
|
@ -59,45 +62,45 @@ BoardData.prototype.set = function (id, data) {
|
|||
* @param {object} child - Object containing the the values to update.
|
||||
* @param {boolean} [create=true] - Whether to create an empty parent if it doesn't exist
|
||||
* @returns {boolean} - True if the child was added, else false
|
||||
*/
|
||||
*/
|
||||
BoardData.prototype.addChild = function (parentId, child) {
|
||||
var obj = this.board[parentId];
|
||||
if (typeof obj !== "object") return false;
|
||||
if (Array.isArray(obj._children)) obj._children.push(child);
|
||||
else obj._children = [child];
|
||||
var obj = this.board[parentId];
|
||||
if (typeof obj !== "object") return false;
|
||||
if (Array.isArray(obj._children)) obj._children.push(child);
|
||||
else obj._children = [child];
|
||||
|
||||
this.validate(obj);
|
||||
this.delaySave();
|
||||
return true;
|
||||
this.validate(obj);
|
||||
this.delaySave();
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Update the data in the board
|
||||
* @param {string} id - Identifier of the data to update.
|
||||
* @param {object} data - Object containing the values to update.
|
||||
* @param {boolean} create - True if the object should be created if it's not currently in the DB.
|
||||
*/
|
||||
*/
|
||||
BoardData.prototype.update = function (id, data, create) {
|
||||
delete data.type;
|
||||
delete data.tool;
|
||||
delete data.type;
|
||||
delete data.tool;
|
||||
|
||||
var obj = this.board[id];
|
||||
if (typeof obj === "object") {
|
||||
for (var i in data) {
|
||||
obj[i] = data[i];
|
||||
}
|
||||
} else if (create || obj !== undefined) {
|
||||
this.board[id] = data;
|
||||
}
|
||||
this.delaySave();
|
||||
var obj = this.board[id];
|
||||
if (typeof obj === "object") {
|
||||
for (var i in data) {
|
||||
obj[i] = data[i];
|
||||
}
|
||||
} else if (create || obj !== undefined) {
|
||||
this.board[id] = data;
|
||||
}
|
||||
this.delaySave();
|
||||
};
|
||||
|
||||
/** Removes data from the board
|
||||
* @param {string} id - Identifier of the data to delete.
|
||||
*/
|
||||
BoardData.prototype.delete = function (id) {
|
||||
//KISS
|
||||
delete this.board[id];
|
||||
this.delaySave();
|
||||
//KISS
|
||||
delete this.board[id];
|
||||
this.delaySave();
|
||||
};
|
||||
|
||||
/** Reads data from the board
|
||||
|
@ -105,7 +108,7 @@ BoardData.prototype.delete = function (id) {
|
|||
* @returns {object} The element with the given id, or undefined if no element has this id
|
||||
*/
|
||||
BoardData.prototype.get = function (id, children) {
|
||||
return this.board[id];
|
||||
return this.board[id];
|
||||
};
|
||||
|
||||
/** Reads data from the board
|
||||
|
@ -113,13 +116,13 @@ BoardData.prototype.get = function (id, children) {
|
|||
* @param {BoardData~processData} callback - Function to be called with each piece of data read
|
||||
*/
|
||||
BoardData.prototype.getAll = function (id) {
|
||||
var results = [];
|
||||
for (var i in this.board) {
|
||||
if (!id || i > id) {
|
||||
results.push(this.board[i]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
var results = [];
|
||||
for (var i in this.board) {
|
||||
if (!id || i > id) {
|
||||
results.push(this.board[i]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -129,134 +132,139 @@ BoardData.prototype.getAll = function (id) {
|
|||
* @param {object} data
|
||||
*/
|
||||
|
||||
|
||||
/** Delays the triggering of auto-save by SAVE_INTERVAL seconds
|
||||
*/
|
||||
*/
|
||||
BoardData.prototype.delaySave = function (file) {
|
||||
if (this.saveTimeoutId !== undefined) clearTimeout(this.saveTimeoutId);
|
||||
this.saveTimeoutId = setTimeout(this.save.bind(this), config.SAVE_INTERVAL);
|
||||
if (Date.now() - this.lastSaveDate > config.MAX_SAVE_DELAY) setTimeout(this.save.bind(this), 0);
|
||||
if (this.saveTimeoutId !== undefined) clearTimeout(this.saveTimeoutId);
|
||||
this.saveTimeoutId = setTimeout(this.save.bind(this), config.SAVE_INTERVAL);
|
||||
if (Date.now() - this.lastSaveDate > config.MAX_SAVE_DELAY)
|
||||
setTimeout(this.save.bind(this), 0);
|
||||
};
|
||||
|
||||
/** Saves the data in the board to a file.
|
||||
* @param {string} [file=this.file] - Path to the file where the board data will be saved.
|
||||
*/
|
||||
*/
|
||||
BoardData.prototype.save = async function (file) {
|
||||
this.lastSaveDate = Date.now();
|
||||
this.clean();
|
||||
if (!file) file = this.file;
|
||||
var tmp_file = backupFileName(file);
|
||||
var board_txt = JSON.stringify(this.board);
|
||||
if (board_txt === "{}") { // empty board
|
||||
try {
|
||||
await fs.promises.unlink(file);
|
||||
log("removed empty board", { 'name': this.name });
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
// If the file already wasn't saved, this is not an error
|
||||
log("board deletion error", { "err": err.toString() })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await fs.promises.writeFile(tmp_file, board_txt);
|
||||
await fs.promises.rename(tmp_file, file);
|
||||
log("saved board", {
|
||||
'name': this.name,
|
||||
'size': board_txt.length,
|
||||
'delay_ms': (Date.now() - this.lastSaveDate),
|
||||
});
|
||||
} catch (err) {
|
||||
log("board saving error", {
|
||||
'err': err.toString(),
|
||||
'tmp_file': tmp_file,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.lastSaveDate = Date.now();
|
||||
this.clean();
|
||||
if (!file) file = this.file;
|
||||
var tmp_file = backupFileName(file);
|
||||
var board_txt = JSON.stringify(this.board);
|
||||
if (board_txt === "{}") {
|
||||
// empty board
|
||||
try {
|
||||
await fs.promises.unlink(file);
|
||||
log("removed empty board", { name: this.name });
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
// If the file already wasn't saved, this is not an error
|
||||
log("board deletion error", { err: err.toString() });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await fs.promises.writeFile(tmp_file, board_txt);
|
||||
await fs.promises.rename(tmp_file, file);
|
||||
log("saved board", {
|
||||
name: this.name,
|
||||
size: board_txt.length,
|
||||
delay_ms: Date.now() - this.lastSaveDate,
|
||||
});
|
||||
} catch (err) {
|
||||
log("board saving error", {
|
||||
err: err.toString(),
|
||||
tmp_file: tmp_file,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Remove old elements from the board */
|
||||
BoardData.prototype.clean = function cleanBoard() {
|
||||
var board = this.board;
|
||||
var ids = Object.keys(board);
|
||||
if (ids.length > config.MAX_ITEM_COUNT) {
|
||||
var toDestroy = ids.sort(function (x, y) {
|
||||
return (board[x].time | 0) - (board[y].time | 0);
|
||||
}).slice(0, -config.MAX_ITEM_COUNT);
|
||||
for (var i = 0; i < toDestroy.length; i++) delete board[toDestroy[i]];
|
||||
log("cleaned board", { 'removed': toDestroy.length, "board": this.name });
|
||||
}
|
||||
}
|
||||
var board = this.board;
|
||||
var ids = Object.keys(board);
|
||||
if (ids.length > config.MAX_ITEM_COUNT) {
|
||||
var toDestroy = ids
|
||||
.sort(function (x, y) {
|
||||
return (board[x].time | 0) - (board[y].time | 0);
|
||||
})
|
||||
.slice(0, -config.MAX_ITEM_COUNT);
|
||||
for (var i = 0; i < toDestroy.length; i++) delete board[toDestroy[i]];
|
||||
log("cleaned board", { removed: toDestroy.length, board: this.name });
|
||||
}
|
||||
};
|
||||
|
||||
/** Reformats an item if necessary in order to make it follow the boards' policy
|
||||
/** Reformats an item if necessary in order to make it follow the boards' policy
|
||||
* @param {object} item The object to edit
|
||||
* @param {object} parent The parent of the object to edit
|
||||
*/
|
||||
*/
|
||||
BoardData.prototype.validate = function validate(item, parent) {
|
||||
if (item.hasOwnProperty("size")) {
|
||||
item.size = parseInt(item.size) || 1;
|
||||
item.size = Math.min(Math.max(item.size, 1), 50);
|
||||
}
|
||||
if (item.hasOwnProperty("x") || item.hasOwnProperty("y")) {
|
||||
item.x = parseFloat(item.x) || 0;
|
||||
item.x = Math.min(Math.max(item.x, 0), config.MAX_BOARD_SIZE);
|
||||
item.x = Math.round(10 * item.x) / 10;
|
||||
item.y = parseFloat(item.y) || 0;
|
||||
item.y = Math.min(Math.max(item.y, 0), config.MAX_BOARD_SIZE);
|
||||
item.y = Math.round(10 * item.y) / 10;
|
||||
}
|
||||
if (item.hasOwnProperty("opacity")) {
|
||||
item.opacity = Math.min(Math.max(item.opacity, 0.1), 1) || 1;
|
||||
if (item.opacity === 1) delete item.opacity;
|
||||
}
|
||||
if (item.hasOwnProperty("_children")) {
|
||||
if (!Array.isArray(item._children)) item._children = [];
|
||||
if (item._children.length > config.MAX_CHILDREN) item._children.length = config.MAX_CHILDREN;
|
||||
for (var i = 0; i < item._children.length; i++) {
|
||||
this.validate(item._children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.hasOwnProperty("size")) {
|
||||
item.size = parseInt(item.size) || 1;
|
||||
item.size = Math.min(Math.max(item.size, 1), 50);
|
||||
}
|
||||
if (item.hasOwnProperty("x") || item.hasOwnProperty("y")) {
|
||||
item.x = parseFloat(item.x) || 0;
|
||||
item.x = Math.min(Math.max(item.x, 0), config.MAX_BOARD_SIZE);
|
||||
item.x = Math.round(10 * item.x) / 10;
|
||||
item.y = parseFloat(item.y) || 0;
|
||||
item.y = Math.min(Math.max(item.y, 0), config.MAX_BOARD_SIZE);
|
||||
item.y = Math.round(10 * item.y) / 10;
|
||||
}
|
||||
if (item.hasOwnProperty("opacity")) {
|
||||
item.opacity = Math.min(Math.max(item.opacity, 0.1), 1) || 1;
|
||||
if (item.opacity === 1) delete item.opacity;
|
||||
}
|
||||
if (item.hasOwnProperty("_children")) {
|
||||
if (!Array.isArray(item._children)) item._children = [];
|
||||
if (item._children.length > config.MAX_CHILDREN)
|
||||
item._children.length = config.MAX_CHILDREN;
|
||||
for (var i = 0; i < item._children.length; i++) {
|
||||
this.validate(item._children[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Load the data in the board from a file.
|
||||
* @param {string} name - name of the board
|
||||
*/
|
||||
*/
|
||||
BoardData.load = async function loadBoard(name) {
|
||||
var boardData = new BoardData(name), data;
|
||||
try {
|
||||
data = await fs.promises.readFile(boardData.file);
|
||||
boardData.board = JSON.parse(data);
|
||||
for (id in boardData.board) boardData.validate(boardData.board[id]);
|
||||
log('disk load', { 'board': boardData.name });
|
||||
} catch (e) {
|
||||
log('empty board creation', {
|
||||
'board': boardData.name,
|
||||
// If the file doesn't exist, this is not an error
|
||||
"error": e.code !== "ENOENT" && e.toString(),
|
||||
});
|
||||
boardData.board = {}
|
||||
if (data) {
|
||||
// There was an error loading the board, but some data was still read
|
||||
var backup = backupFileName(boardData.file);
|
||||
log("Writing the corrupted file to " + backup);
|
||||
try {
|
||||
await fs.promises.writeFile(backup, data);
|
||||
} catch (err) {
|
||||
log("Error writing " + backup + ": " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return boardData;
|
||||
var boardData = new BoardData(name),
|
||||
data;
|
||||
try {
|
||||
data = await fs.promises.readFile(boardData.file);
|
||||
boardData.board = JSON.parse(data);
|
||||
for (id in boardData.board) boardData.validate(boardData.board[id]);
|
||||
log("disk load", { board: boardData.name });
|
||||
} catch (e) {
|
||||
log("empty board creation", {
|
||||
board: boardData.name,
|
||||
// If the file doesn't exist, this is not an error
|
||||
error: e.code !== "ENOENT" && e.toString(),
|
||||
});
|
||||
boardData.board = {};
|
||||
if (data) {
|
||||
// There was an error loading the board, but some data was still read
|
||||
var backup = backupFileName(boardData.file);
|
||||
log("Writing the corrupted file to " + backup);
|
||||
try {
|
||||
await fs.promises.writeFile(backup, data);
|
||||
} catch (err) {
|
||||
log("Error writing " + backup + ": " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return boardData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a board file name, return a name to use for temporary data saving.
|
||||
* @param {string} baseName
|
||||
* Given a board file name, return a name to use for temporary data saving.
|
||||
* @param {string} baseName
|
||||
*/
|
||||
function backupFileName(baseName) {
|
||||
var date = new Date().toISOString().replace(/:/g, '');
|
||||
return baseName + '.' + date + '.bak';
|
||||
var date = new Date().toISOString().replace(/:/g, "");
|
||||
return baseName + "." + date + ".bak";
|
||||
}
|
||||
|
||||
module.exports.BoardData = BoardData;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const fs = require("./fs_promises");
|
||||
const path = require('path');
|
||||
let os = require('os');
|
||||
const path = require("path");
|
||||
let os = require("os");
|
||||
|
||||
const { R_OK, W_OK } = fs.constants;
|
||||
|
||||
|
@ -10,56 +10,66 @@ const { R_OK, W_OK } = fs.constants;
|
|||
* @returns {string?}
|
||||
*/
|
||||
async function get_error(directory) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
return "does not exist";
|
||||
}
|
||||
if (!fs.statSync(directory).isDirectory()) {
|
||||
error = "exists, but is not a directory";
|
||||
}
|
||||
const { uid, gid } = os.userInfo();
|
||||
const tmpfile = path.join(directory, Math.random() + ".json");
|
||||
try {
|
||||
fs.writeFileSync(tmpfile, "{}");
|
||||
fs.unlinkSync(tmpfile);
|
||||
} catch (e) {
|
||||
return "does not allow file creation and deletion. " +
|
||||
"Check the permissions of the directory, and if needed change them so that " +
|
||||
`user with UID ${uid} has access to them. This can be achieved by running the command: chown ${uid}:${gid} on the directory`;
|
||||
}
|
||||
const fileChecks = [];
|
||||
const files = await fs.promises.readdir(directory, {withFileTypes: true});
|
||||
for (const elem of files) {
|
||||
if (/^board-(.*)\.json$/.test(elem.name)) {
|
||||
const elemPath = path.join(directory, elem.name);
|
||||
if (!elem.isFile()) return `contains a board file named "${elemPath}" which is not a normal file`
|
||||
fileChecks.push(fs.promises.access(elemPath, R_OK | W_OK)
|
||||
.catch(function () { return elemPath }))
|
||||
}
|
||||
}
|
||||
const errs = (await Promise.all(fileChecks)).filter(function (x) { return x });
|
||||
if (errs.length > 0) {
|
||||
return `contains the following board files that are not readable and writable by the current user: "` +
|
||||
errs.join('", "') +
|
||||
`". Please make all board files accessible with chown 1000:1000`
|
||||
if (!fs.existsSync(directory)) {
|
||||
return "does not exist";
|
||||
}
|
||||
if (!fs.statSync(directory).isDirectory()) {
|
||||
error = "exists, but is not a directory";
|
||||
}
|
||||
const { uid, gid } = os.userInfo();
|
||||
const tmpfile = path.join(directory, Math.random() + ".json");
|
||||
try {
|
||||
fs.writeFileSync(tmpfile, "{}");
|
||||
fs.unlinkSync(tmpfile);
|
||||
} catch (e) {
|
||||
return (
|
||||
"does not allow file creation and deletion. " +
|
||||
"Check the permissions of the directory, and if needed change them so that " +
|
||||
`user with UID ${uid} has access to them. This can be achieved by running the command: chown ${uid}:${gid} on the directory`
|
||||
);
|
||||
}
|
||||
const fileChecks = [];
|
||||
const files = await fs.promises.readdir(directory, { withFileTypes: true });
|
||||
for (const elem of files) {
|
||||
if (/^board-(.*)\.json$/.test(elem.name)) {
|
||||
const elemPath = path.join(directory, elem.name);
|
||||
if (!elem.isFile())
|
||||
return `contains a board file named "${elemPath}" which is not a normal file`;
|
||||
fileChecks.push(
|
||||
fs.promises.access(elemPath, R_OK | W_OK).catch(function () {
|
||||
return elemPath;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const errs = (await Promise.all(fileChecks)).filter(function (x) {
|
||||
return x;
|
||||
});
|
||||
if (errs.length > 0) {
|
||||
return (
|
||||
`contains the following board files that are not readable and writable by the current user: "` +
|
||||
errs.join('", "') +
|
||||
`". Please make all board files accessible with chown 1000:1000`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the output directory is writeable,
|
||||
* Checks that the output directory is writeable,
|
||||
* ans exits the current process with an error otherwise.
|
||||
* @param {string} directory
|
||||
*/
|
||||
function check_output_directory(directory) {
|
||||
get_error(directory).then(function (error) {
|
||||
if (error) {
|
||||
console.error(
|
||||
`The configured history directory in which boards are stored ${error}.` +
|
||||
`\nThe history directory can be configured with the environment variable HISTORY_DIR. ` +
|
||||
`It is currently set to "${directory}".`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
get_error(directory).then(function (error) {
|
||||
if (error) {
|
||||
console.error(
|
||||
`The configured history directory in which boards are stored ${error}.` +
|
||||
`\nThe history directory can be configured with the environment variable HISTORY_DIR. ` +
|
||||
`It is currently set to "${directory}".`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = check_output_directory
|
||||
module.exports = check_output_directory;
|
||||
|
|
|
@ -2,8 +2,8 @@ const config = require("./configuration");
|
|||
|
||||
/** Settings that should be handed through to the clients */
|
||||
module.exports = {
|
||||
"MAX_BOARD_SIZE": config.MAX_BOARD_SIZE,
|
||||
"MAX_EMIT_COUNT": config.MAX_EMIT_COUNT,
|
||||
"MAX_EMIT_COUNT_PERIOD": config.MAX_EMIT_COUNT_PERIOD,
|
||||
"BLOCKED_TOOLS": config.BLOCKED_TOOLS,
|
||||
};
|
||||
MAX_BOARD_SIZE: config.MAX_BOARD_SIZE,
|
||||
MAX_EMIT_COUNT: config.MAX_EMIT_COUNT,
|
||||
MAX_EMIT_COUNT_PERIOD: config.MAX_EMIT_COUNT_PERIOD,
|
||||
BLOCKED_TOOLS: config.BLOCKED_TOOLS,
|
||||
};
|
||||
|
|
|
@ -2,41 +2,43 @@ const path = require("path");
|
|||
const app_root = path.dirname(__dirname); // Parent of the directory where this file is
|
||||
|
||||
module.exports = {
|
||||
/** Port on which the application will listen */
|
||||
PORT: parseInt(process.env['PORT']) || 8080,
|
||||
/** Port on which the application will listen */
|
||||
PORT: parseInt(process.env["PORT"]) || 8080,
|
||||
|
||||
/** Host on which the application will listen (defaults to undefined,
|
||||
/** Host on which the application will listen (defaults to undefined,
|
||||
hence listen on all interfaces on all IP addresses, but could also be
|
||||
'127.0.0.1' **/
|
||||
HOST: process.env['HOST'] || undefined,
|
||||
HOST: process.env["HOST"] || undefined,
|
||||
|
||||
/** Path to the directory where boards will be saved by default */
|
||||
HISTORY_DIR: process.env['WBO_HISTORY_DIR'] || path.join(app_root, "server-data"),
|
||||
/** Path to the directory where boards will be saved by default */
|
||||
HISTORY_DIR:
|
||||
process.env["WBO_HISTORY_DIR"] || path.join(app_root, "server-data"),
|
||||
|
||||
/** Folder from which static files will be served */
|
||||
WEBROOT: process.env['WBO_WEBROOT'] || path.join(app_root, "client-data"),
|
||||
/** Folder from which static files will be served */
|
||||
WEBROOT: process.env["WBO_WEBROOT"] || path.join(app_root, "client-data"),
|
||||
|
||||
/** Number of milliseconds of inactivity after which the board should be saved to a file */
|
||||
SAVE_INTERVAL: parseInt(process.env['WBO_SAVE_INTERVAL']) || 1000 * 2, // Save after 2 seconds of inactivity
|
||||
/** Number of milliseconds of inactivity after which the board should be saved to a file */
|
||||
SAVE_INTERVAL: parseInt(process.env["WBO_SAVE_INTERVAL"]) || 1000 * 2, // Save after 2 seconds of inactivity
|
||||
|
||||
/** Periodicity at which the board should be saved when it is being actively used (milliseconds) */
|
||||
MAX_SAVE_DELAY: parseInt(process.env['WBO_MAX_SAVE_DELAY']) || 1000 * 60, // Save after 60 seconds even if there is still activity
|
||||
/** Periodicity at which the board should be saved when it is being actively used (milliseconds) */
|
||||
MAX_SAVE_DELAY: parseInt(process.env["WBO_MAX_SAVE_DELAY"]) || 1000 * 60, // Save after 60 seconds even if there is still activity
|
||||
|
||||
/** Maximal number of items to keep in the board. When there are more items, the oldest ones are deleted */
|
||||
MAX_ITEM_COUNT: parseInt(process.env['WBO_MAX_ITEM_COUNT']) || 32768,
|
||||
/** Maximal number of items to keep in the board. When there are more items, the oldest ones are deleted */
|
||||
MAX_ITEM_COUNT: parseInt(process.env["WBO_MAX_ITEM_COUNT"]) || 32768,
|
||||
|
||||
/** Max number of sub-items in an item. This prevents flooding */
|
||||
MAX_CHILDREN: parseInt(process.env['WBO_MAX_CHILDREN']) || 192,
|
||||
/** Max number of sub-items in an item. This prevents flooding */
|
||||
MAX_CHILDREN: parseInt(process.env["WBO_MAX_CHILDREN"]) || 192,
|
||||
|
||||
/** Maximum value for any x or y on the board */
|
||||
MAX_BOARD_SIZE: parseInt(process.env['WBO_MAX_BOARD_SIZE']) || 65536,
|
||||
/** Maximum value for any x or y on the board */
|
||||
MAX_BOARD_SIZE: parseInt(process.env["WBO_MAX_BOARD_SIZE"]) || 65536,
|
||||
|
||||
/** Maximum messages per user over the given time period before banning them */
|
||||
MAX_EMIT_COUNT: parseInt(process.env['WBO_MAX_EMIT_COUNT']) || 192,
|
||||
/** Maximum messages per user over the given time period before banning them */
|
||||
MAX_EMIT_COUNT: parseInt(process.env["WBO_MAX_EMIT_COUNT"]) || 192,
|
||||
|
||||
/** Duration after which the emit count is reset in miliseconds */
|
||||
MAX_EMIT_COUNT_PERIOD: parseInt(process.env['WBO_MAX_EMIT_COUNT_PERIOD']) || 4096,
|
||||
/** Duration after which the emit count is reset in miliseconds */
|
||||
MAX_EMIT_COUNT_PERIOD:
|
||||
parseInt(process.env["WBO_MAX_EMIT_COUNT_PERIOD"]) || 4096,
|
||||
|
||||
/** Blocked Tools. A comma-separated list of tools that should not appear on boards. */
|
||||
BLOCKED_TOOLS: (process.env['WBO_BLOCKED_TOOLS'] || "").split(','),
|
||||
/** Blocked Tools. A comma-separated list of tools that should not appear on boards. */
|
||||
BLOCKED_TOOLS: (process.env["WBO_BLOCKED_TOOLS"] || "").split(","),
|
||||
};
|
||||
|
|
|
@ -1,147 +1,224 @@
|
|||
const fs = require("./fs_promises.js"),
|
||||
path = require("path"),
|
||||
wboPencilPoint = require("../client-data/tools/pencil/wbo_pencil_point.js").wboPencilPoint;
|
||||
path = require("path"),
|
||||
wboPencilPoint = require("../client-data/tools/pencil/wbo_pencil_point.js")
|
||||
.wboPencilPoint;
|
||||
|
||||
function htmlspecialchars(str) {
|
||||
if (typeof str !== "string") return "";
|
||||
if (typeof str !== "string") return "";
|
||||
|
||||
return str.replace(/[<>&"']/g, function (c) {
|
||||
switch (c) {
|
||||
case '<': return '<';
|
||||
case '>': return '>';
|
||||
case '&': return '&';
|
||||
case '"': return '"';
|
||||
case "'": return ''';
|
||||
}
|
||||
});
|
||||
return str.replace(/[<>&"']/g, function (c) {
|
||||
switch (c) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
case '"':
|
||||
return """;
|
||||
case "'":
|
||||
return "'";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderPath(el, pathstring) {
|
||||
return '<path ' +
|
||||
(el.id ?
|
||||
('id="' + htmlspecialchars(el.id) + '" ') : '') +
|
||||
'stroke-width="' + (el.size | 0) + '" ' +
|
||||
(el.opacity ?
|
||||
('opacity="' + parseFloat(el.opacity) + '" ') : '') +
|
||||
'stroke="' + htmlspecialchars(el.color) + '" ' +
|
||||
'd="' + pathstring + '" ' +
|
||||
(el.deltax || el.deltay ?
|
||||
('transform="translate(' + (+el.deltax) + ',' + (+el.deltay) + ')"') : '') +
|
||||
'/>';
|
||||
return (
|
||||
"<path " +
|
||||
(el.id ? 'id="' + htmlspecialchars(el.id) + '" ' : "") +
|
||||
'stroke-width="' +
|
||||
(el.size | 0) +
|
||||
'" ' +
|
||||
(el.opacity ? 'opacity="' + parseFloat(el.opacity) + '" ' : "") +
|
||||
'stroke="' +
|
||||
htmlspecialchars(el.color) +
|
||||
'" ' +
|
||||
'd="' +
|
||||
pathstring +
|
||||
'" ' +
|
||||
(el.deltax || el.deltay
|
||||
? 'transform="translate(' + +el.deltax + "," + +el.deltay + ')"'
|
||||
: "") +
|
||||
"/>"
|
||||
);
|
||||
}
|
||||
|
||||
const Tools = {
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
"Text": function (el) {
|
||||
return '<text ' +
|
||||
'id="' + htmlspecialchars(el.id || "t") + '" ' +
|
||||
'x="' + (el.x | 0) + '" ' +
|
||||
'y="' + (el.y | 0) + '" ' +
|
||||
'font-size="' + (el.size | 0) + '" ' +
|
||||
'fill="' + htmlspecialchars(el.color || "#000") + '" ' +
|
||||
(el.deltax || el.deltay ? ('transform="translate(' + (el.deltax || 0) + ',' + (el.deltay || 0) + ')"') : '') +
|
||||
'>' + htmlspecialchars(el.txt || "") + '</text>';
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
"Pencil": function (el) {
|
||||
if (!el._children) return "";
|
||||
let pts = el._children.reduce(function (pts, point) {
|
||||
return wboPencilPoint(pts, point.x, point.y);
|
||||
}, []);
|
||||
const pathstring = pts.map(function (op) {
|
||||
return op.type + ' ' + op.values.join(' ')
|
||||
}).join(' ');
|
||||
return renderPath(el, pathstring);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
"Rectangle": function (el) {
|
||||
return '<rect ' +
|
||||
(el.id ?
|
||||
('id="' + htmlspecialchars(el.id) + '" ') : '') +
|
||||
'x="' + (el.x || 0) + '" ' +
|
||||
'y="' + (el.y || 0) + '" ' +
|
||||
'width="' + (el.x2 - el.x) + '" ' +
|
||||
'height="' + (el.y2 - el.y) + '" ' +
|
||||
'stroke="' + htmlspecialchars(el.color) + '" ' +
|
||||
'stroke-width="' + (el.size | 0) + '" ' +
|
||||
(el.deltax || el.deltay ? ('transform="translate(' + (el.deltax || 0) + ',' + (el.deltay || 0) + ')"') : '') +
|
||||
'/>';
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
"Ellipse": function (el) {
|
||||
const cx = Math.round((el.x2 + el.x) / 2);
|
||||
const cy = Math.round((el.y2 + el.y) / 2);
|
||||
const rx = Math.abs(el.x2 - el.x) / 2;
|
||||
const ry = Math.abs(el.y2 - el.y) / 2;
|
||||
const pathstring =
|
||||
"M" + (cx - rx) + " " + cy +
|
||||
"a" + rx + "," + ry + " 0 1,0 " + (rx * 2) + ",0" +
|
||||
"a" + rx + "," + ry + " 0 1,0 " + (rx * -2) + ",0";
|
||||
return renderPath(el, pathstring);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
"Straight line": function (el) {
|
||||
const pathstring = "M" + el.x + " " + el.y + "L" + el.x2 + " " + el.y2;
|
||||
return renderPath(el, pathstring);
|
||||
}
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
Text: function (el) {
|
||||
return (
|
||||
"<text " +
|
||||
'id="' +
|
||||
htmlspecialchars(el.id || "t") +
|
||||
'" ' +
|
||||
'x="' +
|
||||
(el.x | 0) +
|
||||
'" ' +
|
||||
'y="' +
|
||||
(el.y | 0) +
|
||||
'" ' +
|
||||
'font-size="' +
|
||||
(el.size | 0) +
|
||||
'" ' +
|
||||
'fill="' +
|
||||
htmlspecialchars(el.color || "#000") +
|
||||
'" ' +
|
||||
(el.deltax || el.deltay
|
||||
? 'transform="translate(' +
|
||||
(el.deltax || 0) +
|
||||
"," +
|
||||
(el.deltay || 0) +
|
||||
')"'
|
||||
: "") +
|
||||
">" +
|
||||
htmlspecialchars(el.txt || "") +
|
||||
"</text>"
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
Pencil: function (el) {
|
||||
if (!el._children) return "";
|
||||
let pts = el._children.reduce(function (pts, point) {
|
||||
return wboPencilPoint(pts, point.x, point.y);
|
||||
}, []);
|
||||
const pathstring = pts
|
||||
.map(function (op) {
|
||||
return op.type + " " + op.values.join(" ");
|
||||
})
|
||||
.join(" ");
|
||||
return renderPath(el, pathstring);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
Rectangle: function (el) {
|
||||
return (
|
||||
"<rect " +
|
||||
(el.id ? 'id="' + htmlspecialchars(el.id) + '" ' : "") +
|
||||
'x="' +
|
||||
(el.x || 0) +
|
||||
'" ' +
|
||||
'y="' +
|
||||
(el.y || 0) +
|
||||
'" ' +
|
||||
'width="' +
|
||||
(el.x2 - el.x) +
|
||||
'" ' +
|
||||
'height="' +
|
||||
(el.y2 - el.y) +
|
||||
'" ' +
|
||||
'stroke="' +
|
||||
htmlspecialchars(el.color) +
|
||||
'" ' +
|
||||
'stroke-width="' +
|
||||
(el.size | 0) +
|
||||
'" ' +
|
||||
(el.deltax || el.deltay
|
||||
? 'transform="translate(' +
|
||||
(el.deltax || 0) +
|
||||
"," +
|
||||
(el.deltay || 0) +
|
||||
')"'
|
||||
: "") +
|
||||
"/>"
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
Ellipse: function (el) {
|
||||
const cx = Math.round((el.x2 + el.x) / 2);
|
||||
const cy = Math.round((el.y2 + el.y) / 2);
|
||||
const rx = Math.abs(el.x2 - el.x) / 2;
|
||||
const ry = Math.abs(el.y2 - el.y) / 2;
|
||||
const pathstring =
|
||||
"M" +
|
||||
(cx - rx) +
|
||||
" " +
|
||||
cy +
|
||||
"a" +
|
||||
rx +
|
||||
"," +
|
||||
ry +
|
||||
" 0 1,0 " +
|
||||
rx * 2 +
|
||||
",0" +
|
||||
"a" +
|
||||
rx +
|
||||
"," +
|
||||
ry +
|
||||
" 0 1,0 " +
|
||||
rx * -2 +
|
||||
",0";
|
||||
return renderPath(el, pathstring);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
"Straight line": function (el) {
|
||||
const pathstring = "M" + el.x + " " + el.y + "L" + el.x2 + " " + el.y2;
|
||||
return renderPath(el, pathstring);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Writes the given board as an svg to the given writeable stream
|
||||
* @param {Object[string, BoardElem]} obj
|
||||
* @param {WritableStream} writeable
|
||||
* @param {Object[string, BoardElem]} obj
|
||||
* @param {WritableStream} writeable
|
||||
*/
|
||||
async function toSVG(obj, writeable) {
|
||||
const margin = 400;
|
||||
const elems = Object.values(obj);
|
||||
const dim = elems.reduce(function (dim, elem) {
|
||||
if (elem._children) elem = elem._children[0];
|
||||
return [
|
||||
Math.max(elem.x + margin + (elem.deltax | 0) | 0, dim[0]),
|
||||
Math.max(elem.y + margin + (elem.deltay | 0) | 0, dim[1]),
|
||||
]
|
||||
}, [margin, margin]);
|
||||
writeable.write(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' +
|
||||
'width="' + dim[0] + '" height="' + dim[1] + '">' +
|
||||
'<defs><style type="text/css"><![CDATA[' +
|
||||
'text {font-family:"Arial"}' +
|
||||
'path {fill:none;stroke-linecap:round;stroke-linejoin:round;}' +
|
||||
'rect {fill:none}' +
|
||||
']]></style></defs>'
|
||||
);
|
||||
await Promise.all(elems.map(async function (elem) {
|
||||
await Promise.resolve(); // Do not block the event loop
|
||||
const renderFun = Tools[elem.tool];
|
||||
if (renderFun) writeable.write(renderFun(elem));
|
||||
else console.warn("Missing render function for tool", elem.tool);
|
||||
}));
|
||||
writeable.write('</svg>');
|
||||
const margin = 400;
|
||||
const elems = Object.values(obj);
|
||||
const dim = elems.reduce(
|
||||
function (dim, elem) {
|
||||
if (elem._children) elem = elem._children[0];
|
||||
return [
|
||||
Math.max((elem.x + margin + (elem.deltax | 0)) | 0, dim[0]),
|
||||
Math.max((elem.y + margin + (elem.deltay | 0)) | 0, dim[1]),
|
||||
];
|
||||
},
|
||||
[margin, margin]
|
||||
);
|
||||
writeable.write(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' +
|
||||
'width="' +
|
||||
dim[0] +
|
||||
'" height="' +
|
||||
dim[1] +
|
||||
'">' +
|
||||
'<defs><style type="text/css"><![CDATA[' +
|
||||
'text {font-family:"Arial"}' +
|
||||
"path {fill:none;stroke-linecap:round;stroke-linejoin:round;}" +
|
||||
"rect {fill:none}" +
|
||||
"]]></style></defs>"
|
||||
);
|
||||
await Promise.all(
|
||||
elems.map(async function (elem) {
|
||||
await Promise.resolve(); // Do not block the event loop
|
||||
const renderFun = Tools[elem.tool];
|
||||
if (renderFun) writeable.write(renderFun(elem));
|
||||
else console.warn("Missing render function for tool", elem.tool);
|
||||
})
|
||||
);
|
||||
writeable.write("</svg>");
|
||||
}
|
||||
|
||||
async function renderBoard(file, stream) {
|
||||
const data = await fs.promises.readFile(file);
|
||||
var board = JSON.parse(data);
|
||||
return toSVG(board, stream);
|
||||
const data = await fs.promises.readFile(file);
|
||||
var board = JSON.parse(data);
|
||||
return toSVG(board, stream);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const config = require("./configuration.js");
|
||||
const HISTORY_FILE = process.argv[2] || path.join(config.HISTORY_DIR, "board-anonymous.json");
|
||||
const config = require("./configuration.js");
|
||||
const HISTORY_FILE =
|
||||
process.argv[2] || path.join(config.HISTORY_DIR, "board-anonymous.json");
|
||||
|
||||
renderBoard(HISTORY_FILE, process.stdout)
|
||||
.catch(console.error.bind(console));
|
||||
renderBoard(HISTORY_FILE, process.stdout).catch(console.error.bind(console));
|
||||
} else {
|
||||
module.exports = { 'renderBoard': renderBoard };
|
||||
module.exports = { renderBoard: renderBoard };
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const fs = require('fs');
|
||||
const fs = require("fs");
|
||||
|
||||
if (typeof fs.promises === "undefined") {
|
||||
console.warn("Using an old node version without fs.promises");
|
||||
console.warn("Using an old node version without fs.promises");
|
||||
|
||||
const util = require("util");
|
||||
fs.promises = {};
|
||||
Object.entries(fs)
|
||||
.filter(([_, v]) => typeof v === 'function')
|
||||
.forEach(([k, v]) => fs.promises[k] = util.promisify(v))
|
||||
const util = require("util");
|
||||
fs.promises = {};
|
||||
Object.entries(fs)
|
||||
.filter(([_, v]) => typeof v === "function")
|
||||
.forEach(([k, v]) => (fs.promises[k] = util.promisify(v)));
|
||||
}
|
||||
|
||||
module.exports = fs;
|
||||
module.exports = fs;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Add a message to the logs
|
||||
* @param {string} type
|
||||
* @param {any} infos
|
||||
* @param {string} type
|
||||
* @param {any} infos
|
||||
*/
|
||||
function log(type, infos) {
|
||||
var msg = new Date().toISOString() + '\t' + type;
|
||||
if (infos) msg += '\t' + JSON.stringify(infos);
|
||||
console.log(msg);
|
||||
var msg = new Date().toISOString() + "\t" + type;
|
||||
if (infos) msg += "\t" + JSON.stringify(infos);
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
module.exports.log = log;
|
||||
module.exports.log = log;
|
||||
|
|
306
server/server.js
306
server/server.js
|
@ -1,24 +1,27 @@
|
|||
var app = require('http').createServer(handler)
|
||||
, sockets = require('./sockets.js')
|
||||
, log = require("./log.js").log
|
||||
, path = require('path')
|
||||
, url = require('url')
|
||||
, fs = require("fs")
|
||||
, crypto = require("crypto")
|
||||
, serveStatic = require("serve-static")
|
||||
, createSVG = require("./createSVG.js")
|
||||
, templating = require("./templating.js")
|
||||
, config = require("./configuration.js")
|
||||
, polyfillLibrary = require('polyfill-library')
|
||||
, check_output_directory = require("./check_output_directory.js");
|
||||
|
||||
var app = require("http").createServer(handler),
|
||||
sockets = require("./sockets.js"),
|
||||
log = require("./log.js").log,
|
||||
path = require("path"),
|
||||
url = require("url"),
|
||||
fs = require("fs"),
|
||||
crypto = require("crypto"),
|
||||
serveStatic = require("serve-static"),
|
||||
createSVG = require("./createSVG.js"),
|
||||
templating = require("./templating.js"),
|
||||
config = require("./configuration.js"),
|
||||
polyfillLibrary = require("polyfill-library"),
|
||||
check_output_directory = require("./check_output_directory.js");
|
||||
|
||||
var MIN_NODE_VERSION = 10.0;
|
||||
|
||||
if (parseFloat(process.versions.node) < MIN_NODE_VERSION) {
|
||||
console.warn(
|
||||
"!!! You are using node " + process.version +
|
||||
", wbo requires at least " + MIN_NODE_VERSION + " !!!");
|
||||
console.warn(
|
||||
"!!! You are using node " +
|
||||
process.version +
|
||||
", wbo requires at least " +
|
||||
MIN_NODE_VERSION +
|
||||
" !!!"
|
||||
);
|
||||
}
|
||||
|
||||
check_output_directory(config.HISTORY_DIR);
|
||||
|
@ -28,168 +31,191 @@ sockets.start(app);
|
|||
app.listen(config.PORT, config.HOST);
|
||||
log("server started", { port: config.PORT });
|
||||
|
||||
var CSP = "default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:";
|
||||
var CSP =
|
||||
"default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:";
|
||||
|
||||
var fileserver = serveStatic(config.WEBROOT, {
|
||||
maxAge: 2 * 3600 * 1000,
|
||||
setHeaders: function (res) {
|
||||
res.setHeader("X-UA-Compatible", "IE=Edge");
|
||||
res.setHeader("Content-Security-Policy", CSP);
|
||||
}
|
||||
maxAge: 2 * 3600 * 1000,
|
||||
setHeaders: function (res) {
|
||||
res.setHeader("X-UA-Compatible", "IE=Edge");
|
||||
res.setHeader("Content-Security-Policy", CSP);
|
||||
},
|
||||
});
|
||||
|
||||
var errorPage = fs.readFileSync(path.join(config.WEBROOT, "error.html"));
|
||||
function serveError(request, response) {
|
||||
return function (err) {
|
||||
log("error", { "error": err && err.toString(), "url": request.url });
|
||||
response.writeHead(err ? 500 : 404, { "Content-Length": errorPage.length });
|
||||
response.end(errorPage);
|
||||
}
|
||||
return function (err) {
|
||||
log("error", { error: err && err.toString(), url: request.url });
|
||||
response.writeHead(err ? 500 : 404, { "Content-Length": errorPage.length });
|
||||
response.end(errorPage);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a request to the logs
|
||||
* @param {import("http").IncomingMessage} request
|
||||
* @param {import("http").IncomingMessage} request
|
||||
*/
|
||||
function logRequest(request) {
|
||||
log('connection', {
|
||||
ip: request.connection.remoteAddress,
|
||||
original_ip: request.headers['x-forwarded-for'] || request.headers['forwarded'],
|
||||
user_agent: request.headers['user-agent'],
|
||||
referer: request.headers['referer'],
|
||||
language: request.headers['accept-language'],
|
||||
url: request.url,
|
||||
});
|
||||
log("connection", {
|
||||
ip: request.connection.remoteAddress,
|
||||
original_ip:
|
||||
request.headers["x-forwarded-for"] || request.headers["forwarded"],
|
||||
user_agent: request.headers["user-agent"],
|
||||
referer: request.headers["referer"],
|
||||
language: request.headers["accept-language"],
|
||||
url: request.url,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('http').RequestListener}
|
||||
*/
|
||||
function handler(request, response) {
|
||||
try {
|
||||
handleRequest(request, response);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
response.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
response.end(err.toString());
|
||||
}
|
||||
try {
|
||||
handleRequest(request, response);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
response.writeHead(500, { "Content-Type": "text/plain" });
|
||||
response.end(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
const boardTemplate = new templating.BoardTemplate(path.join(config.WEBROOT, 'board.html'));
|
||||
const indexTemplate = new templating.Template(path.join(config.WEBROOT, 'index.html'));
|
||||
const boardTemplate = new templating.BoardTemplate(
|
||||
path.join(config.WEBROOT, "board.html")
|
||||
);
|
||||
const indexTemplate = new templating.Template(
|
||||
path.join(config.WEBROOT, "index.html")
|
||||
);
|
||||
|
||||
/**
|
||||
* Throws an error if the given board name is not allowed
|
||||
* @param {string} boardName
|
||||
* @param {string} boardName
|
||||
* @throws {Error}
|
||||
*/
|
||||
function validateBoardName(boardName) {
|
||||
if (/^[\w%\-_~()]*$/.test(boardName)) return boardName;
|
||||
throw new Error("Illegal board name: " + boardName);
|
||||
if (/^[\w%\-_~()]*$/.test(boardName)) return boardName;
|
||||
throw new Error("Illegal board name: " + boardName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('http').RequestListener}
|
||||
*/
|
||||
function handleRequest(request, response) {
|
||||
var parsedUrl = url.parse(request.url, true);
|
||||
var parts = parsedUrl.pathname.split('/');
|
||||
if (parts[0] === '') parts.shift();
|
||||
var parsedUrl = url.parse(request.url, true);
|
||||
var parts = parsedUrl.pathname.split("/");
|
||||
if (parts[0] === "") parts.shift();
|
||||
|
||||
switch (parts[0]) {
|
||||
case "boards":
|
||||
// "boards" refers to the root directory
|
||||
if (parts.length === 1) {
|
||||
// '/boards?board=...' This allows html forms to point to boards
|
||||
var boardName = parsedUrl.query.board || "anonymous";
|
||||
var headers = { Location: 'boards/' + encodeURIComponent(boardName) };
|
||||
response.writeHead(301, headers);
|
||||
response.end();
|
||||
} else if (parts.length === 2 && request.url.indexOf('.') === -1) {
|
||||
validateBoardName(parts[1]);
|
||||
// If there is no dot and no directory, parts[1] is the board name
|
||||
boardTemplate.serve(request, response);
|
||||
} else { // Else, it's a resource
|
||||
request.url = "/" + parts.slice(1).join('/');
|
||||
fileserver(request, response, serveError(request, response));
|
||||
}
|
||||
break;
|
||||
switch (parts[0]) {
|
||||
case "boards":
|
||||
// "boards" refers to the root directory
|
||||
if (parts.length === 1) {
|
||||
// '/boards?board=...' This allows html forms to point to boards
|
||||
var boardName = parsedUrl.query.board || "anonymous";
|
||||
var headers = { Location: "boards/" + encodeURIComponent(boardName) };
|
||||
response.writeHead(301, headers);
|
||||
response.end();
|
||||
} else if (parts.length === 2 && request.url.indexOf(".") === -1) {
|
||||
validateBoardName(parts[1]);
|
||||
// If there is no dot and no directory, parts[1] is the board name
|
||||
boardTemplate.serve(request, response);
|
||||
} else {
|
||||
// Else, it's a resource
|
||||
request.url = "/" + parts.slice(1).join("/");
|
||||
fileserver(request, response, serveError(request, response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "download":
|
||||
var boardName = validateBoardName(parts[1]),
|
||||
history_file = path.join(config.HISTORY_DIR, "board-" + boardName + ".json");
|
||||
if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) {
|
||||
history_file += '.' + parts[2] + '.bak';
|
||||
}
|
||||
log("download", { "file": history_file });
|
||||
fs.readFile(history_file, function (err, data) {
|
||||
if (err) return serveError(request, response)(err);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": 'attachment; filename="' + boardName + '.wbo"',
|
||||
"Content-Length": data.length,
|
||||
});
|
||||
response.end(data);
|
||||
});
|
||||
break;
|
||||
case "download":
|
||||
var boardName = validateBoardName(parts[1]),
|
||||
history_file = path.join(
|
||||
config.HISTORY_DIR,
|
||||
"board-" + boardName + ".json"
|
||||
);
|
||||
if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) {
|
||||
history_file += "." + parts[2] + ".bak";
|
||||
}
|
||||
log("download", { file: history_file });
|
||||
fs.readFile(history_file, function (err, data) {
|
||||
if (err) return serveError(request, response)(err);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": 'attachment; filename="' + boardName + '.wbo"',
|
||||
"Content-Length": data.length,
|
||||
});
|
||||
response.end(data);
|
||||
});
|
||||
break;
|
||||
|
||||
case "export":
|
||||
case "preview":
|
||||
var boardName = validateBoardName(parts[1]),
|
||||
history_file = path.join(config.HISTORY_DIR, "board-" + boardName + ".json");
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Content-Security-Policy": CSP,
|
||||
"Cache-Control": "public, max-age=30",
|
||||
});
|
||||
var t = Date.now();
|
||||
createSVG.renderBoard(history_file, response).then(function () {
|
||||
log("preview", { "board": boardName, "time": Date.now() - t });
|
||||
response.end();
|
||||
}).catch(function (err) {
|
||||
log("error", { "error": err.toString(), "stack": err.stack });
|
||||
response.end('<text>Sorry, an error occured</text>');
|
||||
});
|
||||
break;
|
||||
case "export":
|
||||
case "preview":
|
||||
var boardName = validateBoardName(parts[1]),
|
||||
history_file = path.join(
|
||||
config.HISTORY_DIR,
|
||||
"board-" + boardName + ".json"
|
||||
);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Content-Security-Policy": CSP,
|
||||
"Cache-Control": "public, max-age=30",
|
||||
});
|
||||
var t = Date.now();
|
||||
createSVG
|
||||
.renderBoard(history_file, response)
|
||||
.then(function () {
|
||||
log("preview", { board: boardName, time: Date.now() - t });
|
||||
response.end();
|
||||
})
|
||||
.catch(function (err) {
|
||||
log("error", { error: err.toString(), stack: err.stack });
|
||||
response.end("<text>Sorry, an error occured</text>");
|
||||
});
|
||||
break;
|
||||
|
||||
case "random":
|
||||
var name = crypto.randomBytes(32).toString('base64').replace(/[^\w]/g, '-');
|
||||
response.writeHead(307, { 'Location': 'boards/' + name });
|
||||
response.end(name);
|
||||
break;
|
||||
case "random":
|
||||
var name = crypto
|
||||
.randomBytes(32)
|
||||
.toString("base64")
|
||||
.replace(/[^\w]/g, "-");
|
||||
response.writeHead(307, { Location: "boards/" + name });
|
||||
response.end(name);
|
||||
break;
|
||||
|
||||
case "polyfill.js": // serve tailored polyfills
|
||||
case "polyfill.min.js":
|
||||
polyfillLibrary.getPolyfillString({
|
||||
uaString: request.headers['user-agent'],
|
||||
minify: request.url.endsWith(".min.js"),
|
||||
features: {
|
||||
'default': { flags: ['gated'] },
|
||||
'es5': { flags: ['gated'] },
|
||||
'es6': { flags: ['gated'] },
|
||||
'es7': { flags: ['gated'] },
|
||||
'es2017': { flags: ['gated'] },
|
||||
'es2018': { flags: ['gated'] },
|
||||
'es2019': { flags: ['gated'] },
|
||||
'performance.now': { flags: ['gated'] },
|
||||
}
|
||||
}).then(function (bundleString) {
|
||||
response.setHeader('Cache-Control', 'private, max-age=172800, stale-while-revalidate=1728000');
|
||||
response.setHeader('Vary', 'User-Agent');
|
||||
response.setHeader('Content-Type', 'application/javascript');
|
||||
response.end(bundleString);
|
||||
});
|
||||
break;
|
||||
case "polyfill.js": // serve tailored polyfills
|
||||
case "polyfill.min.js":
|
||||
polyfillLibrary
|
||||
.getPolyfillString({
|
||||
uaString: request.headers["user-agent"],
|
||||
minify: request.url.endsWith(".min.js"),
|
||||
features: {
|
||||
default: { flags: ["gated"] },
|
||||
es5: { flags: ["gated"] },
|
||||
es6: { flags: ["gated"] },
|
||||
es7: { flags: ["gated"] },
|
||||
es2017: { flags: ["gated"] },
|
||||
es2018: { flags: ["gated"] },
|
||||
es2019: { flags: ["gated"] },
|
||||
"performance.now": { flags: ["gated"] },
|
||||
},
|
||||
})
|
||||
.then(function (bundleString) {
|
||||
response.setHeader(
|
||||
"Cache-Control",
|
||||
"private, max-age=172800, stale-while-revalidate=1728000"
|
||||
);
|
||||
response.setHeader("Vary", "User-Agent");
|
||||
response.setHeader("Content-Type", "application/javascript");
|
||||
response.end(bundleString);
|
||||
});
|
||||
break;
|
||||
|
||||
case "": // Index page
|
||||
logRequest(request);
|
||||
indexTemplate.serve(request, response);
|
||||
break;
|
||||
case "": // Index page
|
||||
logRequest(request);
|
||||
indexTemplate.serve(request, response);
|
||||
break;
|
||||
|
||||
default:
|
||||
fileserver(request, response, serveError(request, response));
|
||||
}
|
||||
default:
|
||||
fileserver(request, response, serveError(request, response));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = app;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
var iolib = require('socket.io')
|
||||
, log = require("./log.js").log
|
||||
, BoardData = require("./boardData.js").BoardData
|
||||
, config = require("./configuration");
|
||||
var iolib = require("socket.io"),
|
||||
log = require("./log.js").log,
|
||||
BoardData = require("./boardData.js").BoardData,
|
||||
config = require("./configuration");
|
||||
|
||||
/** Map from name to *promises* of BoardData
|
||||
@type {Object<string, Promise<BoardData>>}
|
||||
|
@ -13,168 +13,179 @@ var boards = {};
|
|||
* If the inner function throws, the outer function just returns undefined
|
||||
* and logs the error.
|
||||
* @template A
|
||||
* @param {A} fn
|
||||
* @param {A} fn
|
||||
* @returns {A}
|
||||
*/
|
||||
function noFail(fn) {
|
||||
return function noFailWrapped(arg) {
|
||||
try {
|
||||
return fn(arg);
|
||||
} catch (e) {
|
||||
console.trace(e);
|
||||
}
|
||||
}
|
||||
return function noFailWrapped(arg) {
|
||||
try {
|
||||
return fn(arg);
|
||||
} catch (e) {
|
||||
console.trace(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function startIO(app) {
|
||||
io = iolib(app);
|
||||
io.on('connection', noFail(socketConnection));
|
||||
return io;
|
||||
io = iolib(app);
|
||||
io.on("connection", noFail(socketConnection));
|
||||
return io;
|
||||
}
|
||||
|
||||
/** Returns a promise to a BoardData with the given name
|
||||
* @returns {Promise<BoardData>}
|
||||
*/
|
||||
*/
|
||||
function getBoard(name) {
|
||||
if (boards.hasOwnProperty(name)) {
|
||||
return boards[name];
|
||||
} else {
|
||||
var board = BoardData.load(name);
|
||||
boards[name] = board;
|
||||
return board;
|
||||
}
|
||||
if (boards.hasOwnProperty(name)) {
|
||||
return boards[name];
|
||||
} else {
|
||||
var board = BoardData.load(name);
|
||||
boards[name] = board;
|
||||
return board;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes on every new connection
|
||||
* @param {iolib.Socket} socket
|
||||
* @param {iolib.Socket} socket
|
||||
*/
|
||||
function socketConnection(socket) {
|
||||
/**
|
||||
* Function to call when an user joins a board
|
||||
* @param {string} name
|
||||
*/
|
||||
async function joinBoard(name) {
|
||||
// Default to the public board
|
||||
if (!name) name = "anonymous";
|
||||
|
||||
/**
|
||||
* Function to call when an user joins a board
|
||||
* @param {string} name
|
||||
*/
|
||||
async function joinBoard(name) {
|
||||
// Default to the public board
|
||||
if (!name) name = "anonymous";
|
||||
// Join the board
|
||||
socket.join(name);
|
||||
|
||||
// Join the board
|
||||
socket.join(name);
|
||||
var board = await getBoard(name);
|
||||
board.users.add(socket.id);
|
||||
log("board joined", { board: board.name, users: board.users.size });
|
||||
return board;
|
||||
}
|
||||
|
||||
var board = await getBoard(name);
|
||||
board.users.add(socket.id);
|
||||
log('board joined', { 'board': board.name, 'users': board.users.size });
|
||||
return board;
|
||||
}
|
||||
socket.on(
|
||||
"error",
|
||||
noFail(function onError(error) {
|
||||
log("ERROR", error);
|
||||
})
|
||||
);
|
||||
|
||||
socket.on("error", noFail(function onError(error) {
|
||||
log("ERROR", error);
|
||||
}));
|
||||
socket.on("getboard", async function onGetBoard(name) {
|
||||
var board = await joinBoard(name);
|
||||
//Send all the board's data as soon as it's loaded
|
||||
socket.emit("broadcast", { _children: board.getAll() });
|
||||
});
|
||||
|
||||
socket.on("getboard", async function onGetBoard(name) {
|
||||
var board = await joinBoard(name);
|
||||
//Send all the board's data as soon as it's loaded
|
||||
socket.emit("broadcast", { _children: board.getAll() });
|
||||
});
|
||||
socket.on("joinboard", noFail(joinBoard));
|
||||
|
||||
socket.on("joinboard", noFail(joinBoard));
|
||||
var lastEmitSecond = (Date.now() / config.MAX_EMIT_COUNT_PERIOD) | 0;
|
||||
var emitCount = 0;
|
||||
socket.on(
|
||||
"broadcast",
|
||||
noFail(function onBroadcast(message) {
|
||||
var currentSecond = (Date.now() / config.MAX_EMIT_COUNT_PERIOD) | 0;
|
||||
if (currentSecond === lastEmitSecond) {
|
||||
emitCount++;
|
||||
if (emitCount > config.MAX_EMIT_COUNT) {
|
||||
var request = socket.client.request;
|
||||
if (emitCount % 100 === 0) {
|
||||
log("BANNED", {
|
||||
user_agent: request.headers["user-agent"],
|
||||
original_ip:
|
||||
request.headers["x-forwarded-for"] ||
|
||||
request.headers["forwarded"],
|
||||
emit_count: emitCount,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
emitCount = 0;
|
||||
lastEmitSecond = currentSecond;
|
||||
}
|
||||
|
||||
var lastEmitSecond = Date.now() / config.MAX_EMIT_COUNT_PERIOD | 0;
|
||||
var emitCount = 0;
|
||||
socket.on('broadcast', noFail(function onBroadcast(message) {
|
||||
var currentSecond = Date.now() / config.MAX_EMIT_COUNT_PERIOD | 0;
|
||||
if (currentSecond === lastEmitSecond) {
|
||||
emitCount++;
|
||||
if (emitCount > config.MAX_EMIT_COUNT) {
|
||||
var request = socket.client.request;
|
||||
if (emitCount % 100 === 0) {
|
||||
log('BANNED', {
|
||||
user_agent: request.headers['user-agent'],
|
||||
original_ip: request.headers['x-forwarded-for'] || request.headers['forwarded'],
|
||||
emit_count: emitCount
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
emitCount = 0;
|
||||
lastEmitSecond = currentSecond;
|
||||
}
|
||||
var boardName = message.board || "anonymous";
|
||||
var data = message.data;
|
||||
|
||||
var boardName = message.board || "anonymous";
|
||||
var data = message.data;
|
||||
if (!socket.rooms.has(boardName)) socket.join(boardName);
|
||||
|
||||
if (!socket.rooms.has(boardName)) socket.join(boardName);
|
||||
if (!data) {
|
||||
console.warn("Received invalid message: %s.", JSON.stringify(message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
console.warn("Received invalid message: %s.", JSON.stringify(message));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!message.data.tool ||
|
||||
config.BLOCKED_TOOLS.includes(message.data.tool)
|
||||
) {
|
||||
log("BLOCKED MESSAGE", message.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.data.tool || config.BLOCKED_TOOLS.includes(message.data.tool)) {
|
||||
log('BLOCKED MESSAGE', message.data);
|
||||
return;
|
||||
}
|
||||
// Save the message in the board
|
||||
handleMessage(boardName, data, socket);
|
||||
|
||||
// Save the message in the board
|
||||
handleMessage(boardName, data, socket);
|
||||
//Send data to all other users connected on the same board
|
||||
socket.broadcast.to(boardName).emit("broadcast", data);
|
||||
})
|
||||
);
|
||||
|
||||
//Send data to all other users connected on the same board
|
||||
socket.broadcast.to(boardName).emit('broadcast', data);
|
||||
}));
|
||||
|
||||
socket.on('disconnecting', function onDisconnecting(reason) {
|
||||
socket.rooms.forEach(async function disconnectFrom(room) {
|
||||
if (boards.hasOwnProperty(room)) {
|
||||
var board = await boards[room];
|
||||
board.users.delete(socket.id);
|
||||
var userCount = board.users.size;
|
||||
log('disconnection', { 'board': board.name, 'users': board.users.size });
|
||||
if (userCount === 0) {
|
||||
board.save();
|
||||
delete boards[room];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
socket.on("disconnecting", function onDisconnecting(reason) {
|
||||
socket.rooms.forEach(async function disconnectFrom(room) {
|
||||
if (boards.hasOwnProperty(room)) {
|
||||
var board = await boards[room];
|
||||
board.users.delete(socket.id);
|
||||
var userCount = board.users.size;
|
||||
log("disconnection", { board: board.name, users: board.users.size });
|
||||
if (userCount === 0) {
|
||||
board.save();
|
||||
delete boards[room];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(boardName, message, socket) {
|
||||
if (message.tool === "Cursor") {
|
||||
message.socket = socket.id;
|
||||
} else {
|
||||
saveHistory(boardName, message);
|
||||
}
|
||||
if (message.tool === "Cursor") {
|
||||
message.socket = socket.id;
|
||||
} else {
|
||||
saveHistory(boardName, message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHistory(boardName, message) {
|
||||
var id = message.id;
|
||||
var board = await getBoard(boardName);
|
||||
switch (message.type) {
|
||||
case "delete":
|
||||
if (id) board.delete(id);
|
||||
break;
|
||||
case "update":
|
||||
if (id) board.update(id, message);
|
||||
break;
|
||||
case "child":
|
||||
board.addChild(message.parent, message);
|
||||
break;
|
||||
default: //Add data
|
||||
if (!id) throw new Error("Invalid message: ", message);
|
||||
board.set(id, message);
|
||||
}
|
||||
var id = message.id;
|
||||
var board = await getBoard(boardName);
|
||||
switch (message.type) {
|
||||
case "delete":
|
||||
if (id) board.delete(id);
|
||||
break;
|
||||
case "update":
|
||||
if (id) board.update(id, message);
|
||||
break;
|
||||
case "child":
|
||||
board.addChild(message.parent, message);
|
||||
break;
|
||||
default:
|
||||
//Add data
|
||||
if (!id) throw new Error("Invalid message: ", message);
|
||||
board.set(id, message);
|
||||
}
|
||||
}
|
||||
|
||||
function generateUID(prefix, suffix) {
|
||||
var uid = Date.now().toString(36); //Create the uids in chronological order
|
||||
uid += (Math.round(Math.random() * 36)).toString(36); //Add a random character at the end
|
||||
if (prefix) uid = prefix + uid;
|
||||
if (suffix) uid = uid + suffix;
|
||||
return uid;
|
||||
var uid = Date.now().toString(36); //Create the uids in chronological order
|
||||
uid += Math.round(Math.random() * 36).toString(36); //Add a random character at the end
|
||||
if (prefix) uid = prefix + uid;
|
||||
if (suffix) uid = uid + suffix;
|
||||
return uid;
|
||||
}
|
||||
|
||||
if (exports) {
|
||||
exports.start = startIO;
|
||||
exports.start = startIO;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const handlebars = require("handlebars");
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const url = require("url");
|
||||
const accept_language_parser = require('accept-language-parser');
|
||||
const accept_language_parser = require("accept-language-parser");
|
||||
const client_config = require("./client_configuration");
|
||||
|
||||
/**
|
||||
|
@ -10,60 +10,66 @@ const client_config = require("./client_configuration");
|
|||
* @const
|
||||
* @type {object}
|
||||
*/
|
||||
const TRANSLATIONS = JSON.parse(fs.readFileSync(path.join(__dirname, "translations.json")));
|
||||
const TRANSLATIONS = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "translations.json"))
|
||||
);
|
||||
const languages = Object.keys(TRANSLATIONS);
|
||||
|
||||
handlebars.registerHelper({
|
||||
json: JSON.stringify.bind(JSON)
|
||||
json: JSON.stringify.bind(JSON),
|
||||
});
|
||||
|
||||
function findBaseUrl(req) {
|
||||
var proto = req.headers['X-Forwarded-Proto'] || (req.connection.encrypted ? 'https' : 'http');
|
||||
var host = req.headers['X-Forwarded-Host'] || req.headers.host;
|
||||
return proto + '://' + host;
|
||||
var proto =
|
||||
req.headers["X-Forwarded-Proto"] ||
|
||||
(req.connection.encrypted ? "https" : "http");
|
||||
var host = req.headers["X-Forwarded-Host"] || req.headers.host;
|
||||
return proto + "://" + host;
|
||||
}
|
||||
|
||||
class Template {
|
||||
constructor(path) {
|
||||
const contents = fs.readFileSync(path, { encoding: 'utf8' });
|
||||
this.template = handlebars.compile(contents);
|
||||
}
|
||||
parameters(parsedUrl, request) {
|
||||
const accept_languages = parsedUrl.query.lang || request.headers['accept-language'];
|
||||
const opts = { loose: true };
|
||||
const language = accept_language_parser.pick(languages, accept_languages, opts) || 'en';
|
||||
const translations = TRANSLATIONS[language] || {};
|
||||
const configuration = client_config || {};
|
||||
const prefix = request.url.split("/boards/")[0].substr(1);
|
||||
const baseUrl = findBaseUrl(request) + (prefix ? prefix + "/" : "");
|
||||
return { baseUrl, languages, language, translations, configuration };
|
||||
}
|
||||
serve(request, response) {
|
||||
const parsedUrl = url.parse(request.url, true);
|
||||
const parameters = this.parameters(parsedUrl, request);
|
||||
var body = this.template(parameters);
|
||||
var headers = {
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'Content-Type': 'text/html',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
};
|
||||
if (!parsedUrl.query.lang) {
|
||||
headers["Vary"] = 'Accept-Language';
|
||||
}
|
||||
response.writeHead(200, headers);
|
||||
response.end(body);
|
||||
constructor(path) {
|
||||
const contents = fs.readFileSync(path, { encoding: "utf8" });
|
||||
this.template = handlebars.compile(contents);
|
||||
}
|
||||
parameters(parsedUrl, request) {
|
||||
const accept_languages =
|
||||
parsedUrl.query.lang || request.headers["accept-language"];
|
||||
const opts = { loose: true };
|
||||
const language =
|
||||
accept_language_parser.pick(languages, accept_languages, opts) || "en";
|
||||
const translations = TRANSLATIONS[language] || {};
|
||||
const configuration = client_config || {};
|
||||
const prefix = request.url.split("/boards/")[0].substr(1);
|
||||
const baseUrl = findBaseUrl(request) + (prefix ? prefix + "/" : "");
|
||||
return { baseUrl, languages, language, translations, configuration };
|
||||
}
|
||||
serve(request, response) {
|
||||
const parsedUrl = url.parse(request.url, true);
|
||||
const parameters = this.parameters(parsedUrl, request);
|
||||
var body = this.template(parameters);
|
||||
var headers = {
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
"Content-Type": "text/html",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
};
|
||||
if (!parsedUrl.query.lang) {
|
||||
headers["Vary"] = "Accept-Language";
|
||||
}
|
||||
response.writeHead(200, headers);
|
||||
response.end(body);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardTemplate extends Template {
|
||||
parameters(parsedUrl, request) {
|
||||
const params = super.parameters(parsedUrl, request);
|
||||
const parts = parsedUrl.pathname.split('boards/', 2);
|
||||
const boardUriComponent = parts[1];
|
||||
params['boardUriComponent'] = boardUriComponent;
|
||||
params['board'] = decodeURIComponent(boardUriComponent);
|
||||
return params;
|
||||
}
|
||||
parameters(parsedUrl, request) {
|
||||
const params = super.parameters(parsedUrl, request);
|
||||
const parts = parsedUrl.pathname.split("boards/", 2);
|
||||
const boardUriComponent = parts[1];
|
||||
params["boardUriComponent"] = boardUriComponent;
|
||||
params["board"] = decodeURIComponent(boardUriComponent);
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Template, BoardTemplate };
|
||||
module.exports = { Template, BoardTemplate };
|
||||
|
|
|
@ -1,417 +1,417 @@
|
|||
{
|
||||
"en": {
|
||||
"hand": "Hand",
|
||||
"loading": "Loading",
|
||||
"tagline": "A free and open-source online collaborative drawing tool. Sketch new ideas together on WBO!",
|
||||
"configuration": "Configuration",
|
||||
"collaborative_whiteboard": "Collaborative whiteboard",
|
||||
"size": "Size",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Tools",
|
||||
"rectangle": "Rectangle",
|
||||
"square": "Square",
|
||||
"circle": "Circle",
|
||||
"ellipse": "Ellipse",
|
||||
"click_to_toggle": "click to toggle",
|
||||
"menu": "Menu",
|
||||
"text": "Text",
|
||||
"mover": "Mover",
|
||||
"straight_line": "Straight line",
|
||||
"pencil": "Pencil",
|
||||
"grid": "Grid",
|
||||
"click_to_zoom": "Click to zoom in\nPress shift and click to zoom out",
|
||||
"keyboard_shortcut": "keyboard shortcut",
|
||||
"mousewheel": "mouse wheel",
|
||||
"opacity": "Opacity",
|
||||
"color": "Color",
|
||||
"eraser": "Eraser",
|
||||
"White-out": "White-out",
|
||||
"index_title": "Welcome to the free online whiteboard WBO!",
|
||||
"introduction_paragraph": "WBO is a <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Free as in free speech, not free beer. This software is released under the AGPL license\">free and open-source</a> online collaborative whiteboard that allows many users to draw simultaneously on a large virtual board. The board is updated in real time for all connected users, and its state is always persisted. It can be used for many different purposes, including art, entertainment, design and teaching.",
|
||||
"share_instructions": "To collaborate on a drawing in real time with someone, just send them its URL.",
|
||||
"public_board_description": "The <b>public board</b> is accessible to everyone. It is a happily disorganized mess where you can meet with anonymous strangers and draw together. Everything there is ephemeral.",
|
||||
"open_public_board": "Go to the public board",
|
||||
"private_board_description": "You can create a <b>private board</b> with a random name, that will be accessible only by its link. Use this if you want to share private information.",
|
||||
"create_private_board": "Create a private board",
|
||||
"named_private_board_description": "You can also create a <strong>named private board</strong>, with a custom URL, that will be accessible to all those who know its name.",
|
||||
"board_name_placeholder": "Name of the board…",
|
||||
"view_source": "Source code on GitHub"
|
||||
},
|
||||
"de": {
|
||||
"hand": "Hand",
|
||||
"mover": "Verschiebung",
|
||||
"loading": "Lädt",
|
||||
"tagline": "Ein freies quelloffenes kollaboratives Zeichentool. Zeichnet eure Ideen zusammen auf WBO!",
|
||||
"configuration": "Konfiguration",
|
||||
"collaborative_whiteboard": "Kollaboratives Whiteboard",
|
||||
"size": "Größe",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Werkzeuge",
|
||||
"rectangle": "Rechteck",
|
||||
"square": "Quadrat",
|
||||
"circle": "Kreis",
|
||||
"ellipse": "Ellipse",
|
||||
"click_to_toggle": "Klicken Sie zum Umschalten",
|
||||
"menu": "Menü",
|
||||
"text": "Text",
|
||||
"straight_line": "Gerade Linie",
|
||||
"pencil": "Stift",
|
||||
"click_to_zoom": "Klicke zum reinzoomen\nHalte die Umschalttaste und klicke zum herauszoomen",
|
||||
"keyboard_shortcut": "Tastenkombination",
|
||||
"mousewheel": "Mausrad",
|
||||
"opacity": "Deckkraft",
|
||||
"color": "Farbe",
|
||||
"eraser": "Radierer",
|
||||
"white-out": "Korrekturflüssigkeit",
|
||||
"grid": "Gitter",
|
||||
"index_title": "Wilkommen bei WBO!",
|
||||
"introduction_paragraph": "WBO ist ein <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Frei im Sinne von Redefreiheit, nicht Freibier. Diese Software wird unter der AGPL Lizenz veröffentlicht.\">freies und quelloffenes</a> kollaboratives Online-Whiteboard das vielen Nutzern erlaubt gleichzeitig auf einem großen virtuellen Whiteboard zu zeichnen. Das Whiteboard wird in Echtzeit für alle Nutzer aktualisiert und sein Inhalt wird gespeichert. Es kann für verschiedenste Anwendungen genutzt werden, z.B. Kunst, Unterhaltung, Design, Unterricht und Lehre.",
|
||||
"share_instructions": "Um mit jemanden zusammen an einem Whiteboard zu arbeiten teile einfach die jeweilige URL.",
|
||||
"public_board_description": " Das <b>öffentliche Whiteboard</b> kann von jedem geöffnet werden. Es ein fröhliches unorganisiertes Chaos wo du zusammen mit anonymen Fremden malen kannst. Alles dort ist vergänglich.",
|
||||
"open_public_board": "Gehe zum öffentlichen Whiteboard",
|
||||
"private_board_description": "Du kannst ein <b>privates Whiteboard</b> mit einem zufälligen Namen erstellen, welches man nur mit seinem Link öffnen kann. Benutze dies wenn du private Informationen teilen möchtest.",
|
||||
"create_private_board": "Erstelle ein privates Whiteboard",
|
||||
"named_private_board_description": "Du kannst auch ein <strong>privates Whiteboard mit Namen</strong> mit einer benutzerdefinierten URL erstellen. Alle die den Namen kennen, können darauf zugreifen.",
|
||||
"board_name_placeholder": "Name des Whiteboards…",
|
||||
"view_source": "Quellcode auf GitHub"
|
||||
},
|
||||
"es": {
|
||||
"hand": "Mano",
|
||||
"mover": "Desplazamiento",
|
||||
"loading": "Cargando",
|
||||
"tagline": "Una herramienta de dibujo colaborativa en línea gratuita y de código abierto. Esboce nuevas ideas en la pizarra colaborativa WBO !",
|
||||
"configuration": "Configuration",
|
||||
"collaborative_whiteboard": "Pizarra colaborativa",
|
||||
"size": "Tamaño",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Herramientas",
|
||||
"rectangle": "Rectángulo",
|
||||
"square": "Cuadrado",
|
||||
"circle": "Círculo",
|
||||
"ellipse": "Elipse",
|
||||
"click_to_toggle": "haga clic para alternar",
|
||||
"menu": "Menú",
|
||||
"text": "Texto",
|
||||
"straight_line": "Línea recta",
|
||||
"pencil": "Lápiz",
|
||||
"click_to_zoom": "Haga clic para acercar, Pulse [Mayús] y haga clic para alejar",
|
||||
"keyboard_shortcut": "atajo de teclado",
|
||||
"mousewheel": "Rueda del Ratón",
|
||||
"opacity": "Opacidad",
|
||||
"color": "Color",
|
||||
"grid": "Cuadrícula",
|
||||
"eraser": "Borrador",
|
||||
"white-out": "Blanqueado",
|
||||
"index_title": "¡Bienvenido a WBO!",
|
||||
"introduction_paragraph": "WBO es una pizarra colaborativa en línea, <a href=\"https://github.com/lovasoa/whitebophir\" title=\"libre como la libertad de expresión, no libre como una cerveza gratis. Este software se lanza bajo la licencia AGPL\">libre y de Código abierto</a>, que permite a muchos usuarios dibujar simultáneamente en una gran pizarra virtual. La pizarra se actualiza en tiempo real para todos los usuarios conectados y su estado siempre es persistente. Se puede utilizar para muchos propósitos diferentes, incluyendo arte, entretenimiento, diseño y enseñanza.",
|
||||
"share_instructions": "Para colaborar en un dibujo en tiempo real con alguien, simplemente envíele la <abbr title=\"un enlace tipo: https://wbo.ophir.dev/boards/el-codigo-de-tu-pizarra\">URL</abbr> de la pizarra que ya creaste.",
|
||||
"public_board_description": "La <b>pizarra pública</b> es accesible para todos. Es un desastre felizmente desorganizado donde puedes reunirte con extraños anónimos. Todo lo que hay es efímero.",
|
||||
"open_public_board": "Ir a la pizarra pública",
|
||||
"private_board_description": "Puede crear una <b>pizarra privada</b> con un nombre aleatorio, al que solo se podrá acceder mediante su enlace. Úselo si desea compartir información privada.",
|
||||
"create_private_board": "Crea una pizarra privada",
|
||||
"named_private_board_description": "También puede crear una <strong>pizarra privada dándole un nombre aleatorio o un nombre especifico</strong>, una <abbr title=\"tipo: https://wbo.ophir.dev/boards/el-código-que-te-parezca\">URL personalizada</abbr>, que será accesible para todos aquellos que conozcan su nombre.",
|
||||
"board_name_placeholder": "Nombre de la pizarra …",
|
||||
"view_source": "Código fuente en GitHub"
|
||||
},
|
||||
"fr": {
|
||||
"collaborative_whiteboard": "Tableau blanc collaboratif",
|
||||
"loading": "Chargement",
|
||||
"menu": "Menu",
|
||||
"tools": "Outils",
|
||||
"size": "Taille",
|
||||
"color": "Couleur",
|
||||
"opacity": "Opacité",
|
||||
"pencil": "Crayon",
|
||||
"text": "Texte",
|
||||
"rectangle": "Rectangle",
|
||||
"square": "Carré",
|
||||
"circle": "Cercle",
|
||||
"ellipse": "Ellipse",
|
||||
"click_to_toggle": "cliquer pour changer",
|
||||
"eraser": "Gomme",
|
||||
"white-out": "Blanco",
|
||||
"hand": "Main",
|
||||
"mover": "Déplacer un élément",
|
||||
"straight_line": "Ligne droite",
|
||||
"grid": "Grille",
|
||||
"keyboard_shortcut": "raccourci clavier",
|
||||
"mousewheel": "molette de la souris",
|
||||
"click_to_zoom": "Cliquez pour zoomer\nCliquez en maintenant la touche majuscule enfoncée pour dézoomer",
|
||||
"tagline": "Logiciel libre pour collaborer en ligne sur un tableau blanc. Venez dessiner vos idées ensemble sur WBO !",
|
||||
"index_title": "Bienvenue sur le tableau blanc collaboratif WBO !",
|
||||
"introduction_paragraph": "WBO est un logiciel <a href=\"https://github.com/lovasoa/whitebophir\" title=\"voir le code sous license AGPL\">libre et gratuit</a> de dessin collaboratif en ligne qui permet à plusieurs utilisateurs de collaborer simultanément sur un tableau blanc. Le tableau est mis à jour en temps réel pour tous les utilisateurs connectés, et reste disponible après votre déconnexion. Il peut être utilisé notamment pour l'enseignement, l'art, le design ou juste pour s'amuser.",
|
||||
"share_instructions": "Pour collaborer sur un tableau avec quelqu'un, envoyez-lui simplement son URL.",
|
||||
"public_board_description": "Le <b>tableau anonyme</b> est accessible publiquement. C'est un joyeux bazar où vous pourrez rencontrer des étrangers anonymes, et dessiner avec eux. Tout ce que vous y inscrivez est éphémère.",
|
||||
"open_public_board": "Ouvrir le tableau anonyme",
|
||||
"private_board_description": "Vous pouvez créer un <b>tableau privé</b> dont le nom sera aléatoire. Il sera accessible uniquement à ceux avec qui vous partagerez son adresse. À utiliser lorsque vous voulez partager des informations confidentielles.",
|
||||
"create_private_board": "Créer un tableau privé",
|
||||
"named_private_board_description": "Vous pouvez aussi créer un <strong>tableau privé nommé</strong>, avec une adresse personnalisée, accessible à tous ceux qui en connaissent le nom.",
|
||||
"board_name_placeholder": "Nom du tableau…",
|
||||
"view_source": "Code source sur GitHub"
|
||||
},
|
||||
"hu": {
|
||||
"hand": "Kéz",
|
||||
"loading": "Betöltés folyamatban",
|
||||
"tagline": "Ingyenes és nyílt forráskódú online együttműködési rajzoló eszköz. Vázoljon fel új ötleteket a WBO-n!",
|
||||
"configuration": "Beállítások",
|
||||
"collaborative_whiteboard": "Együttműködési tábla",
|
||||
"size": "Méret",
|
||||
"zoom": "Nagyítás/kicsinyítés",
|
||||
"tools": "Eszközök",
|
||||
"rectangle": "Téglalap",
|
||||
"square": "Négyzet",
|
||||
"circle": "Kör",
|
||||
"ellipse": "Ellipszis",
|
||||
"click_to_toggle": "kattintson ide a be- és kikapcsolásához",
|
||||
"menu": "Menü",
|
||||
"text": "Szöveg",
|
||||
"mover": "Mozgató",
|
||||
"straight_line": "Egyenes vonal",
|
||||
"pencil": "Ceruza",
|
||||
"grid": "Rács",
|
||||
"click_to_zoom": "Kattintson ide a nagyításhoz.\nShift + kattintás a kicsinyítéshez",
|
||||
"keyboard_shortcut": "billentyűparancs",
|
||||
"mousewheel": "egérkerék",
|
||||
"opacity": "Átlátszatlanság",
|
||||
"color": "Szín",
|
||||
"eraser": "Radír",
|
||||
"White-out": "Lefedő",
|
||||
"index_title": "Isten hozta a WBO ingyenes online tábláján!",
|
||||
"introduction_paragraph": "A WBO egy <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Ingyenes, mint a szabad beszédben, nem ingyenes sör. Ez a szoftver a AGPL licenc alapján kerül kiadásra.\">ingyenes és nyílt forráskódú</a> online együttműködési tábla, amely lehetővé teszi sok felhasználó számára, hogy egyidejűleg rajzoljon egy nagy virtuális táblán. Az alaplap valós időben frissül az összes csatlakoztatott felhasználó számára és állapota állandó. Különböző célokra felhasználható, beleértve a művészetet, a szórakoztatást, a tervezést és a tanítást.",
|
||||
"share_instructions": "Ha valakivel valós időben szeretne együttműködni egy rajzon, küldje el neki az URL-jét.",
|
||||
"public_board_description": "A <b>nyilvános tábla</b> mindenki számára elérhető. Ez egy boldog szervezetlen rendetlenség, ahol találkozhat a névtelen ismeretlenek és dolgozhat együtt. Minden ott rövid távú.",
|
||||
"open_public_board": "Nyilvános tábla megnyitása",
|
||||
"private_board_description": "Készíthet egy <b>saját táblát</b> véletlenszerű névvel, amely csak a linkjével lesz elérhető. Használja ezt, ha személyes adatokat szeretne megosztani.",
|
||||
"create_private_board": "Saját tábla létrehozása",
|
||||
"named_private_board_description": "Készíthet egy <strong>saját nevű táblát</strong> is, egyéni URL-címmel, amely mindenki számára elérhető, aki ismeri a nevét.",
|
||||
"board_name_placeholder": "Tábla neve…",
|
||||
"view_source": "Forráskód a GitHub-on"
|
||||
},
|
||||
"it": {
|
||||
"hand": "Mano",
|
||||
"mover": "Spostamento",
|
||||
"loading": "Caricamento in corso",
|
||||
"tagline": "Uno strumento collaborativo per disegnare online, gratuito e open source. Disegniamo insieme nuove idee su WBO!",
|
||||
"configuration": "Configurazione",
|
||||
"collaborative_whiteboard": "Lavagna collaborativa",
|
||||
"size": "Dimensione",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Strumenti",
|
||||
"rectangle": "Rettangolo",
|
||||
"square": "Quadrato",
|
||||
"circle": "Cerchio",
|
||||
"ellipse": "Ellisse",
|
||||
"click_to_toggle": "Fai Clic per attivare",
|
||||
"menu": "Menu",
|
||||
"text": "Testo",
|
||||
"straight_line": "Linea retta",
|
||||
"pencil": "Matita",
|
||||
"click_to_zoom": "Fai clic per ingrandire \nPremi [MAIUSC] e fai clic per ridurre",
|
||||
"keyboard_shortcut": "scorciatoia da tastiera",
|
||||
"mousewheel": "rotella del mouse",
|
||||
"opacity": "Opacità",
|
||||
"color": "Colore",
|
||||
"eraser": "Gomma",
|
||||
"grid": "Griglia",
|
||||
"white-out": "Bianchetto",
|
||||
"index_title": "Benvenuti a WBO!",
|
||||
"introduction_paragraph": "WBO è una lavagna collaborativa online <a href=\"https://github.com/lovasoa/whitebophir\" title=\"gratuita come é gratuita la libertà di espressione, no come un boccale di birra gratis. Questo software è rilasciato sotto licenza AGPL\">gratuita e open source</a> che consente a molti utenti di disegnare contemporaneamente su una grande lavagna virtuale. La lavagna viene aggiornata in tempo reale per tutti gli utenti connessi e lo stato è sempre persistente. Può essere utilizzato per molti scopi diversi, tra cui arte, intrattenimento, design e insegnamento.",
|
||||
"share_instructions": "Per collaborare a un disegno in tempo reale con qualcuno, basta condividere l'<abbr title=\"un link tipo https://wbo.ophir.dev/boards/il-codice-della-tua-lavagna\">URL della lavagna</abbr>.",
|
||||
"public_board_description": "La <b>lavagna pubblica</b> è accessibile a tutti. È un disastro felicemente disorganizzato dove puoi incontrare sconosciuti anonimi e disegnare insieme. Tutto in questo spazio è effimero.",
|
||||
"open_public_board": "Vai alla lavagna pubblica",
|
||||
"private_board_description": "Puoi creare una <b>lavagna privata</b> con un nome casuale, che sarà accessibile solo dal suo URL. Usalo se vuoi condividere informazioni private.",
|
||||
"create_private_board": "Crea una lavagna privata",
|
||||
"named_private_board_description": "Puoi anche creare una <strong>lavagna privata con un nome creato da te</strong>, con un URL personalizzato, che sarà accessibile a tutti coloro che ne conoscono il nome.",
|
||||
"board_name_placeholder": "Nome della lavagna…",
|
||||
"view_source": "Codice sorgente su GitHub"
|
||||
},
|
||||
"ja": {
|
||||
"hand": "手のひらツール",
|
||||
"mover": "変位",
|
||||
"loading": "読み込み中",
|
||||
"tagline": "無料でオープンソースの協同作業できるオンラインホワイトボード。WBOでアイディアを共有しましょう!",
|
||||
"configuration": "設定",
|
||||
"collaborative_whiteboard": "協同作業できるオンラインホワイトボード",
|
||||
"size": "サイズ",
|
||||
"zoom": "拡大・縮小",
|
||||
"tools": "ツール",
|
||||
"rectangle": "矩形",
|
||||
"square": "正方形",
|
||||
"menu": "メニュー",
|
||||
"text": "テキスト",
|
||||
"straight_line": "直線",
|
||||
"pencil": "ペン",
|
||||
"circle": "サークル",
|
||||
"ellipse": "楕円",
|
||||
"click_to_toggle": "クリックして切り替えます",
|
||||
"click_to_zoom": "クリックで拡大\nシフトを押しながらクリックで縮小",
|
||||
"keyboard_shortcut": "キーボードショートカット",
|
||||
"mousewheel": "ねずみ車",
|
||||
"opacity": "透明度",
|
||||
"color": "色",
|
||||
"eraser": "消去",
|
||||
"grid": "グリッド",
|
||||
"white-out": "修正液",
|
||||
"index_title": "WBOへようこそ!",
|
||||
"introduction_paragraph": "WBOは<a href=\"https://github.com/lovasoa/whitebophir\" title=\"ビール飲み放題ではなく言論の自由。このソフトウェアはAGPLライセンスで公開しています。\">無料かつオープンソース</a>の協同作業できるオンラインホワイトボードです。多くのユーザーが大きな仮想ホワイトボードに図などを書くことができ、接続しているすべてのユーザーの更新をリアルタイムに反映され、その状態を常に保存します。これはアート、エンタテインメント、デザインや教育など、様々な用途で使用できます。",
|
||||
"share_instructions": "URLを送るだけで、リアルタイムな共同作業ができます。",
|
||||
"public_board_description": "<b>公開ボード</b>は、WBOにアクセスできる人であれば誰でも参加できますが、これは一時的な用途に向いています。",
|
||||
"open_public_board": "公開ボードを作成する",
|
||||
"private_board_description": "プライベートな情報を共有したいときは、ランダムな名前を持つ、<b>プライベートボード</b>を作成できます。このボードはリンクを知っている人がアクセスできます。",
|
||||
"create_private_board": "プライベートボードを作成する",
|
||||
"named_private_board_description": "<strong>名前つきプライベートボード</strong>を作ることもできます。このボードは名前かURLを知っている人だけがアクセスできます。",
|
||||
"board_name_placeholder": "ボードの名前",
|
||||
"view_source": "GitHubでソースコード見る"
|
||||
},
|
||||
"ru": {
|
||||
"collaborative_whiteboard": "Онлайн доска для совместного рисования",
|
||||
"loading": "Загрузка",
|
||||
"menu": "Панель",
|
||||
"tools": "Инструменты",
|
||||
"size": "Размер",
|
||||
"color": "Цвет",
|
||||
"opacity": "Непрозрачность",
|
||||
"pencil": "Карандаш",
|
||||
"text": "Текст",
|
||||
"eraser": "Ластик",
|
||||
"white-out": "Корректор",
|
||||
"hand": "Рука",
|
||||
"straight_line": "Прямая линия",
|
||||
"rectangle": "Прямоугольник",
|
||||
"square": "Квадрат",
|
||||
"circle": "Круг",
|
||||
"ellipse": "Эллипс",
|
||||
"click_to_toggle": "нажмите, чтобы переключиться",
|
||||
"zoom": "Лупа",
|
||||
"mover": "Сдвинуть объект",
|
||||
"grid": "Сетка",
|
||||
"configuration": "Настройки",
|
||||
"keyboard_shortcut": "горячая клавиша",
|
||||
"mousewheel": "колёсико мыши ",
|
||||
"tagline": "Бесплатная и открытая доска для совместной работы в интернете. Рисуете свои идеи вместе в WBO !",
|
||||
"index_title": "Добро пожаловать на WBO !",
|
||||
"introduction_paragraph": "WBO это бесплатная и <a href=\"https://github.com/lovasoa/whitebophir\" title=\"открытый исходный код\">открытая</a> виртуальная онлайн доска, позволяющая рисовать одновременно сразу нескольким пользователям. С WBO вы сможете рисовать, работать с коллегами над будущими проектами, проводить онлайн встречи, подкреплять ваши обучающие материалы и даже пробовать себя в дизайне. WBO доступен без регистрации.",
|
||||
"share_instructions": "Использовать платформу для совместного творчества очень просто. Достаточно поделиться ссылкой URL с теми, кто хочет с вами порисовать. Как только они получат URL они смогут к вам присоединиться.",
|
||||
"public_board_description": "<b>Анонимная доска</b> позволяет рисовать вместе онлайн. Используйте этот формат для творчества и кучи разных идей. Вдохновляйтесь уже существующими рисунками, дополняйте их и создавайте совместные работы с другими посетителями. Любой пользователь может удалять уже существующие элементы и рисунки.",
|
||||
"open_public_board": "Открыть анонимную доску",
|
||||
"private_board_description": "<b>Приватная доска</b> обладает тем же функционалом, что и анонимная доска. Разница в том, что приватную доску могут видеть только те пользователи, у которых на нее есть ссылка. Используйте приватную онлайн доску в рабочих целях, проводите онлайн уроки, рисуйте с детьми или друзьями. Другие пользователи не смогут удалять или менять ваши работы без вашего разрешения.",
|
||||
"create_private_board": "Создать приватную доску",
|
||||
"named_private_board_description": "Также можно создать <b>именную приватную доску</b> которая будет доступна всем тем, кому вы отправили название вашей доски.",
|
||||
"board_name_placeholder": "Название доски",
|
||||
"view_source": "Исходный код на GitHub"
|
||||
},
|
||||
"uk": {
|
||||
"hand": "Рука",
|
||||
"loading": "Завантаження",
|
||||
"tagline": "Безкоштовний онлайн засіб для спільного малювання з відкритим кодом. Разом накресліть нові ідеї у WBO!",
|
||||
"configuration": "Налаштування",
|
||||
"collaborative_whiteboard": "Онлайн дошка для спільної роботи",
|
||||
"size": "Розмір",
|
||||
"zoom": "Лупа",
|
||||
"tools": "Засоби",
|
||||
"rectangle": "Прямокутник",
|
||||
"square": "Квадрат",
|
||||
"circle": "Коло",
|
||||
"ellipse": "Еліпс",
|
||||
"click_to_toggle": "клацніть, щоб перемкнути",
|
||||
"menu": "Меню",
|
||||
"text": "Текст",
|
||||
"mover": "Пересунути",
|
||||
"straight_line": "Пряма лінія",
|
||||
"pencil": "Олівець",
|
||||
"grid": "Сітка",
|
||||
"click_to_zoom": "Клацніть для збільшення\nНатисніть shift та клацініть для зменшення",
|
||||
"keyboard_shortcut": "швидкі клавіші",
|
||||
"mousewheel": "коліщатко миші",
|
||||
"opacity": "Прозорість",
|
||||
"color": "Колір",
|
||||
"eraser": "Ґумка",
|
||||
"White-out": "Коректор",
|
||||
"index_title": "Вітаємо у відкритій онлайн дошці WBO!",
|
||||
"introduction_paragraph": "WBO це <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Free as in free speech, not free beer. This software is released under the AGPL license\">безкоштовна та відкрита</a> онлайн дошка для спільної роботи, яка дозволяє багатьом користувачам одночасно писати на великій віртуальній дошці. Дошка оновлюється в реальному часі для всіх приєднаних користувачів, та її стан постійно зберігається. Вона може застосовуватись з різною метою, включаючи мистецтво, розваги, дизайн та навчання.",
|
||||
"share_instructions": "Для спільної роботи на дошці досить повідомити іншій особі адресу URL.",
|
||||
"public_board_description": "<b>Публічна дошка</b> доступна для всіх. Там панує повний безлад, де Ви можете зустріти анонімних незнайомців та малювати разом. Тут все ефемерне.",
|
||||
"open_public_board": "Перейти до публічної дошки",
|
||||
"private_board_description": "Ви можете створити <b>особисту дошку</b> з випадковою назвою, яка буде доступна лише за відповідним посиланням. Користуйтесь нею, якшо Вам потрібно ділитись особистою інформацією.",
|
||||
"create_private_board": "Створити особисту дошку",
|
||||
"named_private_board_description": "Ви також можете створити <strong>особисту дошку з назвою</strong>, з власним URL, яка буде доступна всім, хто знає її назву.",
|
||||
"board_name_placeholder": "Назва дошки…",
|
||||
"view_source": "Вихідний код на GitHub"
|
||||
},
|
||||
"zh": {
|
||||
"collaborative_whiteboard": "在线协作式白板",
|
||||
"loading": "载入中",
|
||||
"menu": "目录",
|
||||
"tools": "工具",
|
||||
"size": "尺寸",
|
||||
"color": "颜色",
|
||||
"opacity": "不透明度",
|
||||
"pencil": "铅笔",
|
||||
"rectangle": "矩形",
|
||||
"square": "正方形",
|
||||
"circle": "圈",
|
||||
"ellipse": "椭圆",
|
||||
"click_to_toggle": "单击以切换",
|
||||
"zoom": "放大",
|
||||
"text": "文本",
|
||||
"eraser": "橡皮",
|
||||
"white-out": "修正液",
|
||||
"hand": "移动",
|
||||
"mover": "平移",
|
||||
"straight_line": "直线",
|
||||
"configuration": "刷设置",
|
||||
"keyboard_shortcut": "键盘快捷键",
|
||||
"mousewheel": "鼠标轮",
|
||||
"grid": "格",
|
||||
"click_to_zoom": "点击放大。\n保持班次并单击缩小。",
|
||||
"tagline": "打开即用的免费在线白板工具",
|
||||
"index_title": "欢迎来到 WBO!",
|
||||
"introduction_paragraph": " WBO是一<a href=\"https://github.com/lovasoa/whitebophir\">个免费的</a>、开源的在线协作白板,它允许许多用户同时在一个大型虚拟板上画图。该板对所有连接的用户实时更新,并且始终可用。它可以用于许多不同的目的,包括艺术、娱乐、设计和教学。",
|
||||
"share_instructions": "要与某人实时协作绘制图形,只需向他们发送白板的URL。",
|
||||
"public_board_description": "每个人都可以使用公共白板。这是一个令人愉快的混乱的地方,你可以会见匿名陌生人,并在一起。那里的一切都是短暂的。",
|
||||
"open_public_board": "进入公共白板",
|
||||
"private_board_description": "您可以创建一个带有随机名称的私有白板,该白板只能通过其链接访问。如果要共享私人信息,请使用此选项。",
|
||||
"create_private_board": "创建私人白板",
|
||||
"named_private_board_description": "您还可以创建一个命名的私有白板,它有一个自定义的URL,所有知道它名字的人都可以访问它。",
|
||||
"board_name_placeholder": "白板名称",
|
||||
"view_source": "GitHub上的源代码"
|
||||
},
|
||||
"vn": {
|
||||
"hand": "Tay",
|
||||
"loading": "Đang tải",
|
||||
"tagline": "Một công cụ vẽ cộng tác trực tuyến miễn phí và mã nguồn mở. Cùng nhau phác thảo những ý tưởng mới trên WBO!",
|
||||
"configuration": "Cấu hình",
|
||||
"collaborative_whiteboard": "Bảng trắng cộng tác",
|
||||
"size": "Size",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Công cụ",
|
||||
"rectangle": "Chữ nhật",
|
||||
"square": "Vuông",
|
||||
"circle": "Tròn",
|
||||
"ellipse": "Hình elip",
|
||||
"click_to_toggle": "Bật/tắt",
|
||||
"menu": "Menu",
|
||||
"text": "Text",
|
||||
"mover": "Di chuyển",
|
||||
"straight_line": "Đường thẳng",
|
||||
"pencil": "Gạch ngang",
|
||||
"grid": "Grid",
|
||||
"click_to_zoom": "Nhấp để phóng to \n Nhấn shift và nhấp để thu nhỏ",
|
||||
"keyboard_shortcut": "Phím tắt",
|
||||
"mousewheel": "Lăn chuột",
|
||||
"opacity": "Độ Mờ",
|
||||
"color": "Màu",
|
||||
"eraser": "Tẩy",
|
||||
"White-out": "Trắng",
|
||||
"index_title": "Chào mừng bạn đến với WBO bảng trắng trực tuyến miễn phí!",
|
||||
"introduction_paragraph": "WBO là một <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Miễn phí như trong tự do ngôn luận, không phải bia miễn phí. Phần mềm này được phát hành theo giấy phép AGPL\">Miễn phí và open-source</a> cho phép nhiều người dùng vẽ đồng thời trên một bảng ảo lớn. Bảng này được cập nhật theo thời gian thực cho tất cả người dùng được kết nối và trạng thái luôn tồn tại. Nó có thể được sử dụng cho nhiều mục đích khác nhau, bao gồm nghệ thuật, giải trí, thiết kế và giảng dạy.",
|
||||
"share_instructions": "Để cộng tác trên một bản vẽ trong thời gian thực với ai đó, chỉ cần gửi cho họ URL của bản vẽ đó.",
|
||||
"public_board_description": "Tất cả mọi người đều có thể truy cập <b> bảng công khai </b>. Đó là một mớ hỗn độn vô tổ chức vui vẻ, nơi bạn có thể gặp gỡ những người lạ vô danh và cùng nhau vẽ. Mọi thứ ở đó là phù du.",
|
||||
"open_public_board": "Đi tới bảng công khai",
|
||||
"private_board_description": "Bạn có thể tạo một <b> bảng riêng </b> với một tên ngẫu nhiên, chỉ có thể truy cập được bằng liên kết của nó. Sử dụng cái này nếu bạn muốn chia sẻ thông tin cá nhân.",
|
||||
"create_private_board": "Tạo một bảng riêng",
|
||||
"named_private_board_description": "Bạn cũng có thể tạo <strong> bảng riêng được đặt tên </strong>, với URL tùy chỉnh, tất cả những người biết tên của nó đều có thể truy cập được.",
|
||||
"board_name_placeholder": "Tên bản …",
|
||||
"view_source": "Source code on GitHub"
|
||||
}
|
||||
"en": {
|
||||
"hand": "Hand",
|
||||
"loading": "Loading",
|
||||
"tagline": "A free and open-source online collaborative drawing tool. Sketch new ideas together on WBO!",
|
||||
"configuration": "Configuration",
|
||||
"collaborative_whiteboard": "Collaborative whiteboard",
|
||||
"size": "Size",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Tools",
|
||||
"rectangle": "Rectangle",
|
||||
"square": "Square",
|
||||
"circle": "Circle",
|
||||
"ellipse": "Ellipse",
|
||||
"click_to_toggle": "click to toggle",
|
||||
"menu": "Menu",
|
||||
"text": "Text",
|
||||
"mover": "Mover",
|
||||
"straight_line": "Straight line",
|
||||
"pencil": "Pencil",
|
||||
"grid": "Grid",
|
||||
"click_to_zoom": "Click to zoom in\nPress shift and click to zoom out",
|
||||
"keyboard_shortcut": "keyboard shortcut",
|
||||
"mousewheel": "mouse wheel",
|
||||
"opacity": "Opacity",
|
||||
"color": "Color",
|
||||
"eraser": "Eraser",
|
||||
"White-out": "White-out",
|
||||
"index_title": "Welcome to the free online whiteboard WBO!",
|
||||
"introduction_paragraph": "WBO is a <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Free as in free speech, not free beer. This software is released under the AGPL license\">free and open-source</a> online collaborative whiteboard that allows many users to draw simultaneously on a large virtual board. The board is updated in real time for all connected users, and its state is always persisted. It can be used for many different purposes, including art, entertainment, design and teaching.",
|
||||
"share_instructions": "To collaborate on a drawing in real time with someone, just send them its URL.",
|
||||
"public_board_description": "The <b>public board</b> is accessible to everyone. It is a happily disorganized mess where you can meet with anonymous strangers and draw together. Everything there is ephemeral.",
|
||||
"open_public_board": "Go to the public board",
|
||||
"private_board_description": "You can create a <b>private board</b> with a random name, that will be accessible only by its link. Use this if you want to share private information.",
|
||||
"create_private_board": "Create a private board",
|
||||
"named_private_board_description": "You can also create a <strong>named private board</strong>, with a custom URL, that will be accessible to all those who know its name.",
|
||||
"board_name_placeholder": "Name of the board…",
|
||||
"view_source": "Source code on GitHub"
|
||||
},
|
||||
"de": {
|
||||
"hand": "Hand",
|
||||
"mover": "Verschiebung",
|
||||
"loading": "Lädt",
|
||||
"tagline": "Ein freies quelloffenes kollaboratives Zeichentool. Zeichnet eure Ideen zusammen auf WBO!",
|
||||
"configuration": "Konfiguration",
|
||||
"collaborative_whiteboard": "Kollaboratives Whiteboard",
|
||||
"size": "Größe",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Werkzeuge",
|
||||
"rectangle": "Rechteck",
|
||||
"square": "Quadrat",
|
||||
"circle": "Kreis",
|
||||
"ellipse": "Ellipse",
|
||||
"click_to_toggle": "Klicken Sie zum Umschalten",
|
||||
"menu": "Menü",
|
||||
"text": "Text",
|
||||
"straight_line": "Gerade Linie",
|
||||
"pencil": "Stift",
|
||||
"click_to_zoom": "Klicke zum reinzoomen\nHalte die Umschalttaste und klicke zum herauszoomen",
|
||||
"keyboard_shortcut": "Tastenkombination",
|
||||
"mousewheel": "Mausrad",
|
||||
"opacity": "Deckkraft",
|
||||
"color": "Farbe",
|
||||
"eraser": "Radierer",
|
||||
"white-out": "Korrekturflüssigkeit",
|
||||
"grid": "Gitter",
|
||||
"index_title": "Wilkommen bei WBO!",
|
||||
"introduction_paragraph": "WBO ist ein <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Frei im Sinne von Redefreiheit, nicht Freibier. Diese Software wird unter der AGPL Lizenz veröffentlicht.\">freies und quelloffenes</a> kollaboratives Online-Whiteboard das vielen Nutzern erlaubt gleichzeitig auf einem großen virtuellen Whiteboard zu zeichnen. Das Whiteboard wird in Echtzeit für alle Nutzer aktualisiert und sein Inhalt wird gespeichert. Es kann für verschiedenste Anwendungen genutzt werden, z.B. Kunst, Unterhaltung, Design, Unterricht und Lehre.",
|
||||
"share_instructions": "Um mit jemanden zusammen an einem Whiteboard zu arbeiten teile einfach die jeweilige URL.",
|
||||
"public_board_description": " Das <b>öffentliche Whiteboard</b> kann von jedem geöffnet werden. Es ein fröhliches unorganisiertes Chaos wo du zusammen mit anonymen Fremden malen kannst. Alles dort ist vergänglich.",
|
||||
"open_public_board": "Gehe zum öffentlichen Whiteboard",
|
||||
"private_board_description": "Du kannst ein <b>privates Whiteboard</b> mit einem zufälligen Namen erstellen, welches man nur mit seinem Link öffnen kann. Benutze dies wenn du private Informationen teilen möchtest.",
|
||||
"create_private_board": "Erstelle ein privates Whiteboard",
|
||||
"named_private_board_description": "Du kannst auch ein <strong>privates Whiteboard mit Namen</strong> mit einer benutzerdefinierten URL erstellen. Alle die den Namen kennen, können darauf zugreifen.",
|
||||
"board_name_placeholder": "Name des Whiteboards…",
|
||||
"view_source": "Quellcode auf GitHub"
|
||||
},
|
||||
"es": {
|
||||
"hand": "Mano",
|
||||
"mover": "Desplazamiento",
|
||||
"loading": "Cargando",
|
||||
"tagline": "Una herramienta de dibujo colaborativa en línea gratuita y de código abierto. Esboce nuevas ideas en la pizarra colaborativa WBO !",
|
||||
"configuration": "Configuration",
|
||||
"collaborative_whiteboard": "Pizarra colaborativa",
|
||||
"size": "Tamaño",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Herramientas",
|
||||
"rectangle": "Rectángulo",
|
||||
"square": "Cuadrado",
|
||||
"circle": "Círculo",
|
||||
"ellipse": "Elipse",
|
||||
"click_to_toggle": "haga clic para alternar",
|
||||
"menu": "Menú",
|
||||
"text": "Texto",
|
||||
"straight_line": "Línea recta",
|
||||
"pencil": "Lápiz",
|
||||
"click_to_zoom": "Haga clic para acercar, Pulse [Mayús] y haga clic para alejar",
|
||||
"keyboard_shortcut": "atajo de teclado",
|
||||
"mousewheel": "Rueda del Ratón",
|
||||
"opacity": "Opacidad",
|
||||
"color": "Color",
|
||||
"grid": "Cuadrícula",
|
||||
"eraser": "Borrador",
|
||||
"white-out": "Blanqueado",
|
||||
"index_title": "¡Bienvenido a WBO!",
|
||||
"introduction_paragraph": "WBO es una pizarra colaborativa en línea, <a href=\"https://github.com/lovasoa/whitebophir\" title=\"libre como la libertad de expresión, no libre como una cerveza gratis. Este software se lanza bajo la licencia AGPL\">libre y de Código abierto</a>, que permite a muchos usuarios dibujar simultáneamente en una gran pizarra virtual. La pizarra se actualiza en tiempo real para todos los usuarios conectados y su estado siempre es persistente. Se puede utilizar para muchos propósitos diferentes, incluyendo arte, entretenimiento, diseño y enseñanza.",
|
||||
"share_instructions": "Para colaborar en un dibujo en tiempo real con alguien, simplemente envíele la <abbr title=\"un enlace tipo: https://wbo.ophir.dev/boards/el-codigo-de-tu-pizarra\">URL</abbr> de la pizarra que ya creaste.",
|
||||
"public_board_description": "La <b>pizarra pública</b> es accesible para todos. Es un desastre felizmente desorganizado donde puedes reunirte con extraños anónimos. Todo lo que hay es efímero.",
|
||||
"open_public_board": "Ir a la pizarra pública",
|
||||
"private_board_description": "Puede crear una <b>pizarra privada</b> con un nombre aleatorio, al que solo se podrá acceder mediante su enlace. Úselo si desea compartir información privada.",
|
||||
"create_private_board": "Crea una pizarra privada",
|
||||
"named_private_board_description": "También puede crear una <strong>pizarra privada dándole un nombre aleatorio o un nombre especifico</strong>, una <abbr title=\"tipo: https://wbo.ophir.dev/boards/el-código-que-te-parezca\">URL personalizada</abbr>, que será accesible para todos aquellos que conozcan su nombre.",
|
||||
"board_name_placeholder": "Nombre de la pizarra …",
|
||||
"view_source": "Código fuente en GitHub"
|
||||
},
|
||||
"fr": {
|
||||
"collaborative_whiteboard": "Tableau blanc collaboratif",
|
||||
"loading": "Chargement",
|
||||
"menu": "Menu",
|
||||
"tools": "Outils",
|
||||
"size": "Taille",
|
||||
"color": "Couleur",
|
||||
"opacity": "Opacité",
|
||||
"pencil": "Crayon",
|
||||
"text": "Texte",
|
||||
"rectangle": "Rectangle",
|
||||
"square": "Carré",
|
||||
"circle": "Cercle",
|
||||
"ellipse": "Ellipse",
|
||||
"click_to_toggle": "cliquer pour changer",
|
||||
"eraser": "Gomme",
|
||||
"white-out": "Blanco",
|
||||
"hand": "Main",
|
||||
"mover": "Déplacer un élément",
|
||||
"straight_line": "Ligne droite",
|
||||
"grid": "Grille",
|
||||
"keyboard_shortcut": "raccourci clavier",
|
||||
"mousewheel": "molette de la souris",
|
||||
"click_to_zoom": "Cliquez pour zoomer\nCliquez en maintenant la touche majuscule enfoncée pour dézoomer",
|
||||
"tagline": "Logiciel libre pour collaborer en ligne sur un tableau blanc. Venez dessiner vos idées ensemble sur WBO !",
|
||||
"index_title": "Bienvenue sur le tableau blanc collaboratif WBO !",
|
||||
"introduction_paragraph": "WBO est un logiciel <a href=\"https://github.com/lovasoa/whitebophir\" title=\"voir le code sous license AGPL\">libre et gratuit</a> de dessin collaboratif en ligne qui permet à plusieurs utilisateurs de collaborer simultanément sur un tableau blanc. Le tableau est mis à jour en temps réel pour tous les utilisateurs connectés, et reste disponible après votre déconnexion. Il peut être utilisé notamment pour l'enseignement, l'art, le design ou juste pour s'amuser.",
|
||||
"share_instructions": "Pour collaborer sur un tableau avec quelqu'un, envoyez-lui simplement son URL.",
|
||||
"public_board_description": "Le <b>tableau anonyme</b> est accessible publiquement. C'est un joyeux bazar où vous pourrez rencontrer des étrangers anonymes, et dessiner avec eux. Tout ce que vous y inscrivez est éphémère.",
|
||||
"open_public_board": "Ouvrir le tableau anonyme",
|
||||
"private_board_description": "Vous pouvez créer un <b>tableau privé</b> dont le nom sera aléatoire. Il sera accessible uniquement à ceux avec qui vous partagerez son adresse. À utiliser lorsque vous voulez partager des informations confidentielles.",
|
||||
"create_private_board": "Créer un tableau privé",
|
||||
"named_private_board_description": "Vous pouvez aussi créer un <strong>tableau privé nommé</strong>, avec une adresse personnalisée, accessible à tous ceux qui en connaissent le nom.",
|
||||
"board_name_placeholder": "Nom du tableau…",
|
||||
"view_source": "Code source sur GitHub"
|
||||
},
|
||||
"hu": {
|
||||
"hand": "Kéz",
|
||||
"loading": "Betöltés folyamatban",
|
||||
"tagline": "Ingyenes és nyílt forráskódú online együttműködési rajzoló eszköz. Vázoljon fel új ötleteket a WBO-n!",
|
||||
"configuration": "Beállítások",
|
||||
"collaborative_whiteboard": "Együttműködési tábla",
|
||||
"size": "Méret",
|
||||
"zoom": "Nagyítás/kicsinyítés",
|
||||
"tools": "Eszközök",
|
||||
"rectangle": "Téglalap",
|
||||
"square": "Négyzet",
|
||||
"circle": "Kör",
|
||||
"ellipse": "Ellipszis",
|
||||
"click_to_toggle": "kattintson ide a be- és kikapcsolásához",
|
||||
"menu": "Menü",
|
||||
"text": "Szöveg",
|
||||
"mover": "Mozgató",
|
||||
"straight_line": "Egyenes vonal",
|
||||
"pencil": "Ceruza",
|
||||
"grid": "Rács",
|
||||
"click_to_zoom": "Kattintson ide a nagyításhoz.\nShift + kattintás a kicsinyítéshez",
|
||||
"keyboard_shortcut": "billentyűparancs",
|
||||
"mousewheel": "egérkerék",
|
||||
"opacity": "Átlátszatlanság",
|
||||
"color": "Szín",
|
||||
"eraser": "Radír",
|
||||
"White-out": "Lefedő",
|
||||
"index_title": "Isten hozta a WBO ingyenes online tábláján!",
|
||||
"introduction_paragraph": "A WBO egy <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Ingyenes, mint a szabad beszédben, nem ingyenes sör. Ez a szoftver a AGPL licenc alapján kerül kiadásra.\">ingyenes és nyílt forráskódú</a> online együttműködési tábla, amely lehetővé teszi sok felhasználó számára, hogy egyidejűleg rajzoljon egy nagy virtuális táblán. Az alaplap valós időben frissül az összes csatlakoztatott felhasználó számára és állapota állandó. Különböző célokra felhasználható, beleértve a művészetet, a szórakoztatást, a tervezést és a tanítást.",
|
||||
"share_instructions": "Ha valakivel valós időben szeretne együttműködni egy rajzon, küldje el neki az URL-jét.",
|
||||
"public_board_description": "A <b>nyilvános tábla</b> mindenki számára elérhető. Ez egy boldog szervezetlen rendetlenség, ahol találkozhat a névtelen ismeretlenek és dolgozhat együtt. Minden ott rövid távú.",
|
||||
"open_public_board": "Nyilvános tábla megnyitása",
|
||||
"private_board_description": "Készíthet egy <b>saját táblát</b> véletlenszerű névvel, amely csak a linkjével lesz elérhető. Használja ezt, ha személyes adatokat szeretne megosztani.",
|
||||
"create_private_board": "Saját tábla létrehozása",
|
||||
"named_private_board_description": "Készíthet egy <strong>saját nevű táblát</strong> is, egyéni URL-címmel, amely mindenki számára elérhető, aki ismeri a nevét.",
|
||||
"board_name_placeholder": "Tábla neve…",
|
||||
"view_source": "Forráskód a GitHub-on"
|
||||
},
|
||||
"it": {
|
||||
"hand": "Mano",
|
||||
"mover": "Spostamento",
|
||||
"loading": "Caricamento in corso",
|
||||
"tagline": "Uno strumento collaborativo per disegnare online, gratuito e open source. Disegniamo insieme nuove idee su WBO!",
|
||||
"configuration": "Configurazione",
|
||||
"collaborative_whiteboard": "Lavagna collaborativa",
|
||||
"size": "Dimensione",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Strumenti",
|
||||
"rectangle": "Rettangolo",
|
||||
"square": "Quadrato",
|
||||
"circle": "Cerchio",
|
||||
"ellipse": "Ellisse",
|
||||
"click_to_toggle": "Fai Clic per attivare",
|
||||
"menu": "Menu",
|
||||
"text": "Testo",
|
||||
"straight_line": "Linea retta",
|
||||
"pencil": "Matita",
|
||||
"click_to_zoom": "Fai clic per ingrandire \nPremi [MAIUSC] e fai clic per ridurre",
|
||||
"keyboard_shortcut": "scorciatoia da tastiera",
|
||||
"mousewheel": "rotella del mouse",
|
||||
"opacity": "Opacità",
|
||||
"color": "Colore",
|
||||
"eraser": "Gomma",
|
||||
"grid": "Griglia",
|
||||
"white-out": "Bianchetto",
|
||||
"index_title": "Benvenuti a WBO!",
|
||||
"introduction_paragraph": "WBO è una lavagna collaborativa online <a href=\"https://github.com/lovasoa/whitebophir\" title=\"gratuita come é gratuita la libertà di espressione, no come un boccale di birra gratis. Questo software è rilasciato sotto licenza AGPL\">gratuita e open source</a> che consente a molti utenti di disegnare contemporaneamente su una grande lavagna virtuale. La lavagna viene aggiornata in tempo reale per tutti gli utenti connessi e lo stato è sempre persistente. Può essere utilizzato per molti scopi diversi, tra cui arte, intrattenimento, design e insegnamento.",
|
||||
"share_instructions": "Per collaborare a un disegno in tempo reale con qualcuno, basta condividere l'<abbr title=\"un link tipo https://wbo.ophir.dev/boards/il-codice-della-tua-lavagna\">URL della lavagna</abbr>.",
|
||||
"public_board_description": "La <b>lavagna pubblica</b> è accessibile a tutti. È un disastro felicemente disorganizzato dove puoi incontrare sconosciuti anonimi e disegnare insieme. Tutto in questo spazio è effimero.",
|
||||
"open_public_board": "Vai alla lavagna pubblica",
|
||||
"private_board_description": "Puoi creare una <b>lavagna privata</b> con un nome casuale, che sarà accessibile solo dal suo URL. Usalo se vuoi condividere informazioni private.",
|
||||
"create_private_board": "Crea una lavagna privata",
|
||||
"named_private_board_description": "Puoi anche creare una <strong>lavagna privata con un nome creato da te</strong>, con un URL personalizzato, che sarà accessibile a tutti coloro che ne conoscono il nome.",
|
||||
"board_name_placeholder": "Nome della lavagna…",
|
||||
"view_source": "Codice sorgente su GitHub"
|
||||
},
|
||||
"ja": {
|
||||
"hand": "手のひらツール",
|
||||
"mover": "変位",
|
||||
"loading": "読み込み中",
|
||||
"tagline": "無料でオープンソースの協同作業できるオンラインホワイトボード。WBOでアイディアを共有しましょう!",
|
||||
"configuration": "設定",
|
||||
"collaborative_whiteboard": "協同作業できるオンラインホワイトボード",
|
||||
"size": "サイズ",
|
||||
"zoom": "拡大・縮小",
|
||||
"tools": "ツール",
|
||||
"rectangle": "矩形",
|
||||
"square": "正方形",
|
||||
"menu": "メニュー",
|
||||
"text": "テキスト",
|
||||
"straight_line": "直線",
|
||||
"pencil": "ペン",
|
||||
"circle": "サークル",
|
||||
"ellipse": "楕円",
|
||||
"click_to_toggle": "クリックして切り替えます",
|
||||
"click_to_zoom": "クリックで拡大\nシフトを押しながらクリックで縮小",
|
||||
"keyboard_shortcut": "キーボードショートカット",
|
||||
"mousewheel": "ねずみ車",
|
||||
"opacity": "透明度",
|
||||
"color": "色",
|
||||
"eraser": "消去",
|
||||
"grid": "グリッド",
|
||||
"white-out": "修正液",
|
||||
"index_title": "WBOへようこそ!",
|
||||
"introduction_paragraph": "WBOは<a href=\"https://github.com/lovasoa/whitebophir\" title=\"ビール飲み放題ではなく言論の自由。このソフトウェアはAGPLライセンスで公開しています。\">無料かつオープンソース</a>の協同作業できるオンラインホワイトボードです。多くのユーザーが大きな仮想ホワイトボードに図などを書くことができ、接続しているすべてのユーザーの更新をリアルタイムに反映され、その状態を常に保存します。これはアート、エンタテインメント、デザインや教育など、様々な用途で使用できます。",
|
||||
"share_instructions": "URLを送るだけで、リアルタイムな共同作業ができます。",
|
||||
"public_board_description": "<b>公開ボード</b>は、WBOにアクセスできる人であれば誰でも参加できますが、これは一時的な用途に向いています。",
|
||||
"open_public_board": "公開ボードを作成する",
|
||||
"private_board_description": "プライベートな情報を共有したいときは、ランダムな名前を持つ、<b>プライベートボード</b>を作成できます。このボードはリンクを知っている人がアクセスできます。",
|
||||
"create_private_board": "プライベートボードを作成する",
|
||||
"named_private_board_description": "<strong>名前つきプライベートボード</strong>を作ることもできます。このボードは名前かURLを知っている人だけがアクセスできます。",
|
||||
"board_name_placeholder": "ボードの名前",
|
||||
"view_source": "GitHubでソースコード見る"
|
||||
},
|
||||
"ru": {
|
||||
"collaborative_whiteboard": "Онлайн доска для совместного рисования",
|
||||
"loading": "Загрузка",
|
||||
"menu": "Панель",
|
||||
"tools": "Инструменты",
|
||||
"size": "Размер",
|
||||
"color": "Цвет",
|
||||
"opacity": "Непрозрачность",
|
||||
"pencil": "Карандаш",
|
||||
"text": "Текст",
|
||||
"eraser": "Ластик",
|
||||
"white-out": "Корректор",
|
||||
"hand": "Рука",
|
||||
"straight_line": "Прямая линия",
|
||||
"rectangle": "Прямоугольник",
|
||||
"square": "Квадрат",
|
||||
"circle": "Круг",
|
||||
"ellipse": "Эллипс",
|
||||
"click_to_toggle": "нажмите, чтобы переключиться",
|
||||
"zoom": "Лупа",
|
||||
"mover": "Сдвинуть объект",
|
||||
"grid": "Сетка",
|
||||
"configuration": "Настройки",
|
||||
"keyboard_shortcut": "горячая клавиша",
|
||||
"mousewheel": "колёсико мыши ",
|
||||
"tagline": "Бесплатная и открытая доска для совместной работы в интернете. Рисуете свои идеи вместе в WBO !",
|
||||
"index_title": "Добро пожаловать на WBO !",
|
||||
"introduction_paragraph": "WBO это бесплатная и <a href=\"https://github.com/lovasoa/whitebophir\" title=\"открытый исходный код\">открытая</a> виртуальная онлайн доска, позволяющая рисовать одновременно сразу нескольким пользователям. С WBO вы сможете рисовать, работать с коллегами над будущими проектами, проводить онлайн встречи, подкреплять ваши обучающие материалы и даже пробовать себя в дизайне. WBO доступен без регистрации.",
|
||||
"share_instructions": "Использовать платформу для совместного творчества очень просто. Достаточно поделиться ссылкой URL с теми, кто хочет с вами порисовать. Как только они получат URL они смогут к вам присоединиться.",
|
||||
"public_board_description": "<b>Анонимная доска</b> позволяет рисовать вместе онлайн. Используйте этот формат для творчества и кучи разных идей. Вдохновляйтесь уже существующими рисунками, дополняйте их и создавайте совместные работы с другими посетителями. Любой пользователь может удалять уже существующие элементы и рисунки.",
|
||||
"open_public_board": "Открыть анонимную доску",
|
||||
"private_board_description": "<b>Приватная доска</b> обладает тем же функционалом, что и анонимная доска. Разница в том, что приватную доску могут видеть только те пользователи, у которых на нее есть ссылка. Используйте приватную онлайн доску в рабочих целях, проводите онлайн уроки, рисуйте с детьми или друзьями. Другие пользователи не смогут удалять или менять ваши работы без вашего разрешения.",
|
||||
"create_private_board": "Создать приватную доску",
|
||||
"named_private_board_description": "Также можно создать <b>именную приватную доску</b> которая будет доступна всем тем, кому вы отправили название вашей доски.",
|
||||
"board_name_placeholder": "Название доски",
|
||||
"view_source": "Исходный код на GitHub"
|
||||
},
|
||||
"uk": {
|
||||
"hand": "Рука",
|
||||
"loading": "Завантаження",
|
||||
"tagline": "Безкоштовний онлайн засіб для спільного малювання з відкритим кодом. Разом накресліть нові ідеї у WBO!",
|
||||
"configuration": "Налаштування",
|
||||
"collaborative_whiteboard": "Онлайн дошка для спільної роботи",
|
||||
"size": "Розмір",
|
||||
"zoom": "Лупа",
|
||||
"tools": "Засоби",
|
||||
"rectangle": "Прямокутник",
|
||||
"square": "Квадрат",
|
||||
"circle": "Коло",
|
||||
"ellipse": "Еліпс",
|
||||
"click_to_toggle": "клацніть, щоб перемкнути",
|
||||
"menu": "Меню",
|
||||
"text": "Текст",
|
||||
"mover": "Пересунути",
|
||||
"straight_line": "Пряма лінія",
|
||||
"pencil": "Олівець",
|
||||
"grid": "Сітка",
|
||||
"click_to_zoom": "Клацніть для збільшення\nНатисніть shift та клацініть для зменшення",
|
||||
"keyboard_shortcut": "швидкі клавіші",
|
||||
"mousewheel": "коліщатко миші",
|
||||
"opacity": "Прозорість",
|
||||
"color": "Колір",
|
||||
"eraser": "Ґумка",
|
||||
"White-out": "Коректор",
|
||||
"index_title": "Вітаємо у відкритій онлайн дошці WBO!",
|
||||
"introduction_paragraph": "WBO це <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Free as in free speech, not free beer. This software is released under the AGPL license\">безкоштовна та відкрита</a> онлайн дошка для спільної роботи, яка дозволяє багатьом користувачам одночасно писати на великій віртуальній дошці. Дошка оновлюється в реальному часі для всіх приєднаних користувачів, та її стан постійно зберігається. Вона може застосовуватись з різною метою, включаючи мистецтво, розваги, дизайн та навчання.",
|
||||
"share_instructions": "Для спільної роботи на дошці досить повідомити іншій особі адресу URL.",
|
||||
"public_board_description": "<b>Публічна дошка</b> доступна для всіх. Там панує повний безлад, де Ви можете зустріти анонімних незнайомців та малювати разом. Тут все ефемерне.",
|
||||
"open_public_board": "Перейти до публічної дошки",
|
||||
"private_board_description": "Ви можете створити <b>особисту дошку</b> з випадковою назвою, яка буде доступна лише за відповідним посиланням. Користуйтесь нею, якшо Вам потрібно ділитись особистою інформацією.",
|
||||
"create_private_board": "Створити особисту дошку",
|
||||
"named_private_board_description": "Ви також можете створити <strong>особисту дошку з назвою</strong>, з власним URL, яка буде доступна всім, хто знає її назву.",
|
||||
"board_name_placeholder": "Назва дошки…",
|
||||
"view_source": "Вихідний код на GitHub"
|
||||
},
|
||||
"zh": {
|
||||
"collaborative_whiteboard": "在线协作式白板",
|
||||
"loading": "载入中",
|
||||
"menu": "目录",
|
||||
"tools": "工具",
|
||||
"size": "尺寸",
|
||||
"color": "颜色",
|
||||
"opacity": "不透明度",
|
||||
"pencil": "铅笔",
|
||||
"rectangle": "矩形",
|
||||
"square": "正方形",
|
||||
"circle": "圈",
|
||||
"ellipse": "椭圆",
|
||||
"click_to_toggle": "单击以切换",
|
||||
"zoom": "放大",
|
||||
"text": "文本",
|
||||
"eraser": "橡皮",
|
||||
"white-out": "修正液",
|
||||
"hand": "移动",
|
||||
"mover": "平移",
|
||||
"straight_line": "直线",
|
||||
"configuration": "刷设置",
|
||||
"keyboard_shortcut": "键盘快捷键",
|
||||
"mousewheel": "鼠标轮",
|
||||
"grid": "格",
|
||||
"click_to_zoom": "点击放大。\n保持班次并单击缩小。",
|
||||
"tagline": "打开即用的免费在线白板工具",
|
||||
"index_title": "欢迎来到 WBO!",
|
||||
"introduction_paragraph": " WBO是一<a href=\"https://github.com/lovasoa/whitebophir\">个免费的</a>、开源的在线协作白板,它允许许多用户同时在一个大型虚拟板上画图。该板对所有连接的用户实时更新,并且始终可用。它可以用于许多不同的目的,包括艺术、娱乐、设计和教学。",
|
||||
"share_instructions": "要与某人实时协作绘制图形,只需向他们发送白板的URL。",
|
||||
"public_board_description": "每个人都可以使用公共白板。这是一个令人愉快的混乱的地方,你可以会见匿名陌生人,并在一起。那里的一切都是短暂的。",
|
||||
"open_public_board": "进入公共白板",
|
||||
"private_board_description": "您可以创建一个带有随机名称的私有白板,该白板只能通过其链接访问。如果要共享私人信息,请使用此选项。",
|
||||
"create_private_board": "创建私人白板",
|
||||
"named_private_board_description": "您还可以创建一个命名的私有白板,它有一个自定义的URL,所有知道它名字的人都可以访问它。",
|
||||
"board_name_placeholder": "白板名称",
|
||||
"view_source": "GitHub上的源代码"
|
||||
},
|
||||
"vn": {
|
||||
"hand": "Tay",
|
||||
"loading": "Đang tải",
|
||||
"tagline": "Một công cụ vẽ cộng tác trực tuyến miễn phí và mã nguồn mở. Cùng nhau phác thảo những ý tưởng mới trên WBO!",
|
||||
"configuration": "Cấu hình",
|
||||
"collaborative_whiteboard": "Bảng trắng cộng tác",
|
||||
"size": "Size",
|
||||
"zoom": "Zoom",
|
||||
"tools": "Công cụ",
|
||||
"rectangle": "Chữ nhật",
|
||||
"square": "Vuông",
|
||||
"circle": "Tròn",
|
||||
"ellipse": "Hình elip",
|
||||
"click_to_toggle": "Bật/tắt",
|
||||
"menu": "Menu",
|
||||
"text": "Text",
|
||||
"mover": "Di chuyển",
|
||||
"straight_line": "Đường thẳng",
|
||||
"pencil": "Gạch ngang",
|
||||
"grid": "Grid",
|
||||
"click_to_zoom": "Nhấp để phóng to \n Nhấn shift và nhấp để thu nhỏ",
|
||||
"keyboard_shortcut": "Phím tắt",
|
||||
"mousewheel": "Lăn chuột",
|
||||
"opacity": "Độ Mờ",
|
||||
"color": "Màu",
|
||||
"eraser": "Tẩy",
|
||||
"White-out": "Trắng",
|
||||
"index_title": "Chào mừng bạn đến với WBO bảng trắng trực tuyến miễn phí!",
|
||||
"introduction_paragraph": "WBO là một <a href=\"https://github.com/lovasoa/whitebophir\" title=\"Miễn phí như trong tự do ngôn luận, không phải bia miễn phí. Phần mềm này được phát hành theo giấy phép AGPL\">Miễn phí và open-source</a> cho phép nhiều người dùng vẽ đồng thời trên một bảng ảo lớn. Bảng này được cập nhật theo thời gian thực cho tất cả người dùng được kết nối và trạng thái luôn tồn tại. Nó có thể được sử dụng cho nhiều mục đích khác nhau, bao gồm nghệ thuật, giải trí, thiết kế và giảng dạy.",
|
||||
"share_instructions": "Để cộng tác trên một bản vẽ trong thời gian thực với ai đó, chỉ cần gửi cho họ URL của bản vẽ đó.",
|
||||
"public_board_description": "Tất cả mọi người đều có thể truy cập <b> bảng công khai </b>. Đó là một mớ hỗn độn vô tổ chức vui vẻ, nơi bạn có thể gặp gỡ những người lạ vô danh và cùng nhau vẽ. Mọi thứ ở đó là phù du.",
|
||||
"open_public_board": "Đi tới bảng công khai",
|
||||
"private_board_description": "Bạn có thể tạo một <b> bảng riêng </b> với một tên ngẫu nhiên, chỉ có thể truy cập được bằng liên kết của nó. Sử dụng cái này nếu bạn muốn chia sẻ thông tin cá nhân.",
|
||||
"create_private_board": "Tạo một bảng riêng",
|
||||
"named_private_board_description": "Bạn cũng có thể tạo <strong> bảng riêng được đặt tên </strong>, với URL tùy chỉnh, tất cả những người biết tên của nó đều có thể truy cập được.",
|
||||
"board_name_placeholder": "Tên bản …",
|
||||
"view_source": "Source code on GitHub"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue