var prot = window.location.protocol;
async function updateProtocolAndUrl(host)
{
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
if (prot == "http:") {
try {
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
}
catch (error) {
try {
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
prot = "https:";
}
catch (error) {}
}
}
url = `${prot}//${host}/_larpix`;
}
console.log(prot);
var url = `${prot}//${window.location.hostname}/_larpix`;
var params = new URLSearchParams(window.location.search);
try {
var loadingScreen = document.querySelector("loading");
var mainScreen = document.querySelector("main");
var loadingStatus = document.getElementById("loadingstatus");
var addDmBtn = document.getElementById("add-dm-btn");
var addGroupBtn = document.getElementById("add-group-btn");
var sidebarHome = document.getElementById("sidebar-home");
var sidebarHomeButton = sidebarHome.children.item(1);
var sidebarHomeIndicator = sidebarHome.children.item(0);
var sidebarAdd = document.getElementById("sidebar-add");
var sidebarAddButton = sidebarAdd.children.item(1);
var sidebarAddIndicator = sidebarAdd.children.item(0);
var roomsBarContainer = document.getElementById("roomsbar");
var roomDetailsBar = document.getElementById("roomdetailsbar");
var roomContent = document.getElementsByTagName("roomcontent")[0];
var roomsBar = document.getElementsByTagName("roomcontent2")[0];
var sideBar = document.getElementsByTagName("sidebar")[0];
var roomContentMain = document.getElementsByTagName("roomcontent2")[1];
var roomDetailsMain = document.getElementsByTagName("roomcontent2")[2];
var roomContentBar = roomContent.children[0];
var roomsTopBar = document.getElementsByTagName("roomtopbar")[0];
var roomTopBar = document.getElementsByTagName("roomtopbar")[1];
var detailsTopBar = document.getElementsByTagName("roomtopbar")[2];
var sidebarProfile = document.getElementById("sidebar-profile");
var sidebarProfileButton = sidebarProfile.children.item(1);
var sidebarProfileIndicator = sidebarProfile.children.item(0);
var sidebarPfp = sidebarProfileButton.children.item(0);
var sidebarInbox = document.getElementById("sidebar-inbox");
var sidebarInboxButton = sidebarInbox.children.item(1);
var sidebarInboxIndicator = sidebarInbox.children.item(0);
var fixedContextMenu = document.getElementById("fixed-context-menu");
} catch (e) {
}
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function packetEncPass(pass, key, accountId) {
return await encryptWithNonce(pass, key, getNonce(accountId, key));
}
async function getNonce(accountId, key) {
let nonce;
let fetchRes = await (await fetch(`${url}/nextnonce?id=${accountId}`)).text();
try {
nonce = await decryptString(fetchRes, key);
} catch (err) {
nonce = await decryptString(fetchRes, "");
}
return nonce;
}
async function encryptWithNonce(value, key, nonce) {
return await encryptString(value, nonce + key);
}
async function calcHybridSharedKeyClient(pubX25519ServerBase64, pubMlKemServerBase64) {
const pubX25519Server = base64ToUint8(pubX25519ServerBase64);
const pubMlKemServer = base64ToUint8(pubMlKemServerBase64);
// X25519
const privX25519Client = window.x25519.utils.randomSecretKey();
const pubX25519Client = window.x25519.getPublicKey(privX25519Client);
const secretX25519 = window.x25519.getSharedSecret(privX25519Client, pubX25519Server);
// ML-KEM-768
const mlkem = new window.MlKem768();
const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemServer);
// Combine and Hash: SHA256(X25519_Secret || MLKEM_Secret)
const combined = new Uint8Array(secretX25519.length + secretMlKem.length);
combined.set(secretX25519);
combined.set(secretMlKem, secretX25519.length);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', combined);
const aesKey = new Uint8Array(hashBuffer);
return [uint8ToBase64(pubX25519Client), uint8ToBase64(ciphertextMlKem), aesKey];
}
function keyDataFromServerJson(jsonFromServer) {
const data = JSON.parse(jsonFromServer);
return [data.pubX25519, data.pubMlKem, data.idKey]
}
function base64ToUint8(base64) {
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
function uint8ToBase64(uint8) {
return fixBase64Padding(btoa(String.fromCharCode(...uint8)));
}
function concatUint8(a, b) {
const out = new Uint8Array(a.length + b.length);
out.set(a, 0);
out.set(b, a.length);
return out;
}
async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(passwordPlain),
{ name: "PBKDF2" },
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
async function encryptJsonWithPassword(jsonObj, passwordPlain) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const iterations = 310000;
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations);
const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj));
const ciphertextBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
const ciphertext = new Uint8Array(ciphertextBuf);
return JSON.stringify({
v: 1,
kdf: "PBKDF2-SHA256",
iter: iterations,
alg: "AES-256-GCM",
salt: uint8ToBase64(salt),
iv: uint8ToBase64(iv),
ct: uint8ToBase64(ciphertext)
});
}
async function decryptJsonWithPassword(envelopeJson, passwordPlain) {
const env = JSON.parse(envelopeJson);
if (!env || env.v !== 1 || env.kdf !== "PBKDF2-SHA256" || env.alg !== "AES-256-GCM") {
throw new Error("unsupported.key.envelope");
}
const salt = base64ToUint8(env.salt);
const iv = base64ToUint8(env.iv);
const ct = base64ToUint8(env.ct);
const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter);
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
return JSON.parse(new TextDecoder().decode(plainBuf));
}
async function sha256Bytes(bytes) {
const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
return new Uint8Array(hashBuffer);
}
async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length = 32) {
const info = new TextEncoder().encode(infoString);
const salt = saltBytes ?? new Uint8Array(32);
const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]);
const bits = await crypto.subtle.deriveBits(
{ name: "HKDF", hash: "SHA-256", salt, info },
keyMaterial,
length * 8
);
return new Uint8Array(bits);
}
async function importAesGcmKey(keyBytes) {
return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
}
async function encryptAesGcmToBase64(plainText, keyBytes) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await importAesGcmKey(keyBytes);
const pt = new TextEncoder().encode(plainText);
const ctBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt);
const combined = new Uint8Array(iv.length + ctBuf.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ctBuf), iv.length);
return uint8ToBase64(combined);
}
async function decryptAesGcmFromBase64(cipherBase64, keyBytes) {
const combined = base64ToUint8(cipherBase64);
const iv = combined.slice(0, 12);
const ct = combined.slice(12);
const key = await importAesGcmKey(keyBytes);
const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
return new TextDecoder().decode(ptBuf);
}
function randomRoomKey256Chars() {
const bytes = crypto.getRandomValues(new Uint8Array(192));
return uint8ToBase64(bytes);
}
async function deriveDmSelfWrapKeyBytes() {
if (!userKeysPrivate) throw new Error("missing.user.private.keys");
const privX = base64ToUint8(userKeysPrivate.privX25519);
const privM = base64ToUint8(userKeysPrivate.privMlKem);
return await hkdfSha256Bytes(concatUint8(privX, privM), "larpix:dm:selfwrap:v1");
}
async function wrapRoomKeyForSelf(roomKey) {
const keyBytes = await deriveDmSelfWrapKeyBytes();
const enc = await encryptAesGcmToBase64(roomKey, keyBytes);
return `DMK1:${enc}`;
}
async function unwrapRoomKeyForSelf(wrapped) {
if (!wrapped || !wrapped.startsWith("DMK1:")) throw new Error("unsupported.dm.key.format");
const keyBytes = await deriveDmSelfWrapKeyBytes();
return await decryptAesGcmFromBase64(wrapped.substring("DMK1:".length), keyBytes);
}
async function deriveHybridKeyToUser(targetPublicKeysObj) {
const pubX25519Target = base64ToUint8(targetPublicKeysObj.pubX25519);
const pubMlKemTarget = base64ToUint8(targetPublicKeysObj.pubMlKem);
const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519);
const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Target);
const mlkem = new window.MlKem768();
const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemTarget);
const combined = concatUint8(secretX25519, secretMlKem);
const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1");
return {
aesKeyBytes,
ciphertextMlKemBase64: uint8ToBase64(ciphertextMlKem)
};
}
async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase64) {
const pubX25519Other = base64ToUint8(otherPublicKeysObj.pubX25519);
const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519);
const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Other);
const ct = base64ToUint8(ciphertextMlKemBase64);
const privMlKemMine = base64ToUint8(userKeysPrivate.privMlKem);
const mlkem = new window.MlKem768();
const secretMlKem = await mlkem.decap(ct, privMlKemMine);
const combined = concatUint8(secretX25519, secretMlKem);
const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1");
return aesKeyBytes;
}
function userPublicKeysFingerprint(pub) {
if (!pub) return "";
return `${pub.pubX25519 || ""}|${pub.pubMlKem || ""}`;
}
async function fetchDmKey(dmId) {
return await fetchEncrypted("dm/key/get", dmId);
}
async function updateDmKey(dmId, keyValue) {
return await fetchEncrypted(
"dm/key/update",
JSON.stringify({
string1: dmId,
string2: keyValue
})
);
}
async function ensureDmRoomKey(dmId) {
//returns decrypted roomkey, and converts SETUP key
await ensureUserKeys();
const raw = await fetchDmKey(dmId);
if (!raw) throw new Error("error:dm.key.missing");
if (raw.startsWith("SETUP:")) {
const rest = raw.substring("SETUP:".length);
const parts = rest.split(";");
if (parts.length < 2) throw new Error("error:dm.key.setup.malformed");
const otherPublicJson = parts[0];
const setupPayloadJson = parts.slice(1).join(";");
const otherPublic = JSON.parse(otherPublicJson);
const setupPayload = JSON.parse(setupPayloadJson);
const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem);
const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
await unwrapRoomKeyForSelf(selfWrapped);
await updateDmKey(dmId, selfWrapped);
return roomKey;
}
return await unwrapRoomKeyForSelf(raw);
}
window.ensureDmRoomKey = ensureDmRoomKey;
async function encrypt(plainText, keyBytes) {
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const cryptoKey = await window.crypto.subtle.importKey(
"raw", keyBytes, "AES-CBC", false, ["encrypt"]
);
const encryptedContent = await window.crypto.subtle.encrypt(
{name: "AES-CBC", iv: iv},
cryptoKey,
data
);
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
result.set(iv);
result.set(new Uint8Array(encryptedContent), iv.length);
return uint8ToBase64(result);
}
async function decrypt(cipherTextBase64, keyBytes) {
const fullData = base64ToUint8(cipherTextBase64);
const iv = fullData.slice(0, 16);
const cipherData = fullData.slice(16);
const cryptoKey = await window.crypto.subtle.importKey(
"raw", keyBytes, "AES-CBC", false, ["decrypt"]
);
const decrypted = await window.crypto.subtle.decrypt(
{name: "AES-CBC", iv: iv},
cryptoKey,
cipherData
);
return new TextDecoder().decode(decrypted);
}
async function getCryptoKey(passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(passphrase);
const hash = await crypto.subtle.digest('SHA-256', data);
return await crypto.subtle.importKey('raw', hash, {name: 'AES-CBC'}, false, ['encrypt', 'decrypt']);
}
async function encryptBytes(plainBytes, passphrase) {
const key = await getCryptoKey(passphrase);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encryptedContent = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
key,
plainBytes
);
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
result.set(iv);
result.set(new Uint8Array(encryptedContent), iv.length);
return result;
}
async function decryptBytes(combinedBytes, passphrase) {
const key = await getCryptoKey(passphrase);
const iv = combinedBytes.slice(0, 16);
const data = combinedBytes.slice(16);
const decryptedContent = await crypto.subtle.decrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
return new Uint8Array(decryptedContent);
}
async function encryptString(plainText, passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
const key = await crypto.subtle.importKey(
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(16));
const encrypted = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return fixBase64Padding(btoa(String.fromCharCode(...combined)));
}
async function decryptString(base64Text, passphrase) {
const encoder = new TextEncoder();
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
const key = await crypto.subtle.importKey(
'raw', pwHash, {name: 'AES-CBC'}, false, ['decrypt']
);
const iv = combined.slice(0, 16);
const data = combined.slice(16);
const decrypted = await crypto.subtle.decrypt(
{name: 'AES-CBC', iv: iv},
key,
data
);
return new TextDecoder().decode(decrypted);
}
let requestMutex = Promise.resolve();
async function fetchPost(url, value) {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let response = await fetch(url, {
method: "POST",
body: value,
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
return data;
} finally {
release();
}
}
async function fetchPostEnc(url, value) {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let nonce = await getNonce(id, passwordHash);
let response = await fetch(url, {
method: "POST",
body: await encryptWithNonce(value, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return data;
} finally {
release();
}
}
async function fetchAsync(url) {
let response = await fetch(url, {
method: "GET",
});
let data = await response.text();
return data;
}
async function fetchAsyncWAuth(url) {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let response = await fetch(url, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(id, passwordHash))
}
});
let data = await response.text();
return data;
} finally {
release();
}
}
async function getServerInfo(host) {
console.log(`${prot}//${host}/_larpix/serverinfo`)
return JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
}
async function Auth(loginUsername, loginPassword) {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${loginUsername}`);
if (!resolvedId || resolvedId.trim() === "" || resolvedId.startsWith("error:")) {
return "error:invalid.username.or.password";
}
let actualId = resolvedId.split(":")[0];
let passwordHash = await hashSHA3_512(loginPassword);
let response = await fetch(`${url}/auth?id=${actualId}`, {
method: "GET",
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(actualId, passwordHash))
}
});
let data = await response.text();
if (data.startsWith("success:")) {
data += "|" + actualId;
}
return data;
} finally {
release();
}
}
async function fetchEncrypted(request, body = "") {
let release;
const lock = new Promise(resolve => release = resolve);
const prevMutex = requestMutex;
requestMutex = prevMutex.then(() => lock);
await prevMutex;
try {
let nonce = await getNonce(id, passwordHash);
let response = await fetch(`${url}/encryptedrequest?id=${id}`, {
method: "POST",
body: await encryptWithNonce(
JSON.stringify({
string1: request,
string2: body
})
, passwordHash, nonce),
headers: {
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
}
});
let data = await response.text();
return await decryptString(data, passwordHash);
} finally {
release();
}
}
function power(base, exponent, mod) {
let res = 1n;
base = base % mod;
while (exponent > 0n) {
if (exponent % 2n === 1n) res = (res * base) % mod;
base = (base * base) % mod;
exponent = exponent / 2n;
}
return res;
}
function fixBase64Padding(base64String) {
let str = base64String.replace(/-/g, '+').replace(/_/g, '/');
const pad = str.length % 4;
if (pad) {
if (pad === 1) {
throw new Error("");
}
str += '='.repeat(4 - pad);
}
return str;
}
function showBlahNotification(blahmessage, duration = 3500) {
try {
let split = blahmessage.split(":");
showNotification(processBlah(blahmessage), split[0], duration);
} catch (e) {
showNotification(blahmessage, "info", duration);
}
}
function showNotification(message, type = 'info', duration = 3500) {
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
}
const notif = document.createElement('div');
notif.className = `notification ${type}`;
notif.textContent = message;
container.appendChild(notif);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
notif.classList.add('show');
});
});
setTimeout(() => {
notif.classList.remove('show');
notif.addEventListener('transitionend', () => {
notif.remove();
});
setTimeout(() => notif.remove(), 300);
}, duration);
}
function showAction(message, actionid) {
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
}
const notif = document.createElement('div');
notif.className = `notification`;
notif.textContent = processBlah(message);
notif.id = `notification-${actionid}`;
container.appendChild(notif);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
notif.classList.add('show');
});
});
}
function clearAction(actionid) {
const notifications = document.querySelectorAll(`[id="notification-${actionid}"]`);
notifications.forEach(notif => {
notif.classList.remove('show');
notif.addEventListener('transitionend', async () => {
notif.remove();
}, { once: true });
setTimeout(() => notif.remove(), 300);
});
}
async function hashSHA3_512(input) {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const hashBuffer = await crypto.subtle.digest('SHA-512', data); //-3 kiedys xddddddddddddddddd
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
//placeholder pfp
function getInitials(name) {
const cleanName = (name || "").trim();
//name empty
if (!cleanName) return "";
const parts = cleanName.split(/\s+/);
//at least 2 words
if (parts.length >= 2) {
const firstInitial = parts[0][0];
const secondInitial = parts[parts.length - 1][0]; //from last word
return (firstInitial + secondInitial).toUpperCase();
}
//only 1 word with 1 letter
const word = parts[0];
if (word.length === 1) {
return word.toUpperCase();
}
//1 word at least 2 letters
return word.substring(0, 2).toUpperCase();
}
function stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const h = Math.abs(hash) % 360;
const s = 50;
const l = 65;
return `hsl(${h}, ${s}%, ${l}%)`;
}
function createAvatarSvg(name, size = 512) {
const initials = getInitials(name);
const color = stringToColor(name);
const svg = `
`.trim();
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
async function getAvatarUrl(id, username)
{
try {
let pfpUrl = `${url}/user/storage/public/getentry?id=${id}&e=larp.profile.pfp`;
if ((await fetchAsync(pfpUrl)) == "")
{
throw Error();
}
return pfpUrl;
}
catch (e) {
return createAvatarSvg(username);
}
}
function updateLoadingStatus(message) {
loadingStatus.innerHTML = processBlah(message);
}
async function loadingFadeOut() {
mainScreen.style.transform = "scale(0.85)";
mainScreen.style.opacity = "0";
loadingScreen.style.transform = "scale(0.85)";
loadingScreen.style.opacity = "0";
await delay(200);
loadingScreen.style.display = "none";
mainScreen.style.display = "";
await delay(200);
mainScreen.style.transform = "";
mainScreen.style.opacity = "";
}
async function loadingFadeIn() {
loadingScreen.style.transform = "scale(0.85)";
loadingScreen.style.opacity = "0";
mainScreen.style.transform = "scale(0.85)";
mainScreen.style.opacity = "0";
await delay(200);
mainScreen.style.display = "none";
loadingScreen.style.display = "";
await delay(200);
loadingScreen.style.transform = "";
loadingScreen.style.opacity = "";
}
var blah;
async function initBlahs() {
lang = lang.toLowerCase();
let res;
let path = window.location.pathname.replace("/login/index.html", "/");
try { //try user lang first
res = await fetchAsync(`${path}blah/${lang}.json`);
} catch (e) { //fallback to en-us
res = await fetchAsync(`${path}blah/en-us.json`);
}
blah = JSON.parse(res);
let blahTags = document.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = document.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = document.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
}
function splitLimit(str, separator, limit) {
const parts = str.split(separator);
if (parts.length <= limit) {
return parts;
}
return [
...parts.slice(0, limit - 1),
parts.slice(limit - 1).join(separator)
];
}
function processBlah(blahmessage) {
let prepended = false;
try {
if (!blahmessage.includes(":")) {
blahmessage = `:${blahmessage}`;
prepended = true;
}
let split = splitLimit(blahmessage, ":", 3);
let values = [];
try {
if (split[2]) {
values = split[2].split(";");
}
} catch (e) {
}
let message = blah[split[1]];
if (message === undefined) throw new Error();
let valueslist = "";
for (let i = 0; i < values.length; i++) {
let value = processBlah(values[i]);
valueslist+=`${value}, `;
message = message.replaceAll(`{${i}}`, value);
}
if (values.length > 0) {
valueslist = valueslist.slice(0, -2);
}
if (message.includes('{all}')) {
message = message.replaceAll('{all}', valueslist);
}
return message;
}
catch (e)
{
if (prepended) {
return blahmessage.substring(1);
}
return blahmessage;
}
}
function getLang() {
return (navigator.language || navigator.languages[0]);
//return "en-cat";
}
var id = "";
var password = "";
var username = "";
var passwordHash = "";
var host = "";
var lang = getLang();
var userKeysPublic = null; //2string json
var userKeysPrivate = null; // decrypted private bundle (JSON object)
var userKeysEncrypted = null; // encrypted private bundle (string envelope JSON)
function publishUserKeysGlobals() {
window.userKeysPublic = userKeysPublic;
window.userKeysPrivate = userKeysPrivate;
window.userKeysEncrypted = userKeysEncrypted;
}
async function generateUserKeysBundle() {
// X25519 keypair
const privX25519 = window.x25519.utils.randomSecretKey();
const pubX25519 = window.x25519.getPublicKey(privX25519);
// ML-KEM-768 keypair
const mlkem = new window.MlKem768();
const [pubMlKem, privMlKem] = await mlkem.generateKeyPair();
const pub = {
v: 1,
alg: "hybrid-x25519-mlkem768",
pubX25519: uint8ToBase64(pubX25519),
pubMlKem: uint8ToBase64(pubMlKem)
};
const priv = {
v: 1,
alg: "hybrid-x25519-mlkem768",
privX25519: uint8ToBase64(privX25519),
privMlKem: uint8ToBase64(privMlKem)
};
return { pub, priv };
}
async function ensureUserKeys() {
const lsEncKey = `userKeys.enc.v1:${id}`;
const lsPubKey = `userKeys.pub.v1:${id}`;
let serverKeys = null;
let serverKeysRaw = "";
try { //1. check server
serverKeysRaw = await fetchEncrypted("user/key/get");
if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) {
serverKeys = JSON.parse(serverKeysRaw);
const serverEncPriv = serverKeys.string1;
const serverPub = serverKeys.string2;
if (serverEncPriv && serverPub) {
userKeysEncrypted = serverEncPriv;
userKeysPublic = JSON.parse(serverPub);
userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
localStorage.setItem(lsEncKey, userKeysEncrypted);
localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic));
publishUserKeysGlobals();
return "success:keys.server";
}
}
} catch (e) {
if (serverKeys?.string1) {
throw new Error("error:keys.server.decrypt.failed");
}
}
//fallback to local
const localEnc = localStorage.getItem(lsEncKey);
const localPub = localStorage.getItem(lsPubKey);
if (localEnc && localPub) {
try {
const localPubObj = JSON.parse(localPub);
if (serverKeys?.string2) {
const serverPubObj = JSON.parse(serverKeys.string2);
if (userPublicKeysFingerprint(localPubObj) !== userPublicKeysFingerprint(serverPubObj)) {
throw new Error("error:keys.local.server.mismatch");
}
}
userKeysEncrypted = localEnc;
userKeysPublic = localPubObj;
userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
//push keys to server only when server has none yet
if (!serverKeys?.string1) {
const updateRes = await fetchEncrypted(
"user/key/update",
JSON.stringify({
string1: userKeysEncrypted,
string2: JSON.stringify(userKeysPublic)
})
);
if (updateRes === "error:keys.public.mismatch") {
throw new Error("error:keys.local.server.mismatch");
}
}
publishUserKeysGlobals();
return "success:keys.local";
} catch (e) {
if (e.message && e.message.startsWith("error:keys.")) {
throw e;
}
}
}
if (serverKeys?.string1) {
throw new Error("error:keys.server.decrypt.failed");
}
//new keys, encrypt private with user's password
const bundle = await generateUserKeysBundle();
userKeysPublic = bundle.pub;
userKeysPrivate = bundle.priv;
userKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, password);
localStorage.setItem(lsEncKey, userKeysEncrypted);
localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic));
await fetchEncrypted(
"user/key/update",
JSON.stringify({
string1: userKeysEncrypted,
string2: JSON.stringify(userKeysPublic)
})
);
publishUserKeysGlobals();
return "success:keys.generated";
}
async function mainJS() {
await initBlahs();
passwordHash = await hashSHA3_512(password);
if (localStorage.getItem('lang') != null) {
lang = localStorage.getItem('lang');
}
if (host) {
await updateProtocolAndUrl(host);
} else {
await updateProtocolAndUrl(window.location.hostname);
}
if (!id && username) {
let resolvedId = await fetchAsync(`${url}/nametoid?u=${username}`);
if (resolvedId && !resolvedId.startsWith("error:")) {
id = resolvedId.split(":")[0];
localStorage.setItem("id", id);
}
}
await start();
}
id = localStorage.getItem('id');
username = localStorage.getItem('username');
password = localStorage.getItem('password');
host = localStorage.getItem('host');
mainJS();
function showFixedContextMenu(rect, html) {
let parser = new DOMParser();
let doc = parser.parseFromString(html, "text/html");
let blahTags = doc.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = doc.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = doc.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
fixedContextMenu.innerHTML = doc.body.innerHTML;
fixedContextMenu.style.left = `${rect.right + 10}px`;
fixedContextMenu.style.top = `${rect.top}px`;
fixedContextMenu.classList.add("show");
}
var currentRoomsBarTitle = null;
var currentRoomsBarHtml = null;
async function switchRoomsBar(title, content) {
if (currentRoomsBarTitle === title && currentRoomsBarHtml === content) return;
currentRoomsBarTitle = title;
currentRoomsBarHtml = content;
let roomsTopBarTransition = roomsTopBar.children.item(0);
roomsBar.style.transform = "scale(0.85)";
roomsBar.style.opacity = "0";
roomsTopBarTransition.style.transform = "scale(0.85)";
roomsTopBarTransition.style.opacity = "0";
await delay(200);
roomsTopBarTransition.innerHTML = processBlah(title);
let parser = new DOMParser();
let doc = parser.parseFromString(content, "text/html");
let blahTags = doc.getElementsByTagName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
blahTags = doc.getElementsByClassName("blah");
for (let i = 0; i < blahTags.length; i++) {
blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML);
}
let placeholders = doc.querySelectorAll("[placeholder]");
for (let i = 0; i < placeholders.length; i++) {
if (placeholders[i].placeholder.startsWith("{blah("))
{
let value = placeholders[i].placeholder.split("{blah(")[1].split(")}")[0];
placeholders[i].placeholder = processBlah(value);
}
}
roomsBar.innerHTML = doc.body.innerHTML;
roomsBar.style.transform = "";
roomsBar.style.opacity = "";
roomsTopBarTransition.style.transform = "";
roomsTopBarTransition.style.opacity = "";
}
var currentRoomContentTitle = null;
var currentRoomContentHtml = null;
async function switchRoomContent(title, content, showRoomBar, icon = "", skipMobileSlide = false)
{
if (currentRoomContentTitle === title && currentRoomContentHtml === content) {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
if (title != "title.splash") { //do not show splash on mobile
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
}
}
return;
}
currentRoomContentTitle = title;
currentRoomContentHtml = content;
let roomsTopBarTransition = roomTopBar.children.item(0);
roomContentMain.style.transform = "scale(0.85)";
roomContentMain.style.opacity = "0";
roomsTopBarTransition.style.transform = "scale(0.85)";
roomsTopBarTransition.style.opacity = "0";
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
if (window.innerWidth <= 52 * rem && !skipMobileSlide) {
if (title != "title.splash") //do not show splash on mobile
{
mainScreen.classList.remove('mobile-details');
mainScreen.classList.add('mobile-content');
}
}
else
{
await delay(200);
}
let detailsBtnHtml = showRoomBar ? detailsBtn : '';
roomsTopBarTransition.innerHTML = `${backBtnHtml}${icon}