mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-06 09:27:44 +00:00
Stable state before Final Polish optimizations
This commit is contained in:
+2
-1
@@ -3,7 +3,8 @@
|
||||
Welcome to the KoalaSync project. This file is the primary entry point for any developer or AI agent working on this codebase. It defines the architecture, non-negotiables, and workflows required to maintain the stability and security of the system.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Privacy & Data Sovereignty**: KoalaSync follows a strict "Zero-External-Requests" policy. No fonts, scripts, or assets (images/icons) may be loaded from 3rd-party CDNs or external servers. Everything must be served locally or from the project's own infrastructure.
|
||||
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
|
||||
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+78
-39
@@ -11,6 +11,7 @@ let currentTabId = null;
|
||||
let currentTabTitle = null; // New: for Smart Matching
|
||||
let logs = [];
|
||||
let history = []; // New: for Action History
|
||||
let reconnectTimer = null;
|
||||
|
||||
// Force Sync Coordination
|
||||
let isForceSyncInitiator = false;
|
||||
@@ -53,7 +54,12 @@ function addLog(message, type = 'info') {
|
||||
|
||||
// --- WebSocket Client ---
|
||||
async function connect() {
|
||||
if (isConnecting || (socket && socket.readyState === WebSocket.OPEN)) return;
|
||||
if (isConnecting) return;
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
|
||||
if (!navigator.onLine) {
|
||||
addLog('Browser is offline. Waiting...', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!peerId) peerId = await getPeerId();
|
||||
const settings = await getSettings();
|
||||
@@ -61,31 +67,52 @@ async function connect() {
|
||||
isConnecting = true;
|
||||
broadcastConnectionStatus('connecting');
|
||||
const isCustomServer = settings.serverUrl && settings.useCustomServer;
|
||||
const finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
|
||||
let finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
|
||||
|
||||
// Robustness: Ensure finalUrl is not empty and has a protocol
|
||||
if (isCustomServer) {
|
||||
finalUrl = finalUrl.trim();
|
||||
if (!finalUrl.includes('://')) {
|
||||
finalUrl = 'ws://' + finalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
|
||||
|
||||
const url = new URL(finalUrl);
|
||||
url.pathname = '/socket.io/';
|
||||
url.searchParams.set('EIO', '4');
|
||||
url.searchParams.set('transport', 'websocket');
|
||||
url.searchParams.set('version', APP_VERSION);
|
||||
try {
|
||||
const url = new URL(finalUrl);
|
||||
url.pathname = '/socket.io/';
|
||||
url.searchParams.set('EIO', '4');
|
||||
url.searchParams.set('transport', 'websocket');
|
||||
url.searchParams.set('version', APP_VERSION);
|
||||
|
||||
if (!isCustomServer) {
|
||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||
}
|
||||
if (!isCustomServer) {
|
||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||
} else {
|
||||
// Self-hosted servers use the same official token by design.
|
||||
// This allows users to run their own relay while still using
|
||||
// the official extension — the token is public and not a secret.
|
||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||
}
|
||||
|
||||
socket = new WebSocket(url.toString());
|
||||
socket = new WebSocket(url.toString());
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.onopen = () => {
|
||||
isConnecting = false;
|
||||
reconnectDelay = 1000;
|
||||
addLog('WebSocket Connection Opened', 'success');
|
||||
broadcastConnectionStatus('connected');
|
||||
|
||||
// Socket.IO Handshake: Send "40" to join default namespace
|
||||
socket.send('40');
|
||||
};
|
||||
} catch (e) {
|
||||
isConnecting = false;
|
||||
reconnectDelay = 1000;
|
||||
addLog('WebSocket Connection Opened', 'success');
|
||||
broadcastConnectionStatus('connected');
|
||||
|
||||
// Socket.IO Handshake: Send "40" to join default namespace
|
||||
socket.send('40');
|
||||
};
|
||||
addLog(`Invalid Server URL: ${finalUrl}`, 'error');
|
||||
broadcastConnectionStatus('disconnected');
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const msg = event.data;
|
||||
@@ -169,7 +196,10 @@ function showNotification(senderName, action) {
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
setTimeout(() => {
|
||||
if (reconnectTimer) return; // Already scheduled
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
@@ -182,6 +212,17 @@ function emit(event, data) {
|
||||
}
|
||||
}
|
||||
|
||||
function addToHistory(action, senderId) {
|
||||
const historyEntry = {
|
||||
action,
|
||||
senderId: senderId || 'You',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
history.unshift(historyEntry);
|
||||
if (history.length > 20) history.pop();
|
||||
chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
|
||||
}
|
||||
|
||||
// --- Event Handlers ---
|
||||
function handleServerEvent(event, data) {
|
||||
// console.log(`[RECV] ${event}`, data);
|
||||
@@ -203,7 +244,7 @@ function handleServerEvent(event, data) {
|
||||
break;
|
||||
case EVENTS.ERROR:
|
||||
addLog(`Server Error: ${data.message}`, 'error');
|
||||
chrome.notifications.create({
|
||||
chrome.notifications.create(`error_${Date.now()}`, {
|
||||
type: 'basic',
|
||||
iconUrl: 'icons/icon128.png',
|
||||
title: 'KoalaSync Error',
|
||||
@@ -220,6 +261,10 @@ function handleServerEvent(event, data) {
|
||||
case EVENTS.PAUSE:
|
||||
case EVENTS.SEEK:
|
||||
case EVENTS.FORCE_SYNC_PREPARE:
|
||||
if (data.senderId) {
|
||||
addToHistory(event, data.senderId);
|
||||
showNotification(data.senderId, event);
|
||||
}
|
||||
routeToContent(event, data);
|
||||
break;
|
||||
case EVENTS.FORCE_SYNC_ACK:
|
||||
@@ -234,6 +279,10 @@ function handleServerEvent(event, data) {
|
||||
}
|
||||
break;
|
||||
case EVENTS.FORCE_SYNC_EXECUTE:
|
||||
if (data.senderId) {
|
||||
addToHistory(event, data.senderId);
|
||||
showNotification(data.senderId, event);
|
||||
}
|
||||
routeToContent(event, data);
|
||||
break;
|
||||
case EVENTS.PEER_STATUS:
|
||||
@@ -263,19 +312,7 @@ function handleServerEvent(event, data) {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// History Tracking
|
||||
const historyEntry = {
|
||||
action: event,
|
||||
senderId: data.senderId || 'You',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
history.unshift(historyEntry);
|
||||
if (history.length > 20) history.pop();
|
||||
chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
|
||||
|
||||
// Notification for remote actions
|
||||
if (data.senderId) showNotification(data.senderId, event);
|
||||
routeToContent(event, data);
|
||||
addLog(`Received unknown event from server: ${event}`, 'warn');
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -303,8 +340,8 @@ async function routeToContent(action, payload) {
|
||||
action,
|
||||
payload
|
||||
}).catch(err => {
|
||||
// Auto-Reinject if content script is missing
|
||||
if (err.message.includes('Receiving end does not exist')) {
|
||||
// Auto-Reinject if content script is missing or extension was reloaded
|
||||
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
@@ -335,8 +372,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'CONNECT') {
|
||||
connect();
|
||||
} else if (message.type === 'GET_STATUS') {
|
||||
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting ? 'connecting' : 'disconnected')) : 'disconnected';
|
||||
sendResponse({ status, peers: currentRoom ? currentRoom.peers : [] });
|
||||
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting || socket.readyState === WebSocket.CONNECTING ? 'connecting' : 'disconnected')) : 'disconnected';
|
||||
sendResponse({ status, peerId, peers: currentRoom ? currentRoom.peers : [] });
|
||||
// Global return true at the end handles this
|
||||
} else if (message.type === 'LEAVE_ROOM') {
|
||||
emit(EVENTS.LEAVE_ROOM, { peerId });
|
||||
currentRoom = null;
|
||||
@@ -377,6 +415,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
addToHistory(message.action, 'You');
|
||||
emit(message.action, { ...message.payload, peerId });
|
||||
} else if (message.type === 'FORCE_SYNC_ACK') {
|
||||
emit(EVENTS.FORCE_SYNC_ACK, { peerId });
|
||||
@@ -388,7 +427,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// Peer status heartbeat from content script
|
||||
emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, tabTitle: currentTabTitle });
|
||||
}
|
||||
return true;
|
||||
return true; // Keep channel open for async responses
|
||||
});
|
||||
|
||||
// Tab removal listener
|
||||
|
||||
+11
-2
@@ -145,7 +145,8 @@
|
||||
}
|
||||
|
||||
// Heartbeat
|
||||
setInterval(() => {
|
||||
let heartbeatErrorCount = 0;
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
const video = findVideo();
|
||||
if (video) {
|
||||
chrome.runtime.sendMessage({
|
||||
@@ -154,7 +155,15 @@
|
||||
playbackState: video.paused ? 'paused' : 'playing',
|
||||
currentTime: video.currentTime
|
||||
}
|
||||
}).catch(() => {});
|
||||
}).catch(err => {
|
||||
if (err.message.includes('Extension context invalidated')) {
|
||||
heartbeatErrorCount++;
|
||||
if (heartbeatErrorCount === 1) {
|
||||
console.warn('KoalaSync: Extension reloaded. Please refresh the page if sync stops working.');
|
||||
}
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"tabs",
|
||||
"scripting",
|
||||
"alarms",
|
||||
"activeTab"
|
||||
"activeTab",
|
||||
"notifications"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
|
||||
+58
-18
@@ -1,8 +1,16 @@
|
||||
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
|
||||
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
const elements = {
|
||||
tabs: document.querySelectorAll('.tab-btn'),
|
||||
tabs: document.querySelectorAll('.tabs .tab-btn'),
|
||||
contents: document.querySelectorAll('.tab-content'),
|
||||
copyInvite: document.getElementById('copyInvite'),
|
||||
targetTab: document.getElementById('targetTab'),
|
||||
@@ -30,6 +38,8 @@ const elements = {
|
||||
roomError: document.getElementById('roomError')
|
||||
};
|
||||
|
||||
let localPeerId = null;
|
||||
|
||||
// --- Initialization ---
|
||||
async function init() {
|
||||
// Load Settings
|
||||
@@ -55,6 +65,7 @@ async function init() {
|
||||
// Initial Status Check
|
||||
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
|
||||
if (res) {
|
||||
localPeerId = res.peerId;
|
||||
applyConnectionStatus(res.status);
|
||||
updatePeerList(res.peers);
|
||||
}
|
||||
@@ -78,24 +89,36 @@ function updateUI(roomId, password) {
|
||||
}
|
||||
|
||||
function updatePeerList(peers) {
|
||||
if (!peers) return;
|
||||
elements.peerList.innerHTML = peers.map(id => `
|
||||
<div class="peer-item">
|
||||
<span>👤 ${id}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
if (!peers || !elements.peerList) return;
|
||||
elements.peerList.innerHTML = peers.map(p => {
|
||||
const id = escapeHtml(typeof p === 'object' ? p.peerId : p);
|
||||
const titleText = (typeof p === 'object' && p.tabTitle) ? escapeHtml(p.tabTitle) : '';
|
||||
const title = titleText ? `<div style="font-size:10px; color:var(--text-muted);">${titleText}</div>` : '';
|
||||
return `
|
||||
<div class="peer-item" style="display:block; padding: 6px 0;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span style="font-weight:600;">👤 ${id}</span>
|
||||
${id === escapeHtml(localPeerId) ? '<span style="font-size:10px; color:var(--accent)">YOU</span>' : ''}
|
||||
</div>
|
||||
${title}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
// Re-populate tabs to update Star Matching when peers change
|
||||
populateTabs();
|
||||
populateTabs(peers);
|
||||
}
|
||||
|
||||
async function populateTabs() {
|
||||
async function populateTabs(providedPeers = null) {
|
||||
const data = await chrome.storage.sync.get(['targetTabId', 'filterNoise']);
|
||||
const isFilterActive = data.filterNoise !== false;
|
||||
const currentTargetTabId = data.targetTabId;
|
||||
|
||||
// Get current peers from background to do matching
|
||||
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
|
||||
const peerIds = status?.peers || [];
|
||||
|
||||
// Use provided peers or fetch if missing
|
||||
let peerIds = providedPeers;
|
||||
if (!peerIds) {
|
||||
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
|
||||
peerIds = status?.peers || [];
|
||||
}
|
||||
|
||||
const tabs = await chrome.tabs.query({});
|
||||
|
||||
@@ -165,8 +188,9 @@ function updateHistory(history) {
|
||||
}
|
||||
elements.historyList.innerHTML = history.map(item => {
|
||||
const time = new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
const actionLabel = item.action.toUpperCase().replace('FORCE_SYNC_', '');
|
||||
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : item.senderId;
|
||||
const actionLabel = escapeHtml(item.action.toUpperCase().replace('FORCE_SYNC_', ''));
|
||||
const senderIdEscaped = escapeHtml(item.senderId);
|
||||
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : senderIdEscaped;
|
||||
return `<div style="margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px;">
|
||||
<span style="color:#64748b">[${time}]</span> <b>${actionLabel}</b> by ${sender}
|
||||
</div>`;
|
||||
@@ -185,9 +209,12 @@ function updateRoomList(rooms) {
|
||||
return;
|
||||
}
|
||||
elements.publicRooms.innerHTML = rooms.map(r => `
|
||||
<div class="room-item" style="display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;" data-id="${r.id}">
|
||||
<span style="font-weight:600;">${r.id}</span>
|
||||
<span style="font-size:11px; color:var(--accent)">${r.peerCount} peers</span>
|
||||
<div class="room-item" style="display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;" data-id="${escapeHtml(r.id)}">
|
||||
<div style="display:flex; align-items:center; gap: 6px;">
|
||||
<span style="font-weight:600;">${escapeHtml(r.id)}</span>
|
||||
${r.hasPassword ? '<span title="Password Protected">🔒</span>' : ''}
|
||||
</div>
|
||||
<span style="font-size:11px; color:var(--accent)">${parseInt(r.peerCount)} peers</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
@@ -232,6 +259,19 @@ elements.filterNoise.addEventListener('change', () => {
|
||||
});
|
||||
});
|
||||
|
||||
elements.serverUrl.addEventListener('input', () => {
|
||||
chrome.storage.sync.set({ serverUrl: elements.serverUrl.value });
|
||||
});
|
||||
|
||||
elements.serverUrl.addEventListener('change', () => {
|
||||
let url = elements.serverUrl.value.trim();
|
||||
if (url && !url.includes('://')) {
|
||||
url = 'ws://' + url;
|
||||
elements.serverUrl.value = url;
|
||||
chrome.storage.sync.set({ serverUrl: url });
|
||||
}
|
||||
});
|
||||
|
||||
elements.tabs.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
elements.tabs.forEach(b => b.classList.remove('active'));
|
||||
|
||||
+14
-3
@@ -178,6 +178,7 @@ io.on('connection', (socket) => {
|
||||
passwordHash,
|
||||
peers: new Set(),
|
||||
peerIds: new Map(),
|
||||
peerData: new Map(), // socketId -> { peerId, tabTitle }
|
||||
lastActivity: Date.now()
|
||||
};
|
||||
rooms.set(roomId, room);
|
||||
@@ -199,12 +200,13 @@ io.on('connection', (socket) => {
|
||||
socket.join(roomId);
|
||||
room.peers.add(socket.id);
|
||||
room.peerIds.set(socket.id, peerId);
|
||||
room.peerData.set(socket.id, { peerId, tabTitle: null });
|
||||
socketToRoom.set(socket.id, { roomId, peerId });
|
||||
|
||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'joined' });
|
||||
socket.emit(EVENTS.ROOM_DATA, {
|
||||
roomId,
|
||||
peers: Array.from(room.peers).map(sid => room.peerIds.get(sid))
|
||||
peers: Array.from(room.peers).map(sid => room.peerData.get(sid))
|
||||
});
|
||||
log('ROOM', `Peer ${peerId} joined: ${roomId}`);
|
||||
} catch (err) {
|
||||
@@ -231,7 +233,13 @@ io.on('connection', (socket) => {
|
||||
const mapping = socketToRoom.get(socket.id);
|
||||
if (mapping) {
|
||||
const room = rooms.get(mapping.roomId);
|
||||
if (room) room.lastActivity = Date.now();
|
||||
if (room) {
|
||||
room.lastActivity = Date.now();
|
||||
// Update metadata if it's a peer_status (heartbeat)
|
||||
if (eventName === EVENTS.PEER_STATUS && data.tabTitle) {
|
||||
room.peerData.set(socket.id, { peerId: mapping.peerId, tabTitle: data.tabTitle });
|
||||
}
|
||||
}
|
||||
socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId });
|
||||
}
|
||||
});
|
||||
@@ -240,7 +248,8 @@ io.on('connection', (socket) => {
|
||||
socket.on(EVENTS.GET_ROOMS, () => {
|
||||
const list = Array.from(rooms.entries()).map(([id, r]) => ({
|
||||
id,
|
||||
peerCount: r.peers.size
|
||||
peerCount: r.peers.size,
|
||||
hasPassword: !!r.passwordHash
|
||||
}));
|
||||
socket.emit(EVENTS.ROOM_LIST, { rooms: list });
|
||||
});
|
||||
@@ -254,6 +263,7 @@ io.on('connection', (socket) => {
|
||||
if (room) {
|
||||
room.peers.delete(socket.id);
|
||||
room.peerIds.delete(socket.id);
|
||||
room.peerData.delete(socket.id);
|
||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
||||
if (room.peers.size === 0) {
|
||||
rooms.delete(roomId);
|
||||
@@ -273,6 +283,7 @@ io.on('connection', (socket) => {
|
||||
if (room) {
|
||||
room.peers.delete(socket.id);
|
||||
room.peerIds.delete(socket.id);
|
||||
room.peerData.delete(socket.id);
|
||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
||||
if (room.peers.size === 0) {
|
||||
rooms.delete(roomId);
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ export const APP_VERSION = "1.0.0";
|
||||
|
||||
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
|
||||
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';
|
||||
export const OFFICIAL_SERVER_TOKEN = 'koala_secure_access_2026';
|
||||
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
|
||||
|
||||
export const EVENTS = {
|
||||
// Connection & Room
|
||||
|
||||
Reference in New Issue
Block a user