Update app.js
Browse files
app.js
CHANGED
|
@@ -18,6 +18,21 @@ const OWNER_NAME = 'Noah';
|
|
| 18 |
const OWNER_PASS = 'Noah100419!';
|
| 19 |
const POLL_MS = 3000; // Polling-Intervall in ms
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
// βββ STATE ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
let currentUser = null;
|
| 23 |
let currentChannel = 'allgemein';
|
|
@@ -67,11 +82,21 @@ function getFingerprint() {
|
|
| 67 |
}
|
| 68 |
const MY_FP = getFingerprint();
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
// βββ JSONBIN API ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 71 |
const isConfigured = () =>
|
| 72 |
!JSONBIN_KEY.includes('DEIN') && !JSONBIN_BIN.includes('DEINE');
|
| 73 |
|
| 74 |
-
async function apiLoad() {
|
| 75 |
if (!isConfigured()) {
|
| 76 |
const v = localStorage.getItem('nc_db');
|
| 77 |
DB = v ? JSON.parse(v) : DB_DEFAULT();
|
|
@@ -83,12 +108,35 @@ async function apiLoad() {
|
|
| 83 |
headers: { 'X-Master-Key': JSONBIN_KEY, 'X-Bin-Meta': 'false' }
|
| 84 |
});
|
| 85 |
if (!r.ok) throw new Error(r.status);
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
ensureFields();
|
| 88 |
-
localStorage.setItem('nc_db', JSON.stringify(DB));
|
| 89 |
} catch(e) {
|
| 90 |
console.warn('Load fehlgeschlagen:', e);
|
| 91 |
-
// Fallback: letzten lokalen Stand nehmen
|
| 92 |
const v = localStorage.getItem('nc_db');
|
| 93 |
if (v) DB = JSON.parse(v);
|
| 94 |
else DB = DB_DEFAULT();
|
|
@@ -240,6 +288,7 @@ async function doLogin() {
|
|
| 240 |
if (decode(entry.password)!==p)return err('Falscher Username oder Passwort');
|
| 241 |
if (DB.moderation.bans?.[u.toLowerCase()]) return err('π« Du bist von NoahsChat gebannt.');
|
| 242 |
currentUser={...entry};
|
|
|
|
| 243 |
refreshCaptcha('login');
|
| 244 |
dbSet(`users/${u.toLowerCase()}/_fp`, MY_FP);
|
| 245 |
dbSet(`online/${entry.username}`, Date.now());
|
|
@@ -251,6 +300,7 @@ async function doLogin() {
|
|
| 251 |
// βββ LOGOUT βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 252 |
async function doLogout() {
|
| 253 |
if (!currentUser) return;
|
|
|
|
| 254 |
dbDelete(`online/${currentUser.username}`);
|
| 255 |
if (voiceActive) leaveVoice();
|
| 256 |
clearInterval(pollTimer);
|
|
@@ -285,9 +335,9 @@ function startPolling() {
|
|
| 285 |
async function pollTick() {
|
| 286 |
if (!currentUser) return;
|
| 287 |
|
| 288 |
-
// DB neu laden β bekommt Γnderungen anderer User
|
| 289 |
setSyncStatus('sync');
|
| 290 |
-
await apiLoad();
|
| 291 |
setSyncStatus('ok');
|
| 292 |
|
| 293 |
// Eigene Daten aktualisieren (Rolle kΓΆnnte geΓ€ndert worden sein)
|
|
@@ -299,9 +349,11 @@ async function pollTick() {
|
|
| 299 |
translationEnabled = me.settings?.translate||false;
|
| 300 |
}
|
| 301 |
|
| 302 |
-
// Heartbeat
|
|
|
|
| 303 |
DB.online[currentUser.username] = Date.now();
|
| 304 |
-
|
|
|
|
| 305 |
|
| 306 |
// Moderation-Checks
|
| 307 |
const mod = DB.moderation;
|
|
@@ -1347,7 +1399,23 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
| 1347 |
// Loading entfernen
|
| 1348 |
loadEl.remove();
|
| 1349 |
|
| 1350 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1351 |
refreshCaptcha('login');
|
| 1352 |
refreshCaptcha('reg');
|
| 1353 |
});
|
|
|
|
| 18 |
const OWNER_PASS = 'Noah100419!';
|
| 19 |
const POLL_MS = 3000; // Polling-Intervall in ms
|
| 20 |
|
| 21 |
+
// βββ SESSION PERSISTENCE ββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
function saveSession(user) {
|
| 23 |
+
if (user) localStorage.setItem('nc_session', JSON.stringify({ username: user.username, ts: Date.now() }));
|
| 24 |
+
else localStorage.removeItem('nc_session');
|
| 25 |
+
}
|
| 26 |
+
function loadSession() {
|
| 27 |
+
try {
|
| 28 |
+
const s = localStorage.getItem('nc_session');
|
| 29 |
+
if (!s) return null;
|
| 30 |
+
const parsed = JSON.parse(s);
|
| 31 |
+
if (Date.now() - parsed.ts > 7 * 86400000) { localStorage.removeItem('nc_session'); return null; }
|
| 32 |
+
return parsed;
|
| 33 |
+
} catch { return null; }
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
// βββ STATE ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
let currentUser = null;
|
| 38 |
let currentChannel = 'allgemein';
|
|
|
|
| 82 |
}
|
| 83 |
const MY_FP = getFingerprint();
|
| 84 |
|
| 85 |
+
let heartbeatTimer = null;
|
| 86 |
+
function scheduleHeartbeatSave() {
|
| 87 |
+
// Nur alle 6 Sekunden wirklich speichern (Polling ist 3s, also jeden 2. Tick)
|
| 88 |
+
if (heartbeatTimer) return;
|
| 89 |
+
heartbeatTimer = setTimeout(() => {
|
| 90 |
+
heartbeatTimer = null;
|
| 91 |
+
apiSave();
|
| 92 |
+
}, 6000);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
// βββ JSONBIN API ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 96 |
const isConfigured = () =>
|
| 97 |
!JSONBIN_KEY.includes('DEIN') && !JSONBIN_BIN.includes('DEINE');
|
| 98 |
|
| 99 |
+
async function apiLoad(isMerge = false) {
|
| 100 |
if (!isConfigured()) {
|
| 101 |
const v = localStorage.getItem('nc_db');
|
| 102 |
DB = v ? JSON.parse(v) : DB_DEFAULT();
|
|
|
|
| 108 |
headers: { 'X-Master-Key': JSONBIN_KEY, 'X-Bin-Meta': 'false' }
|
| 109 |
});
|
| 110 |
if (!r.ok) throw new Error(r.status);
|
| 111 |
+
const remote = await r.json();
|
| 112 |
+
|
| 113 |
+
if (isMerge && DB) {
|
| 114 |
+
// Smart merge: Remote-Daten haben Vorrang, aber lokale Γnderungen die
|
| 115 |
+
// noch nicht gepusht wurden (pending saves) bleiben erhalten.
|
| 116 |
+
// Nachrichten: Union beider Sets (remote + lokal), remote gewinnt bei Konflikt
|
| 117 |
+
const mergedMsgs = {};
|
| 118 |
+
const allChannels = new Set([...Object.keys(DB.msgs || {}), ...Object.keys(remote.msgs || {})]);
|
| 119 |
+
for (const ch of allChannels) {
|
| 120 |
+
const localCh = DB.msgs?.[ch] || {};
|
| 121 |
+
const remoteCh = remote.msgs?.[ch] || {};
|
| 122 |
+
mergedMsgs[ch] = { ...localCh, ...remoteCh }; // Remote ΓΌberschreibt lokal bei gleichem Key
|
| 123 |
+
}
|
| 124 |
+
const mergedDms = {};
|
| 125 |
+
const allDms = new Set([...Object.keys(DB.dms || {}), ...Object.keys(remote.dms || {})]);
|
| 126 |
+
for (const dm of allDms) {
|
| 127 |
+
const localDm = DB.dms?.[dm] || {};
|
| 128 |
+
const remoteDm = remote.dms?.[dm] || {};
|
| 129 |
+
mergedDms[dm] = { ...localDm, ...remoteDm };
|
| 130 |
+
}
|
| 131 |
+
DB = { ...remote, msgs: mergedMsgs, dms: mergedDms };
|
| 132 |
+
} else {
|
| 133 |
+
DB = remote;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
ensureFields();
|
| 137 |
+
localStorage.setItem('nc_db', JSON.stringify(DB));
|
| 138 |
} catch(e) {
|
| 139 |
console.warn('Load fehlgeschlagen:', e);
|
|
|
|
| 140 |
const v = localStorage.getItem('nc_db');
|
| 141 |
if (v) DB = JSON.parse(v);
|
| 142 |
else DB = DB_DEFAULT();
|
|
|
|
| 288 |
if (decode(entry.password)!==p)return err('Falscher Username oder Passwort');
|
| 289 |
if (DB.moderation.bans?.[u.toLowerCase()]) return err('π« Du bist von NoahsChat gebannt.');
|
| 290 |
currentUser={...entry};
|
| 291 |
+
saveSession(currentUser);
|
| 292 |
refreshCaptcha('login');
|
| 293 |
dbSet(`users/${u.toLowerCase()}/_fp`, MY_FP);
|
| 294 |
dbSet(`online/${entry.username}`, Date.now());
|
|
|
|
| 300 |
// βββ LOGOUT βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 301 |
async function doLogout() {
|
| 302 |
if (!currentUser) return;
|
| 303 |
+
saveSession(null);
|
| 304 |
dbDelete(`online/${currentUser.username}`);
|
| 305 |
if (voiceActive) leaveVoice();
|
| 306 |
clearInterval(pollTimer);
|
|
|
|
| 335 |
async function pollTick() {
|
| 336 |
if (!currentUser) return;
|
| 337 |
|
| 338 |
+
// DB neu laden β bekommt Γnderungen anderer User (mit Merge, damit lokale Msgs nicht verloren gehen)
|
| 339 |
setSyncStatus('sync');
|
| 340 |
+
await apiLoad(true);
|
| 341 |
setSyncStatus('ok');
|
| 342 |
|
| 343 |
// Eigene Daten aktualisieren (Rolle kΓΆnnte geΓ€ndert worden sein)
|
|
|
|
| 349 |
translationEnabled = me.settings?.translate||false;
|
| 350 |
}
|
| 351 |
|
| 352 |
+
// Heartbeat β NUR den eigenen Online-Eintrag setzen, NICHT die komplette DB speichern
|
| 353 |
+
// Direkt im Objekt setzen und gezielt speichern um Race-Conditions zu vermeiden
|
| 354 |
DB.online[currentUser.username] = Date.now();
|
| 355 |
+
// Heartbeat-only save: kleines Debounce damit nicht zu viele Requests entstehen
|
| 356 |
+
scheduleHeartbeatSave();
|
| 357 |
|
| 358 |
// Moderation-Checks
|
| 359 |
const mod = DB.moderation;
|
|
|
|
| 1399 |
// Loading entfernen
|
| 1400 |
loadEl.remove();
|
| 1401 |
|
| 1402 |
+
// Session wiederherstellen (Auto-Login nach Reload)
|
| 1403 |
+
const session = loadSession();
|
| 1404 |
+
if (session) {
|
| 1405 |
+
const entry = DB.users[session.username.toLowerCase()];
|
| 1406 |
+
if (entry && !DB.moderation?.bans?.[session.username.toLowerCase()] && !DB.moderation?.banned_fps?.[MY_FP]) {
|
| 1407 |
+
currentUser = { ...entry };
|
| 1408 |
+
userLanguage = entry.settings?.lang || 'de';
|
| 1409 |
+
translationEnabled = entry.settings?.translate || false;
|
| 1410 |
+
saveSession(currentUser); // Timestamp erneuern
|
| 1411 |
+
launchApp();
|
| 1412 |
+
return; // Auth-Screen nicht zeigen
|
| 1413 |
+
} else {
|
| 1414 |
+
saveSession(null); // UngΓΌltige Session lΓΆschen
|
| 1415 |
+
}
|
| 1416 |
+
}
|
| 1417 |
+
|
| 1418 |
+
// Captchas immer initialisieren (auch falls Session nicht klappt)
|
| 1419 |
refreshCaptcha('login');
|
| 1420 |
refreshCaptcha('reg');
|
| 1421 |
});
|