Alt+Shift+R — toggle reveal mode (fake→real in responses) Alt+Shift+S — toggle Silent Send on/off Badge flashes "EYE" (blue) when reveal activates, "ON"/"OFF" (green/red) when toggling enabled state. Users can remap these in chrome://extensions/shortcuts or Firefox about:addons. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
237 lines
6.6 KiB
JavaScript
237 lines
6.6 KiB
JavaScript
/**
|
|
* Silent Send - Background Service Worker
|
|
*
|
|
* Manages badge count, coordinates between popup and content scripts.
|
|
* Injects content scripts on custom domains dynamically.
|
|
* Uses `api` alias for cross-browser compatibility (Chrome + Firefox).
|
|
*/
|
|
|
|
import Storage from '../lib/storage.js';
|
|
import api from '../lib/browser-polyfill.js';
|
|
|
|
// Track substitution counts per tab
|
|
const tabCounts = new Map();
|
|
|
|
// Built-in URL patterns
|
|
const BUILTIN_URL_PATTERNS = [
|
|
'https://claude.ai/*',
|
|
'https://chatgpt.com/*',
|
|
'https://chat.openai.com/*',
|
|
'https://grok.x.ai/*',
|
|
'https://x.com/i/grok*',
|
|
'https://gemini.google.com/*',
|
|
'http://localhost/*',
|
|
'http://127.0.0.1/*',
|
|
];
|
|
|
|
// --- Badge Management ---
|
|
|
|
function updateBadge(tabId) {
|
|
const count = tabCounts.get(tabId) || 0;
|
|
const text = count > 0 ? String(count) : '';
|
|
|
|
api.action.setBadgeText({ text, tabId });
|
|
api.action.setBadgeBackgroundColor({ color: count > 0 ? '#10b981' : '#6b7280', tabId });
|
|
}
|
|
|
|
// Reset count when tab navigates
|
|
api.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
if (changeInfo.status === 'loading') {
|
|
tabCounts.set(tabId, 0);
|
|
updateBadge(tabId);
|
|
}
|
|
|
|
// Inject content script on custom domains when page loads
|
|
if (changeInfo.status === 'complete' && tab.url) {
|
|
await injectOnCustomDomain(tabId, tab.url);
|
|
}
|
|
});
|
|
|
|
// Cleanup when tab closes
|
|
api.tabs.onRemoved.addListener((tabId) => {
|
|
tabCounts.delete(tabId);
|
|
});
|
|
|
|
// --- Dynamic injection for custom domains ---
|
|
|
|
async function injectOnCustomDomain(tabId, tabUrl) {
|
|
const settings = await Storage.getSettings();
|
|
const customDomains = settings.customDomains || [];
|
|
if (customDomains.length === 0) return;
|
|
|
|
const matches = customDomains.some((domain) => tabUrl.startsWith(domain));
|
|
if (!matches) return;
|
|
|
|
// Check if already injected (avoid double-injection)
|
|
try {
|
|
const results = await api.scripting.executeScript({
|
|
target: { tabId },
|
|
func: () => !!window.__silentSendInjected,
|
|
});
|
|
if (results?.[0]?.result) return;
|
|
} catch (e) {
|
|
// Permission denied — user hasn't granted access to this domain
|
|
return;
|
|
}
|
|
|
|
// Inject CSS
|
|
try {
|
|
await api.scripting.insertCSS({
|
|
target: { tabId },
|
|
files: ['src/content/content.css'],
|
|
});
|
|
} catch (e) { /* non-fatal */ }
|
|
|
|
// Inject content script
|
|
try {
|
|
await api.scripting.executeScript({
|
|
target: { tabId },
|
|
files: ['src/content/injector.js'],
|
|
});
|
|
} catch (e) {
|
|
console.warn('[Silent Send] Failed to inject on custom domain:', e);
|
|
}
|
|
}
|
|
|
|
// --- Message Handling ---
|
|
|
|
api.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
const handler = messageHandlers[message.type];
|
|
if (handler) {
|
|
handler(message, sender, sendResponse);
|
|
return true; // async response
|
|
}
|
|
});
|
|
|
|
const messageHandlers = {
|
|
async 'substitution:performed'(message, sender) {
|
|
const tabId = sender.tab?.id;
|
|
if (tabId == null) return;
|
|
|
|
const current = tabCounts.get(tabId) || 0;
|
|
tabCounts.set(tabId, current + message.count);
|
|
updateBadge(tabId);
|
|
|
|
// Log each replacement
|
|
for (const replacement of message.replacements) {
|
|
await Storage.addLogEntry({
|
|
type: 'substitution',
|
|
direction: 'outbound',
|
|
original: replacement.original,
|
|
replaced: replacement.replaced,
|
|
category: replacement.category,
|
|
url: sender.tab?.url || '',
|
|
});
|
|
}
|
|
},
|
|
|
|
async 'get:mappings'(_message, _sender, sendResponse) {
|
|
const mappings = await Storage.getMappings();
|
|
sendResponse({ mappings });
|
|
},
|
|
|
|
async 'get:settings'(_message, _sender, sendResponse) {
|
|
const settings = await Storage.getSettings();
|
|
sendResponse({ settings });
|
|
},
|
|
|
|
async 'get:log'(_message, _sender, sendResponse) {
|
|
const log = await Storage.getLog();
|
|
sendResponse({ log });
|
|
},
|
|
|
|
async 'get:tab-count'(message, sender, sendResponse) {
|
|
const tabId = message.tabId || sender.tab?.id;
|
|
sendResponse({ count: tabCounts.get(tabId) || 0 });
|
|
},
|
|
|
|
async 'update:settings'(message) {
|
|
await Storage.saveSettings(message.settings);
|
|
|
|
// Build list of all URL patterns (built-in + custom)
|
|
const allPatterns = [...BUILTIN_URL_PATTERNS];
|
|
const customDomains = message.settings.customDomains || [];
|
|
for (const domain of customDomains) {
|
|
allPatterns.push(domain + '/*');
|
|
}
|
|
|
|
// Broadcast to content scripts
|
|
for (const urlPattern of allPatterns) {
|
|
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
|
|
for (const tab of tabs) {
|
|
api.tabs.sendMessage(tab.id, {
|
|
type: 'settings:updated',
|
|
settings: message.settings,
|
|
}).catch(() => {});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
// --- Keyboard Shortcuts ---
|
|
api.commands.onCommand.addListener(async (command) => {
|
|
const settings = await Storage.getSettings();
|
|
|
|
if (command === 'toggle-reveal') {
|
|
settings.revealMode = !settings.revealMode;
|
|
await Storage.saveSettings({ revealMode: settings.revealMode });
|
|
|
|
// Broadcast to all tabs
|
|
broadcastSettings(settings);
|
|
|
|
// Show brief badge text
|
|
const [tab] = await api.tabs.query({ active: true, currentWindow: true });
|
|
if (tab) {
|
|
api.action.setBadgeText({ text: settings.revealMode ? 'EYE' : '', tabId: tab.id });
|
|
api.action.setBadgeBackgroundColor({
|
|
color: settings.revealMode ? '#1d4ed8' : '#6b7280',
|
|
tabId: tab.id,
|
|
});
|
|
if (!settings.revealMode) {
|
|
// Restore normal badge after a moment
|
|
setTimeout(() => updateBadge(tab.id), 1500);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (command === 'toggle-enabled') {
|
|
settings.enabled = !settings.enabled;
|
|
await Storage.saveSettings({ enabled: settings.enabled });
|
|
|
|
broadcastSettings(settings);
|
|
|
|
// Flash badge
|
|
const [tab] = await api.tabs.query({ active: true, currentWindow: true });
|
|
if (tab) {
|
|
api.action.setBadgeText({ text: settings.enabled ? 'ON' : 'OFF', tabId: tab.id });
|
|
api.action.setBadgeBackgroundColor({
|
|
color: settings.enabled ? '#10b981' : '#dc2626',
|
|
tabId: tab.id,
|
|
});
|
|
setTimeout(() => updateBadge(tab.id), 1500);
|
|
}
|
|
}
|
|
});
|
|
|
|
async function broadcastSettings(settings) {
|
|
const allPatterns = [...BUILTIN_URL_PATTERNS];
|
|
const customDomains = settings.customDomains || [];
|
|
for (const domain of customDomains) {
|
|
allPatterns.push(domain + '/*');
|
|
}
|
|
for (const urlPattern of allPatterns) {
|
|
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
|
|
for (const tab of tabs) {
|
|
api.tabs.sendMessage(tab.id, {
|
|
type: 'settings:updated',
|
|
settings,
|
|
}).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Set initial badge state ---
|
|
api.runtime.onInstalled.addListener(() => {
|
|
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
|
|
});
|