initial commit/backup

This commit is contained in:
2024-11-01 20:55:18 -04:00
commit bc53ce53b1
39 changed files with 10456 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
secrets/
cache/
node_modules/
temp.*
*.rdb
*.log
logs/
temp/
tmp/
+163
View File
@@ -0,0 +1,163 @@
/* general styles */
body {
background-color: #181818; /* darker background */
color: #E0E0E0; /* light text for readability */
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
margin: auto;
}
a {
color: #9BBFFF; /* more muted blue links */
text-decoration: none;
}
a:hover {
color: #74A0FF; /* slightly lighter blue on hover */
}
/* header */
#header {
background-color: #212121; /* consistent dark background */
padding: 15px 0;
}
h1 {
color: #D9534F; /* softer red for DuckDuckGo title */
font-size: 24px;
text-align: center;
margin: 0;
}
/* search bar */
form {
/* display: flex; */
justify-content: center;
padding: 20px 0;
}
input[type="text"] {
width: 50%;
padding: 10px;
border: none;
border-radius: 5px;
background-color: #2C2C2C;
color: #E0E0E0;
font-size: 16px;
margin-right: 10px;
}
input[type="submit"] {
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #74A0FF;
color: white;
font-size: 16px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #578AFF;
}
/* search results */
#results {
padding: 20px;
}
.result {
padding: 15px;
margin-bottom: 15px; /* more spacing between results */
background-color: #222;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
}
.result:hover {
background-color: #333;
}
.result a {
font-size: 18px;
color: #9BBFFF;
}
.result a:hover {
color: #74A0FF;
}
.result .snippet {
color: #B0B0B0;
font-size: 14px;
margin-top: 5px;
}
/* buttons */
button, input[type="submit"] {
background-color: #505CFF;
color: white;
border-radius: 5px;
}
button:hover {
background-color: #4266F5;
}
#footer {
background-color: #212121;
color: #777;
text-align: center;
padding: 20px 0;
margin-top: 20px;
}
#footer a {
color: #9BBFFF;
}
#footer a:hover {
color: #74A0FF;
}
/* fine-tune form elements */
input[type="text"]::placeholder {
color: #999; /* dimmed placeholder */
}
input[type="text"]:focus {
outline: none;
background-color: #333;
}
input[type="submit"]:focus {
outline: none;
}
.link-text {
color: lightblue;
}
/* scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
/* mobile responsiveness */
@media (max-width: 768px) {
input[type="text"] {
width: 80%;
}
}
+37
View File
@@ -0,0 +1,37 @@
#historybar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: 20%;
min-width: 100px;
margin-left: -100%; /* initially off-screen */
transition: margin-left 0.5s ease; /* smooth slide-in/out */
background-color: rgba(100, 100, 100, 0.6) !important; /* translucent black background */
overflow-x: hidden;
overflow-y: scroll;
padding: 10px 2px;
padding-left: 5px;
z-index: 99999999999999999999999999999999999999999999999; /* fuck you in particular howtogeek and your goddamn sidebar */
font-size: medium !important;
color: lightblue !important;
}
#historybar table {
border-collapse: separate; /* keep cells separate */
border-spacing: 0 10px;
width: 100%;
}
#historybar table td {
cursor: pointer;
/* padding: 5px; */
border-bottom: 1px solid rgba(100, 100, 100, 0.8);
}
#historybar table td:hover {
background-color: rgba(100, 100, 100, 0.8);;
}
+22
View File
@@ -0,0 +1,22 @@
/* style.css */
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
}
h1 {
color: #333;
}
webview {
display: none;
}
.open-webview {
display: block;
}
#tabwebview {
display: block !important;
}
+61
View File
@@ -0,0 +1,61 @@
html {
overflow-y: hidden;
}
.tabs-container {
display: flex;
align-items: center;
border-bottom: 1px solid #ccc;
overflow-y: hidden; /* no y overflow */
position: relative;
}
.browser-tabs {
display: flex;
flex-grow: 1;
overflow-y: hidden;
overflow-x: auto; /* allow horizontal scrolling if needed */
scrollbar-width: thin; /* Firefox-specific scrollbar width */
margin-right: 10px;
}
.tab {
padding: 10px 15px;
border: none;
background-color: #f0f0f0;
cursor: pointer;
margin-right: 5px;
transition: background-color 0.3s;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
white-space: nowrap; /* keep text on a single line */
}
.tab:hover,
.tab.active {
background-color: #ddd;
}
.add-tab {
padding: 7px 12px;
border: none;
background-color: #4b4b6b; /* dark purple-gray background */
color: #ddd; /* light gray text color */
cursor: pointer;
font-size: 16px;
border-radius: 5px;
white-space: nowrap;
display: inline-flex;
align-items: center;
margin-left: auto;
transition: background-color 0.3s, color 0.3s;
}
.add-tab:hover {
background-color: #3b3b5a; /* slightly darker shade for hover */
color: #fff; /* lighter color on hover */
}
button {
cursor: pointer;
}
+149
View File
@@ -0,0 +1,149 @@
/* general background and text */
body,
#page-manager,
ytd-app {
background-color: #11111b !important;
/* dark purpleish black */
color: #d1d1e9 !important;
/* soft light purple text */
}
/* video background */
#player-container,
#movie_player,
.html5-video-player {
background-color: #0f0f17 !important;
/* darker background for video */
}
/* sidebar and primary navigation */
#container,
ytd-guide-entry-renderer,
ytd-mini-guide-renderer {
background-color: #181828 !important;
/* deep dark purple */
}
#sections,
ytd-guide-renderer {
background-color: #1b1b2f !important;
/* matches body */
}
/* header and search bar */
#masthead-container {
background-color: #2a2a4d !important;
/* dark purplish shade */
}
#search-icon-legacy {
color: #d1d1e9 !important;
}
/* video title and descriptions */
#video-title,
.ytp-chrome-top,
.ytp-title-text {
color: #d1d1e9 !important;
}
/* video progress bar */
.ytp-play-progress {
background-color: #6b5b95 !important;
/* purplish progress bar */
}
.ytp-scrubber-button {
background-color: #b794f4 !important;
/* purplish scrubber */
}
/* links and video titles */
a,
.yt-simple-endpoint {
color: #b794f4 !important;
/* purplish links */
}
a:hover {
color: #d1d1e9 !important;
/* lighter on hover */
}
/* comments section */
ytd-comment-thread-renderer {
background-color: #181828 !important;
/* dark purpleish black */
border: 1px solid #262646 !important;
}
ytd-comment-renderer #content-text {
color: #d1d1e9 !important;
}
/* misc elements */
#related,
#items,
#panels {
background-color: #1b1b2f !important;
}
/* subscribe button */
ytd-subscribe-button-renderer {
background-color: #6b5b95 !important;
color: #ffffff !important;
}
ytd-subscribe-button-renderer:hover {
background-color: #9d7bcf !important;
}
#search-form {
background: transparent;
}
#voice-search-button .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--text {
background-color: red;
}
#voice-search-button .yt-spec-button-shape-next--mono.yt-spec-button-shape-next--text:hover {
background-color: coral;
}
#search-clear-button {
background-color: transparent !important;
}
ytd-button-renderer {
background: transparent;
}
.yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response:hover {
background-color: rgba(255, 255, 255, 0.596);
}
yt-icon.ytd-logo {
color: inherit;
background-color: inherit;
}
.guide-icon.ytd-guide-entry-renderer {
color: white;
color: white;
}
html[darker-dark-theme], [darker-dark-theme] [light] {
--yt-spec-text-primary: white;
--ytd-searchbox-legacy-button-color: grey;
--yt-spec-raised-background: rgb(20, 70, 37);
--yt-spec-text-secondary: offwhite;
--ytd-searchbox-text-color: white;
--yt-spec-icon-active-other: white;;
--yt-spec-icon-inactive: darkgrey;
--yt-spec-icon-disabled: darkgrey;
--yt-spec-brand-icon-active: white;
--yt-spec-brand-icon-inactive: grey;
--yt-button-icon-button-text-color: black;
}
+7
View File
@@ -0,0 +1,7 @@
FROM ubuntu:22.04
RUN echo "TODO: IMPLEMENT CONTAINERIZATION!!!"
# RUN apt-get update && apt-get install neovim git nodejs curl lsb-core -y
# RUN curl https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh > installer.sh
CMD ["bin/bash"]
+22
View File
@@ -0,0 +1,22 @@
<!-- index.html -->
<!DOCTYPE html>
<html lang="en" style="height: 100%; width: 100%;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Electron App</title>
<link rel="stylesheet" href="../CSS/style.css">
</head>
<body style="width: 100%; height: 100%; margin: 0; padding: 0;">
<webview id="tabwebview" src="../HTML/tabs.html" style="width: auto; height: 35px; margin: 0; padding: 0; border: none;" nodeintegration preload="../organization/tabs.cjs"></webview>
<webview id="webview-0" src="https://duckduckgo.com" style="width: 100%; height: calc(100% - 40px);;" preload="JS/preload.cjs"
partition="persist:myPartition"
webpreferences="nodeIntegration=1, contextIsolation=1, javascript=1, plugins=1, enableBlinkFeatures=WebContentsForceDark"
class="open-webview">
</webview>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ION Browser Metrics</title>
<script src="metrics.js"></script>
</head>
<body>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>No Internet Connection</title>
<style>
/* dark mode background and text styling */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #121212;
color: #eeeeee;
}
/* container styling and subtle shadow effect */
.container {
text-align: center;
background: #1e1e1e;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
animation: fadeIn 1.5s ease-out;
}
/* header styling with animation */
h1 {
font-size: 3em;
margin-bottom: 0.3em;
animation: bounceIn 1.5s ease-out;
}
/* paragraph styling and margin adjustments */
p {
font-size: 1.2em;
margin-top: 0.3em;
}
/* keyframes for animations */
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounceIn {
0%,
20%,
40%,
60%,
80%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* button styling for retry */
.retry-button {
margin-top: 1.5em;
padding: 0.7em 1.5em;
font-size: 1em;
color: #121212;
background-color: #eeeeee;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
/* hover effect for retry button */
.retry-button:hover {
background-color: #f1c40f;
color: #1e1e1e;
}
</style>
</head>
<body>
<div class="container">
<h1>Connection Offline</h1>
<p>It looks like you are not connected to the internet. Please check your connection and try again.</p>
<button class="retry-button" onclick="window.location.reload()">Retry</button>
</div>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" style="margin: 0; padding: 0; height: min-content;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../CSS/tabs.css">
</head>
<body style="margin: 0; padding: 0; height: fit-content">
<div class="tabs-container">
<div class="browser-tabs">
<button class="tab" data-tab="0">Tab 0</button>
</div>
<button class="add-tab">+</button>
</div>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
// save the original window.open function
const originalWindowOpen = window.open;
// override the window.open function
window.open = function (url, target, features) {
console.log('A new window is attempting to open:');
console.log('URL:', url);
console.log('Target:', target);
console.log('Features:', features);
window.electronAPI.checkperms(window.location.hostname);
// call the original window.open function if you want the popup to proceed
return originalWindowOpen.call(window, url, target, features);
};
+12
View File
@@ -0,0 +1,12 @@
/**
* @param { @param {Electron.BrowserWindow} window} window
*/
export async function changeZoom(window, zoomIn = true, reset = false) {
let zl = window.webContents.getZoomLevel();
if (reset) zl = 0
else if (zoomIn) zl++;
else zl--;
window.webContents.setZoomLevel(zl);
}
+64
View File
@@ -0,0 +1,64 @@
async function waitForVideo() {
return new Promise(resolve => {
const i = setInterval(() => {
if (document.querySelector('video')) {
clearInterval(i);
resolve(true);
}
}, 500)
});
}
async function revertQuality() {
setQuality(document?.body?.dataset?.oldel || 'auto');
delete document?.body?.dataset?.oldel;
}
async function setQuality(quality = 'lowest') {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await waitForVideo();
console.info(`changing quality to ${quality}`);
const qualityMenuMain = [document.querySelector('.ytp-settings-menu'), document.querySelector('.ytp-panel-menu')],
settingsButton = document.querySelector('.ytp-settings-button');
qualityMenuMain.map(el => el.style.opacity = '0');
settingsButton.click();
await sleep(500);
const qualityMenu = document.querySelector('.ytp-panel-menu').lastElementChild;
qualityMenu.click();
await sleep(500);
const qualityOptions = [...document.querySelector('.ytp-panel.ytp-quality-menu').querySelectorAll('.ytp-menuitem')];
let selection;
if (quality === 'lowest') selection = qualityOptions.findLast(el => (!el.textContent.toLowerCase().includes('auto')));
else selection = qualityOptions.find((el) => el.textContent.toLowerCase().includes(quality));
const currentQuality = qualityOptions.find(el => el.ariaChecked);
if (currentQuality) document.body.dataset.oldel = currentQuality.textContent.toLowerCase();
if (!selection) {
let qualityTexts = qualityOptions.map((el) => el.textContent).join('\n');
console.info('"' + quality + '" not found. Options are: \n\nHighest\n' + qualityTexts + '\n' + 'setting to auto');
settingsButton.click(); // click the menu button to close it
selection = qualityOptions.findLast(el => (el.textContent.toLowerCase().includes('auto')));
}
selection.click();
qualityMenuMain.map(el => el.style.opacity = '1');
}
function optimize() {
sessionStorage.setItem('ran-optimize', 1);
console.info('quality tuning script loaded!');
// setQuality('Highest');
}
if (document.readyState === 'complete' && !sessionStorage.getItem('ran-optimize')) optimize();
+104
View File
@@ -0,0 +1,104 @@
const { contextBridge, ipcRenderer } = require('electron');
// renderer.js or script loaded in the renderer process
const policy = window.trustedTypes.createPolicy('default', {
createHTML: (input) => input, // policy allows only sanitized HTML strings
});
process.once("loaded", () => {
console.info('injecting...');
contextBridge.exposeInMainWorld('safeHTML', {
ping: () => console.info('pong'),
write: (selector, htmlString) => {
const element = document.querySelector(selector);
if (element) element.innerHTML += policy.createHTML(htmlString);
},
insertBefore: (selector, htmlString) => {
const element = document.querySelector(selector);
if (element) element.innerHTML = policy.createHTML(htmlString) + element.innerHTML;
},
addScript: (content, src) => {
// validate and securely add the script to the document's head
const head = document.head;
if (!head || (!src && !content)) return;
// create a script element
const script = document.createElement('script');
if (content) script.innerText = content;
else script.src = src;
// set script properties securely
script.async = true;
// append to head
head.appendChild(script);
},
addStylesheet: (href, inlineContent) => {
const head = document.head;
if (!head) return;
if (href) {
// if href is provided, use it for external stylesheet
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
head.appendChild(link);
} else if (inlineContent) {
// create a style element for inline CSS
const style = document.createElement('style');
style.textContent = inlineContent;
head.appendChild(style);
}
}
});
});
// TODO: replaceme
const uid = 1;
contextBridge.exposeInMainWorld('electronAPI', {
displayHistory: () => ipcRenderer.send('display-history', uid), // send message to main process
getHistory: () => ipcRenderer.invoke('get-history', uid), // request history data and wait for response
showHistory: (history) => ipcRenderer.send('show-history', history), // sends data to the main process
initTabs: () => ipcRenderer.send('init-tabs'),
addTab: (url) => window.dispatchEvent(new CustomEvent('add-tab', { detail: url })),
sendToMain: (channel, data) => {
ipcRenderer.send(channel, data);
},
onReceive: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
checkperms: (sitehostname) => ipcRenderer.send('get-site-perms', sitehostname)
});
// ipcRenderer.on('tab-opened', (ev, id) => {
// document.querySelector('.open-webview')?.classList.remove('open-webview');
// document.querySelector(`#webview-${id}`)?.classList.add('open-webview');
// });
ipcRenderer.on('tab-created', (ev, id, url = 'https://duckduckgo.com') => createWebview(url, id));
contextBridge.exposeInMainWorld('tabAPI', {
ping: () => console.info('pong'),
addTab: (url) => ipcRenderer.send('add-tab', url || 'about:blank')
});
const load = () => {
console.info("PRELOAD LOADED!");
if (window.location.origin === 'lite.duckduckgo.com') {
document.body.querySelector('img[src="//duckduckgo.com/t/sl_l"]').remove();
}
if (document.body) {
// document.body.innerHTML = `<webview src="../HTML/tabs.html" style="flex:0 0.5 auto;" nodeintegration preload="../organization/tabs.cjs"></webview>` + document.body.innerHTML;
ipcRenderer.send('init-tabs');
}
}
// document.onload = () => load;
document.addEventListener('DOMContentLoaded', load);
+100
View File
@@ -0,0 +1,100 @@
function runinit() {
console.info("added renderer script!");
sessionStorage.setItem('ran-renderer', 1);
forceDarkMode();
}
var removehbar = removehbar || function (e) {
const hbar = document.querySelector('#historybar');
if (!hbar || hbar.contains(e?.target)) return;
// slide out by setting margin-left to -100%
hbar.style.marginLeft = '-100%';
setTimeout(() => hbar.remove(), 500);
document.removeEventListener('click', removehbar);
}
function showHistory(...h) {
if (Array.isArray(h[0])) h = h.flat(1);
const hbar = document.querySelector('#historybar');
if (hbar) return removehbar();
setTimeout(() => document.addEventListener('click', removehbar), 1000);
const sidebar = document.createElement('div');
sidebar.id = 'historybar';
sidebar.style.marginLeft = '-100%'; // start off-screen
const t = document.createElement('table'),
tbody = document.createElement('tbody');
t.appendChild(tbody);
h.forEach(iraw => {
const i = (typeof iraw === 'string') ? JSON.parse(iraw) : iraw,
el = document.createElement('tr'),
el2 = document.createElement('td'),
a = document.createElement('a');
a.textContent = i.title;
a.href = i.query;
a.style.width = '100%';
a.style.height = '100%';
el2.appendChild(a);
el.appendChild(el2);
tbody.appendChild(el);
});
sidebar.appendChild(t);
document.body.appendChild(sidebar);
// force reflow to apply the transition correctly
window.getComputedStyle(sidebar).marginLeft;
// slide in by setting margin-left to 0
sidebar.style.marginLeft = '0';
}
function forceDarkMode() {
document.querySelector('[value="night"]')?.click(); // wikipedia
// manual
// function to convert rgb values to a hex string
const rgbToHex = (r, g, b) => {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}
// function to get the computed background and text color of the body
const checkColors = () => {
const body = document.querySelector('body') || document.querySelector('main') // select the body element
const bgColor = window.getComputedStyle(body).backgroundColor // get background color
const textColor = window.getComputedStyle(body).color // get text color
// parse the rgb values from the background and text color
const bgMatch = bgColor.match(/\d+/g)
const textMatch = textColor.match(/\d+/g)
// if both colors are in rgb format
if (bgMatch && textMatch) {
const bgHex = rgbToHex(parseInt(bgMatch[0]), parseInt(bgMatch[1]), parseInt(bgMatch[2]))
const textHex = rgbToHex(parseInt(textMatch[0]), parseInt(textMatch[1]), parseInt(textMatch[2]))
// check if background is white (#FFFFFF) and text is dark (let's assume below #777777 as dark)
if (bgHex === "#FFFFFF" && textHex <= "#777777") {
// swap the background to dark and text to light
body.style.backgroundColor = "#000000" // set background to black
body.style.color = "#FFFFFF" // set text color to white
}
}
}
checkColors();
}
document.addEventListener('DOMContentLoaded', runinit);
if (document.readyState === 'complete' && !sessionStorage.getItem('ran-renderer')) runinit();
+144
View File
@@ -0,0 +1,144 @@
// main.js
import { app, BaseWindow, BrowserWindow, session, pushNotifications } from 'electron';
import { exec } from 'child_process';
import path from 'path';
import {
logger, intercept, setUpShortcuts, organizeTabIds,
getSavedTabs, loadTabs, flushCookies, askUserQuestion,
ipcinit, checkInternetConnectivity, findPath, createWebview,
handleWebViewInit, setupRedis, quitRedis
} from './serverJS/imports.js';
await setupRedis();
export const uid = 1,
partitionName = 'persist:default',
agent = 'Chrome';
// ensuring the userData directory is set
app.setPath('userData', path.join(app.getPath('home'), '.ion-browser-data'));
app.commandLine.appendSwitch('load-extension');
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('ionbrowser', process.execPath, [path.resolve(process.argv[1])]);
}
} else {
app.setAsDefaultProtocolClient('ionbrowser');
}
async function createWindow(customSession) {
organizeTabIds();
const mainWindow = new BaseWindow({
width: 800,
height: 600,
// backgroundColor: 'black',
darkTheme: true,
autoHideMenuBar: true,
nodeIntegrationInWorker: true,
});
const tabWebView = await createWebview(-1, mainWindow, customSession, 'tabs.cjs'),
mainWebView = await createWebview(0, mainWindow, customSession);
if (!(await checkInternetConnectivity())) return mainWebView.webContents.loadFile(await findPath('nointernet.html'));
// load existing cookies
logger.info('flushing cookies...');
await flushCookies(customSession);
logger.info('cookies flushed!');
// need to load the initial window
await mainWebView.webContents.loadURL('https://start.duckduckgo.com');
// tab caching stuff
const tabs = await getSavedTabs();
if (Object.keys(tabs).length) {
const r = await askUserQuestion(mainWindow, 'Restore History', `Would you like to restore ${Object.keys(tabs).length} previous tabs?`);
if (r) loadTabs(customSession, tabs);
}
else {
// mainWebView.webContents('https://duckduckgo.com/?t=h_&hps=1&start=1&q=hi&ia=web');
mainWebView.webContents.loadURL('https://www.youtube.com/watch?v=aPO5JaShu2U', { userAgent: agent });
// mainWebView.webContents.loadURL('https://www.youtube.com', { userAgent: agent });
// mainWebView.webContents.loadURL('https://electronjs.org');
mainWebView.webContents.setBackgroundThrottling(true);
mainWindow.currentView = mainWebView;
}
tabWebView.webContents.loadFile(await findPath('tabs.html'));
// tabWebView.webContents.openDevTools({ mode: 'detach' });
}
app.on('open-url', (e, webURL) => {
e.preventDefault();
console.log(`attempted to navigate to ${webURL}`);
});
// listen for app ready event to create window
app.whenReady().then(async () => {
const customSession = session.fromPartition(partitionName);
customSession.setPermissionRequestHandler((webContents, permission, callback) => {
// handle third-party cookies
if (permission === 'media' || permission === 'display-capture' || permission === 'notifications' || permission === 'fullscreen') {
callback(true);
}
else callback(false);
});
// session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
// if (details.url.startsWith('https://duckduckgo.com/l/?')) cb({ redirectURL: transformduckurl(details.url) });
// else cb({ cancel: false });
// })
customSession.protocol.handle('file', (r) => intercept(r, uid));
customSession.protocol.handle('http', (r) => intercept(r, uid));
customSession.protocol.handle('https', (r) => intercept(r, uid));
ipcinit(customSession);
createWindow(customSession);
// for macOS: recreate a window if the dock icon is clicked and there are no open windows
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// app.on('browser-window-created', (_, window) => setUpShortcuts(uid, window));
app.on('web-contents-created', async (e, contents) => {
// contents.openDevTools({ mode: 'detach' });
setUpShortcuts(uid);
const u = await contents.executeJavaScript('window.location.href');
if (contents.getType() === 'webview') handleWebViewInit(contents);
});
// quit the app when all windows are closed (except on macOS)
app.on('window-all-closed', () => {
logger.info('all windows closed!');
if (process.platform !== 'darwin') {
quitRedis();
app.quit();
}
});
app.on('before-quit', async (e) => {
e.preventDefault();
logger.info('shutting down...');
const p = exec('node utils/clearCache.js --ischildproc 1>> logs/log.log 2>> logs/err.log');
p.unref();
setTimeout(() => {
app.quit();
process.exit(0);
}, 1500);
quitRedis();
});
app.commandLine.appendSwitch('ignore-gpu-blacklist');
app.commandLine.appendSwitch('disable-gpu-compositing');
+57
View File
@@ -0,0 +1,57 @@
const { contextBridge, ipcRenderer } = require('electron');
const tabClick = (tab, tabsContainer) => {
tab.addEventListener('click', () => {
if (tab.classList.contains('active')) return;
const tabs = tabsContainer.querySelectorAll('.tab');
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
ipcRenderer.send('tab-open', Number(tab.dataset.tab));
});
}
function setup() {
ipcRenderer.send('ping');
const tabsContainer = document.querySelector('.browser-tabs'),
tabs = document.querySelectorAll('.tab'),
addTabButton = document.querySelector('.add-tab');
tabs.forEach(tab => tabClick(tab, tabsContainer));
addTabButton.addEventListener('click', () => {
// create a new tab element
const newTab = document.createElement('button');
newTab.classList.add('tab');
newTab.dataset.tab = `${tabsContainer.children.length}`;
newTab.textContent = `Tab ${tabsContainer.children.length}`;
tabsContainer.appendChild(newTab);
newTab.scrollIntoView({ behavior: 'smooth' });
ipcRenderer.send('tab-new', tabsContainer.children.length - 1);
// add click event listener to the new tab
newTab.addEventListener('click', () => tabClick(newTab, tabsContainer));
});
}
process.once("loaded", () => {
contextBridge.exposeInMainWorld('tabapi', {
ping: () => ipcRenderer.send('ping'),
click: () => ipcRenderer.invoke('tab-open'),
close: () => ipcRenderer.invoke('tab-close'),
newtab: () => ipcRenderer.invoke('tab-new')
});
if (document.readyState === 'complete') setup();
else document.addEventListener('DOMContentLoaded', () => setup());
ipcRenderer.on('pong', () => console.info('the server replied with pong'));
});
// document.addEventListener('click', () => ipcRenderer.send('ping'));
+8049
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"dependencies": {
"@atproto/api": "^0.13.12",
"@gorhill/ubo-core": "^0.1.30",
"cron": "^3.1.7",
"dompurify": "^3.1.7",
"electron-oauth-helper": "^5.1.1",
"electron-oauth2": "^3.0.0",
"express": "^4.21.1",
"googleapis": "^144.0.0",
"jquery": "^3.7.1",
"jsdom": "^25.0.1",
"mhtml2html": "^3.0.0",
"node-cache": "^5.1.2",
"open": "^10.1.0",
"redis": "^4.7.0",
"winston": "^3.15.0"
},
"build": {
"appId": "ion.browser",
"linux": {}
},
"preload": {
"js": "./JS/preload.cjs"
},
"name": "ion-browser",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron . --trace-warnings --no-sandbox --load-extension",
"pack": "electron-builder --dir",
"dist": "electron-builder"
},
"author": "",
"license": "ISC",
"description": "the ion browser!",
"type": "module",
"devDependencies": {
"electron": "^33.0.1",
"electron-builder": "^25.1.8"
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Agent, CredentialSession } from '@atproto/api';
import { CronJob } from 'cron';
import json from '../secrets/config.json' with { type: 'json' };
const { uname, upass } = json.bluesky;
// Create a Bluesky Agent
const session = new CredentialSession(new URL('https://bsky.social'));
const agent = new Agent(session);
async function main() {
await session.login({ identifier: uname, password: upass });
const { data: accountData } = await agent.getProfile({ actor: session.did });
console.log(accountData);
}
main();
+78
View File
@@ -0,0 +1,78 @@
import { StaticNetFilteringEngine } from '@gorhill/ubo-core';
import fs from 'fs/promises';
import { checkInternetConnectivity } from '../utils/misc.js';
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
const blocklists = [
"https://ublockorigin.github.io/uAssetsCDN/filters/badlists.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/filters.min.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/privacy.min.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/badware.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/quick-fixes.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/unbreak.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/annoyances.txt",
"https://ublockorigin.github.io/uAssetsCDN/filters/lan-block.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easylist.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easyprivacy.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easylist-annoyances.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easylist-cookies.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easylist-newsletters.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easylist-notifications.txt",
"https://ublockorigin.github.io/uAssetsCDN/thirdparties/easylist-social.txt",
"https://raw.githubusercontent.com/laylavish/uBlockOrigin-HUGE-AI-Blocklist/main/list.txt"
];
async function fetchList(url) {
return fetch(url).then(r => {
return r.text();
}).then(raw => {
return { raw };
}).catch(reason => {
logger.error(reason);
});
}
const snfe = await StaticNetFilteringEngine.create();
// const rsf = await fetch('https://api.github.com/repos/uBlockOrigin/uAssets/contents/filters'),
// safeLists = (await rsf.json()).map(o => o.download_url);
const pathToSelfie = 'cache/selfie.txt';
// Up to date serialization data (aka selfie) available?
let selfie;
const ageInDays = await fs.stat(pathToSelfie).then(stat => {
const fileDate = new Date(stat.mtime);
return (Date.now() - fileDate.getTime()) / (7 * 24 * 60 * 60);
}).catch(() => Number.MAX_SAFE_INTEGER);
// Use a selfie if available and not older than 7 days
if (ageInDays <= 7) {
selfie = await fs.readFile(pathToSelfie, { encoding: 'utf8' })
.then(data => typeof data === 'string' && data !== '' && data)
.catch(() => { });
if (typeof selfie === 'string') {
await snfe.deserialize(selfie);
}
}
// Fetch filter lists if no up to date selfie available
if (!selfie && (await checkInternetConnectivity())) {
logger.info(`Fetching lists...`);
await snfe.useLists(blocklists.map(fetchList).filter(o => o));
const selfie = await snfe.serialize();
fs.mkdir('cache', { recursive: true });
await fs.writeFile(pathToSelfie, selfie);
}
/**
* runs ublock origin url safe-checking
*/
const blocked = (url, originURL = undefined, mimeType = undefined) => url ? snfe.matchRequest({ url, originURL, type: mimeType }) : false;
export default blocked;
+60
View File
@@ -0,0 +1,60 @@
const { createClient } = require('redis');
const { exec } = require('child_process');
const fs = require('fs');
const loggermod = require('../utils/logger.cjs');
const { logger } = loggermod;
/** @type {import('redis').RedisClientType} */
let client;
async function addHistory(uid, query, code, title) {
// logger.info(uid, query);
return await client.lPush(`searchHistory:${uid}`, JSON.stringify({ title, query, timestamp: new Date(), code }));
}
async function getHistory(uid) {
return await client.lRange(`searchHistory:${uid}`, 0, -1);
}
/**
* @param {Electron.WebContents} webContents
*/
async function displayHistory(uid, webContents) {
const history = JSON.stringify(await getHistory(uid));
webContents.executeJavaScript(`showHistory(${history})`);
}
const quitRedis = () => client.quit().then(() => logger.info('redis quit')).catch(_ => null);
async function setupRedis() {
await new Promise((resolve, reject) => {
if (!fs.existsSync('../cache/redis.conf')) {
fs.writeFileSync('../cache/redis.conf', `dir ./\ndbfilename dump.rdb`);
}
const p = exec('redis-server ../cache/redis.conf', (err, stdout, stderr) => {
if (err) return reject(err);
});
p.on('message', logger.info);
p.on('error', logger.error);
p.on('spawn', resolve);
});
client = await createClient()
.on('error', err => {
logger.info('Redis Client Error', err);
})
.connect();
// clear history on browser boot
client.flushDb();
logger.info('Redis Client Connected!');
}
module.exports = { setupRedis, redisclient: client, getHistory, addHistory, displayHistory, quitRedis };
+32
View File
@@ -0,0 +1,32 @@
import loggermod from '../utils/logger.cjs';
import intercept from '../serverJS/intercept.js';
import setUpShortcuts from '../serverJS/shortcuts.js';
import { organizeTabIds } from '../serverJS/tabs_server.js';
import { getSavedTabs, loadTabs } from '../utils/clearCache.js';
import flushCookies from '../utils/cookies.js';
import { askUserQuestion } from '../utils/dialogue.js';
import ipcinit from '../utils/ipc.js';
import { checkInternetConnectivity } from '../utils/misc.js';
import { findPath } from '../utils/paths.js';
import { createWebview, handleWebViewInit } from '../utils/webviewHelpers.js';
const { setupRedis, quitRedis } = await import('../serverJS/history.cjs');
const { logger } = loggermod;
export {
logger,
intercept,
setUpShortcuts,
organizeTabIds,
getSavedTabs,
loadTabs,
flushCookies,
askUserQuestion,
ipcinit,
checkInternetConnectivity,
findPath,
createWebview,
handleWebViewInit,
setupRedis,
quitRedis
};
+140
View File
@@ -0,0 +1,140 @@
import { findPath } from "../utils/paths.js";
import blocked from "./adblock.js";
import fs from "fs";
import * as history from "./history.cjs";
const { addHistory } = history;
import { net, shell } from "electron";
import spawnworker from "./spawnworker.js";
import { checkInternetConnectivity } from "../utils/misc.js";
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
export const noworker = ['www.youtube.com', 'accounts.youtube.com', 'accounts.google.com', '.*\.googlevideo\.com'];
export const transformduckurl = (u) => {
try {
const urlParams = new URLSearchParams(new URL(u).search);
const actualUrl = decodeURIComponent(urlParams.get('uddg'));
return actualUrl;
}
catch (err) {
console.warn(err);
return u;
}
}
/**
* @param {GlobalRequest} req
*/
export default async function intercept(request, uid) {
try {
const u = new URL(request.url);
if (u.protocol === 'file:' || u.hostname.startsWith('ion-local')) {
const filePath = await findPath(u.pathname.split('/')?.at(-1), true);
if (!filePath || !fs.existsSync(filePath)) return new Response(`file"${filePath}" not found!`, { status: 404 });
// read the file from the filesystem
let fileData = fs.readFileSync(filePath).toString();
// guess the mime type (e.g., text/html, application/javascript)
let mimeType = 'text/plain';
if (filePath.endsWith('.html')) mimeType = 'text/html';
else if (filePath.endsWith('.js')) mimeType = 'application/javascript';
else if (filePath.endsWith('.css')) mimeType = 'text/css';
if (filePath.endsWith('HTML/nointernet.html')) {
const hascon = await checkInternetConnectivity();
const rNew = { 'Location': 'https://start.duckduckgo.com', 'Content-Type': 'text/html' };
if (hascon) return new Response(Buffer.from(`<html><body>Loading...</body></html>`, 'utf-8'), { status: 301, headers: rNew });
}
// send the file content along with a mime type
return new Response(fileData, { headers: { 'Content-Type': mimeType } });
}
else {
if (blocked(request.url)) return new Response('Request Blocked by UBlock Origin', { status: 503, statusText: 'Request Blocked by UBlock Origin' });
let newURL = request.url;
// force dark mode and turn off safe search
if (u.hostname.includes('duckduckgo.com')) newURL += (u.search) ? '&kae=d&kp=-2' : '?kae=d';
else if (u.hostname.includes('google.com')) newURL += (u.search) ? '&safe=off&&pccc=1' : '?pccc=1';
// here to avoid `TypeError: Cannot set property url of #<_Request> which has only a getter`
try { request.url = newURL; }
catch (_) { }
const iswebpagereq = request.method?.toUpperCase() === 'GET' && request.headers.get('Accept').includes('text/html');
// Odd duckduckgo redir thing (I hate it)
if (request.url.startsWith('https://duckduckgo.com/l/?')) {
const newURL = transformduckurl(request.url);
const rNew = {
'Location': newURL, // Set the redirect location header
'Content-Type': 'text/html',
}
return new Response(Buffer.from(`<html><body>Redirecting to <a href="${newURL}">${newURL}</a>...</body></html>`, 'utf-8'), { status: 301, headers: rNew });
}
let r;
// special case
if (u.href.match(/https:\/\/accounts\.(google|youtube)\.com\/(.*\/)?(signin\/challenge|ServiceLogin)\/?.*/gm)) {
// const urlObj = new URL(u.href);
// // Decode the `continue` parameter and modify it
// let continueUrl = decodeURIComponent(urlObj.searchParams.get('continue'));
// // You can modify the `continue` URL to use your custom protocol (myapp://callback)
// continueUrl = 'iobrowser://callback';
// // Encode and set the updated `continue` parameter back in the original URL
// urlObj.searchParams.set('continue', encodeURIComponent(continueUrl));
// // This is your modified URL that you will use to launch the browser
// return shell.openExternal(urlObj.href, { logUsage: true, activate: true });
r = await net.fetch(request);
}
// sloppy fix
else r = await net.fetch(request);
// else if (iswebpagereq || noworker.find(o => u.hostname.match(o))) r = await net.fetch(request);
// else r = await spawnworker(request, uid);
if (request.headers.get('Accept').includes('text/html')) {
if (u.hostname === 'lite.duckduckgo.com') {
const params = new URLSearchParams(u.search);
addHistory(uid, `${u.href}?${body}`, r.status, `DuckDuckGo${params.has('q') ? (' - ' + params.get('q')) : ''}`);
}
else {
// const res = await fetch(u.href, { method: 'HEAD' }).catch(_ => null);
// logger.info(res);
// addHistory(uid, u.href, r.status, 'title!');
}
}
return r;
/*
REMOVED BECAUSE IT'S TOO EXPENSIVE (SIGKILL-ed)
// https://accounts.google.com/v3/signin/_/AccountsSignInUi/browserinfo?f.sid=3210847140573431127&bl=boq_identityfrontendauthuiserver_20241015.01_p0&hl=en&_reqid=139420&rt=j
if (request.url === 'https://www.youtube.com/' || skip.includes(u.hostname)) return net.fetch(request);
let newURL = request.url;
if (u.hostname.includes('duckduckgo.com')) newURL += (u.search) ? '&kae=d&kp=-2' : '?kae=d';
else if (u.hostname.includes('google.com')) newURL += (u.search) ? '&safe=off&&pccc=1' : '?pccc=1';
const r = await net.fetch(request);
return r;
*/
}
}
catch (err) {
logger.error(request.url, err);
return new Response('Error', { status: 500 });
}
}
+32
View File
@@ -0,0 +1,32 @@
import { session, globalShortcut } from "electron";
import { changeZoom } from "../JS/display.js";
import { getCurrentTab, getCurrentWindow } from "./tabs_server.js";
import { logger } from "./imports.js";
/**
* @param {Electron.Event} e
* @param {Electron.BrowserWindow} window
*/
export default async function setUpShortcuts(uid) {
globalShortcut.register('Control+Shift+I', () => {
console.log("A", getCurrentWindow().currentView);
getCurrentWindow().currentView.webContents.toggleDevTools();
// getCurrentWindow().isFocused() ? getCurrentTab()?.toggleDevTools() : null
});
globalShortcut.register('Control+H', () => getCurrentTab()?.webContents.executeJavaScript('window.electronAPI.displayHistory()'));
// zoom
globalShortcut.register('Control+=', () => changeZoom(getCurrentTab(), true));
globalShortcut.register('Control+-', () => changeZoom(getCurrentTab(), false));
globalShortcut.register('Control+Plus', () => changeZoom(getCurrentTab(), false, true));
globalShortcut.register('Control+T', () => window.webContents.executeJavaScript('window.tabAPI.addTab()'))
// window.webContents.on('did-navigate', async (_, url, code, stat) => {
// if (isValidURL(url)?.hostname === 'lite.duckduckgo.com') return;
// const title = await window.webContents.executeJavaScript('document.title');
// });
}
+55
View File
@@ -0,0 +1,55 @@
import { Worker } from 'worker_threads';
import { findPath } from '../utils/paths.js';
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
// need to find something for this to do, as it breaks a lot of stuff if used for requests
/**
* runs the given function with the given args in a Nodejs Worker
* @param {Function} fn
* @param {any[]} args
* @returns
*/
export default function spawnWorker(fn, args) {
return new Promise(async (resolve, reject) => {
// find path to worker.js
const workerScriptPath = await findPath('worker.js');
// create a new worker with request data
const worker = new Worker(workerScriptPath, {
workerData: { fn: fn.toString(), args },
// workerData: request,
});
logger.info(`spawning worker for ${fn}(${args})`);
worker.on('online', () => logger.info(`started worker for ${fn}(${args})`));
// handle messages from the worker
worker.on('message', (msg) => {
try {
resolve(msg);
} catch (err) {
logger.error('Error creating response:', err);
resolve(msg);
}
// terminate the worker when done (removed bc it results in interrupts)
// worker.terminate().then(c => console.log(`${request.url} - ${c}`));
});
// handle errors from the worker
worker.on('error', (error) => {
logger.error('Worker error:', error);
resolve({ status: 500 });
});
// handle worker exiting unexpectedly
worker.on('exit', (code) => {
if (code !== 0) {
logger.warn(`Worker exited with code ${code} for ${request.url}`);
resolve({ status: 500 });
}
});
});
}
+177
View File
@@ -0,0 +1,177 @@
import fs from 'fs';
import mhtml2html from 'mhtml2html';
import { BaseWindow, WebContentsView } from 'electron';
import { createWebview } from '../utils/webviewHelpers.js';
import { CACHE_DIRECTORY, getLoadPath, saveTabState } from '../utils/clearCache.js';
import path from 'path';
import { logger } from './imports.js';
const webViewContentsMap = {}; // Memory storage for active tabs
/**
* returns the focused window, if there is no focused window, returns the first one spawned
*/
const getCurrentWindow = () => {
const allWins = BaseWindow.getAllWindows(),
w = allWins.find((win) => win.isFocused());
if (!w && allWins.length > 0) return allWins?.at(0);
else return w;
};
/**
* @returns {Electron.CrossProcessExports.WebContentsView | undefined}
*/
const getCurrentTab = () => {
const cw = getCurrentWindow();
return cw?.currentView;
}
const settabqual = (tabId) => getCurrentTab().webContents.executeJavaScript('setQuality()').catch(err => logger.warn(`setting quality for window ${tabId} failed with reason:\`\`\`${err}\`\`\``));
/**
* Switch to the specified view by its ID.
* @param {string | Electron.WebContentsView} tabId
*/
async function switchToView(tabId) {
const currentWindow = getCurrentWindow();
/** @type {WebContentsView} */
const viewData = (tabId instanceof WebContentsView) ? tabId : webViewContentsMap[tabId];
if (!viewData || !currentWindow) return;
else if (viewData.id < 0) return; // Don't modify views with negative IDs
viewData.webContents.setBackgroundThrottling(false);
const id = tabId.id || tabId;
// Save the current active view state
// find the non-background-playing window
/** @type {WebContentsView} */
const oldView = currentWindow.contentView.children.find((view) => view instanceof WebContentsView && (view.id >= 0 && !view.webContents.isCurrentlyAudible()));
// undo the optimizations
const undoOptimize = async () => {
await viewData.webContents.executeJavaScript('revertQuality()');
viewData.setVisible(true);
}
if (oldView) {
// REMOVEME
// currentWindow.contentView.removeChildView(oldView);
console.log(`saving`, JSON.stringify(id));
// DO NOT AWAIT THIS CALL FFS, INEFFICIENT!!!
saveTabState(oldView.id, oldView).then(() => console.log(`saved ${id}`));
currentWindow.contentView.removeChildView(oldView);
if (viewData.webContents.isCurrentlyAudible()) return undoOptimize();
}
else if (viewData?.webContents?.isCurrentlyAudible()) return undoOptimize();
// Set the new view as active and add it to the window
// viewData.webContents.setBackgroundThrottling(true);
currentWindow.contentView.addChildView(viewData);
await viewData.webContents.loadURL('https://start.duckduckgo.com');
currentWindow.contentView.children.map(c => c.setVisible((c.id === id) || c.id < 0));
currentWindow.contentView.children.map(c => console.log(c.id, c.webContents.isCurrentlyAudible()));
settabqual(tabId);
}
/**
* moves the tab to the "background" and tries to minimize the footprint
* @param {Electron.BaseWindow} currentWindow
* @param {String} tabId
* @param {Electron.WebContentsView} oldView
*/
async function shiftTabToBK(currentWindow, tabId, oldView, customSession) {
try {
webViewContentsMap[oldView.id] = oldView;
// this is currently playing stuff, keep it open
oldView.webContents.setBackgroundThrottling(false);
oldView.setVisible(false);
// TODO: optimize the page more
settabqual(tabId);
const newView = await createWebview(tabId, currentWindow, customSession);
webViewContentsMap[tabId] = newView;
switchToView(newView);
}
catch (err) {
console.error(err);
return null;
}
}
/**
* Add a new tab with caching.
* @param {string} tabId
* @param {Electron.Session} customSession
* @param {string} [url]
*/
async function addTab(event, tabId, customSession, url = 'https://duckduckgo.com', isOpen = false) {
const currentWindow = getCurrentWindow();
const tabPath = getLoadPath(tabId),
currentTab = getCurrentTab();
if (currentTab?.webContents?.isCurrentlyAudible() && !isOpen) return shiftTabToBK(currentWindow, tabId, currentTab, customSession);
else if (webViewContentsMap[tabId]?.webContents.isCurrentlyAudible()) return switchToView(tabId);
const newView = await createWebview(tabId, currentWindow, customSession);
webViewContentsMap[tabId] = newView;
if (tabPath && fs.existsSync(tabPath)) newView.webContents.loadFile(tabPath);
else newView.webContents.loadURL(url);
switchToView(newView);
}
/**
* Close a tab and save its state to disk.
* @param {string} tabId
*/
async function closeTab(event, tabId) {
const currentWindow = getCurrentWindow();
const view = webViewContentsMap[tabId];
if (view && view.id >= 0) {
await saveTabState(tabId, view.webContents);
currentWindow.contentView.removeChildView(view);
view.webContents.destroy();
delete webViewContentsMap[tabId];
}
}
/**
* Open an existing tab by restoring its cached state.
* @param {string} tabId
*/
function openTab(event, tabId, customSession) {
addTab(event, tabId, customSession, undefined, true); // Reopen the tab by calling addTab with its ID
}
function organizeTabIds() {
const tabs = fs.readdirSync(CACHE_DIRECTORY, { withFileTypes: true })
.filter(o => (o.isFile() && o.name.endsWith('.html')))
.map(o => o.name);
const tmpcachepath = 'cache/tmp/tabs';
fs.mkdirSync(tmpcachepath, { recursive: true });
for (let i = 0; i < tabs.length; i++) {
fs.cpSync(`${CACHE_DIRECTORY}/${tabs[i]}`, `${tmpcachepath}/${i}.html`);
}
fs.rmSync(CACHE_DIRECTORY, { recursive: true });
fs.cpSync(tmpcachepath, CACHE_DIRECTORY, { recursive: true });
fs.rmSync(tmpcachepath, { recursive: true });
}
export { closeTab, addTab, openTab, getCurrentWindow, getCurrentTab, organizeTabIds };
+56
View File
@@ -0,0 +1,56 @@
import { parentPort, workerData } from 'worker_threads';
import fs from 'fs';
import mhtml2html from "mhtml2html";
import { JSDOM } from 'jsdom';
function MHTMLtoHTML(savePath, tabId) {
return new Promise((resolve) => {
// read the MHTML file content
const mhtmlContent = fs.readFileSync(savePath, 'utf8');
/** @type {JSDOM} */
const parsedResult = mhtml2html.convert(mhtmlContent, { parseDOM: (html) => new JSDOM(html) });
// save the extracted HTML file
const htmlPath = savePath.replace('.mhtml', '.html');
fs.writeFile(htmlPath, parsedResult.serialize(), (err) => {
if (err) {
logger.error(`error saving ${tabId}`);
return resolve(false);
}
fs.rmSync(savePath);
resolve(true)
});
});
}
// a function to process the request data
const processRequest = async (data) => {
try {
const { fn, args } = data;
let result;
switch (fn) {
case 'convertpage': result = await MHTMLtoHTML(...args);
break;
default: console.log(`unknown function "${fn}(${args})`);
}
return result;
} catch (err) {
console.error(err);
// handle and report any errors
return { success: false, error: err.message };
}
};
// read input from the workerData passed from the main thread and process it
(async () => {
const result = await processRequest(workerData);
// send the response to the main process
parentPort.postMessage(result);
process.exit(0);
})();
+37
View File
@@ -0,0 +1,37 @@
// read input from stdin (from main process) and process it
process.stdin.on('data', async (data) => {
try {
// parse the request data (assumed to be in JSON format)
const requestData = JSON.parse(data.toString());
// create a new Request object using the parsed request data
const request = new Request(requestData.url, {
method: requestData.method || 'GET',
headers: requestData.headers || {},
body: requestData.body ? JSON.stringify(requestData.body) : undefined,
params: requestData.params,
query: requestData.query
})
// perform the fetch using Node.js native Fetch API
const response = await fetch(request);
// read the response as an array buffer
const buffer = await response.arrayBuffer();
// create response object
const responseObject = {
success: true,
headers: Object.fromEntries(Array.from(response.headers)),
mimeType: response.headers.get('Content-Type') || 'application/octet-stream',
data: Buffer.from(buffer).toString('base64'), // encode as base64 for transmission
}
// write the response to stdout
logger.info(JSON.stringify(responseObject));
} catch (error) {
// handle and report any errors
const errorResponse = { success: false, error: error.message }
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
});
+75
View File
@@ -0,0 +1,75 @@
import { spawn } from 'child_process';
import { findPath } from '../utils/paths.js';
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
export default function spawnworker(request, uid) {
return new Promise(async (resolve, reject) => {
const child = spawn('node', [await findPath('worker.js')]);
const requestObject = {
method: request.method,
headers: Object.fromEntries(Array.from(request.headers)),
url: request.url,
body: await request.text(),
params: request.params,
query: request.query,
};
// send the request data to the child process
child.stdin.write(JSON.stringify(requestObject) + '\n');
// Accumulate data from child process stdout
let accumulatedData = '';
// handle response from the child process
child.stdout.on('data', (data) => {
accumulatedData += data.toString();
// Check if accumulatedData contains a complete JSON object
if (accumulatedData.trim().endsWith('}')) {
let response;
try {
response = JSON.parse(accumulatedData);
} catch (err) {
logger.error('Failed to parse response from child process:', err);
child.kill(1);
resolve({ status: 500 });
return;
}
// If JSON is parsed successfully, resolve the promise
try {
// Decode the base64 data back to a buffer
if (response.data) response.data = Buffer.from(response.data, 'base64');
resolve(new Response(response.data || response, {
headers: response.headers,
status: response.status || 200,
}));
} catch (err) {
logger.error('Error creating response:', err);
resolve(response);
}
child.kill(0);
// Reset accumulatedData for the next potential message
accumulatedData = '';
}
});
// handle errors from the child process
child.stderr.on('data', (error) => {
logger.error(`Child process stderr: ${error}`);
resolve({ status: 500 });
});
// handle if the child process exits unexpectedly
child.on('close', (code) => {
// not 0 or null
if (!!code) {
logger.error(`Child process exited with code ${code}`);
resolve({ status: 500 });
}
});
});
}
+165
View File
@@ -0,0 +1,165 @@
import NodeCache from "node-cache";
import fs from 'fs';
import fsp from 'fs/promises';
import path from "path";
import { addTab } from "../serverJS/tabs_server.js";
import loggermod from '../utils/logger.cjs';
import spawnWorker from "../serverJS/spawnworker.js";
const { logger } = loggermod;
// go through all saved web pages and save them as a JSON object with ID: URL to load on browser start
// delete the tabCache directory
// Path to the directory where cached tab data will be saved
export const CACHE_DIRECTORY = path.join(process.cwd(), 'cache', 'tabCache');
if (!fs.existsSync(CACHE_DIRECTORY)) fs.mkdirSync(CACHE_DIRECTORY);
const cache = new NodeCache({
checkperiod: 1,
deleteOnExpire: true,
errorOnMissing: false,
useClones: false
});
export async function updateTabUrl(tabId, view) {
try {
const url = await view.webContents.executeJavaScript('window.location.href');
const fpath = path.join(CACHE_DIRECTORY, 'tabs.json');
let tabs = {};
try {
const fileData = await fsp.readFile(fpath, 'utf-8');
tabs = JSON.parse(fileData);
} catch (err) {
if (err.code !== 'ENOENT') return logger.info(err);
}
tabs[tabId] = tabs[tabId] || url;
await fsp.writeFile(fpath, JSON.stringify(tabs), 'utf-8');
logger.info(`Updated URL for tab ${tabId}`);
} catch (err) {
logger.error(`Error updating tab URL: ${err.message}`);
}
}
async function readPartOfFile(filePath, start = 0, length = 200) {
// open the file in read-only mode
const fileHandle = await fsp.open(filePath, 'r');
try {
const buffer = Buffer.alloc(length);
const { bytesRead } = await fileHandle.read(buffer, 0, length, start);
const s = buffer.subarray(0, bytesRead).toString('utf8');
// regex to match the URL inside the HTML comment
const commentRegex = /<!--\s*saved from url=\(\d+\)((https?|file):\/\/[^\s]+)\s*-->/i
const match = s.match(commentRegex);
return (match && match[1]) ? match[1] : null;
} finally {
await fileHandle.close();
return null;
}
}
export async function finalTabCleanup() {
const jsonfile = `${CACHE_DIRECTORY}/tabs.json`,
fnames = fs.readdirSync(`${CACHE_DIRECTORY}`, { withFileTypes: true })
.filter(o => o.isFile() && !o.name.endsWith('.json'));
const jsonconf = (fs.existsSync(jsonfile)) ? JSON.parse(fs.readFileSync(jsonfile, 'utf-8')) : {};
const jsonconfnew = await Promise.all(fnames.map(async ({ name }) => {
const fpath = `${CACHE_DIRECTORY}/${name}`,
tabid = name.replace('.html', '');
if (jsonconf[tabid]) return [tabid, jsonconf[tabid]];
const flink = await readPartOfFile(fpath);
return [tabid, flink];
}));
const o = Object.fromEntries(jsonconfnew);
logger.info(o);
if (!fs.existsSync(`${process.cwd()}/cache/tabs.json`)) fs.writeFile(`${process.cwd()}/cache/tabs.json`, JSON.stringify(o), (err) => {
if (err) logger.error(err);
});
fs.rmSync(CACHE_DIRECTORY, { recursive: true });
}
// Save tab state to disk when switching or closing a tab
/**
* @param {*} tabId
* @param {Electron.WebContentsView} view
*/
export async function saveTabState(tabId, view) {
return new Promise(async (resolve) => {
const savePath = path.join(CACHE_DIRECTORY, `${tabId}.mhtml`);
if (fs.existsSync(savePath)) {
// fs.rmSync(`${CACHE_DIRECTORY}/${tabId}_files`, { recursive: true });
fs.rmSync(savePath);
}
try {
view.webContents.savePage(savePath, 'MHTML').then(() => {
spawnWorker('convertpage', [savePath, tabId]);
updateTabUrl(tabId, view).then(() => resolve(true));
});
} catch (err) {
logger.error(err);
resolve(false);
}
})
}
export function getSavedTabs() {
try {
const tabpath = `${process.cwd()}/cache/tabs.json`;
if (fs.existsSync(tabpath)) {
const tabs = fs.readFileSync(tabpath, 'utf-8');
// fs.rmSync(tabpath);
return JSON.parse(tabs);
}
else return {};
}
catch (err) {
logger.error(err);
return {};
}
}
export async function loadTabs(customSession, tabs) {
try {
for (const key in tabs) {
await addTab(null, key, customSession, tabs[key]);
}
}
catch (err) {
logger.error(err);
}
}
export function removeTabData(tabId) {
const savePath = path.join(CACHE_DIRECTORY, `${tabId}.mhtml`);
if (fs.existsSync(savePath)) {
fs.rmSync(`${CACHE_DIRECTORY}/${tabId}_files`, { recursive: true });
fs.rmSync(savePath);
}
const fpath = `${CACHE_DIRECTORY}/tabs.json`,
tabs = (fs.existsSync(fpath)) ? JSON.parse(fs.readFileSync(fpath, 'utf-8')) : {};
if (tabs[tabId]) delete tabs[tabId];
fs.writeFileSync(fpath, JSON.stringify(tabs), 'utf-8');
}
export const getLoadPath = (tabId) => path.join(CACHE_DIRECTORY, `${tabId}.html`);
if (process.argv?.at(2)?.trim() === '--ischildproc') finalTabCleanup();
+33
View File
@@ -0,0 +1,33 @@
import { session } from 'electron';
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
const noflush = ['youtube.com', 'chatgpt.com']; // replace with your domains
async function flushCookies(customSession = session.defaultSession) {
// session.defaultSession.cookies.flushStore();
// get all cookies from the default session
const allCookies = await customSession.cookies.get({});
// filter out cookies from domains in the noflush array
const cookiesToDelete = allCookies.filter(cookie => {
return !noflush.some(domain => cookie.domain.includes(domain));
});
// delete each cookie that is not in the noflush list
for (const cookie of cookiesToDelete) {
// create the URL that matches the cookie's domain
const cookieUrl = `http${cookie.secure ? 's' : ''}://${cookie.domain.replace(/^\./, '')}${cookie.path}`;
try {
await customSession.cookies.remove(cookieUrl, cookie.name);
logger.info(`Deleted cookie: ${cookie.name} from ${cookie.domain}`);
} catch (error) {
logger.error(`Failed to delete cookie: ${cookie.name} from ${cookie.domain}`, error);
}
}
}
export default flushCookies;
+13
View File
@@ -0,0 +1,13 @@
import { dialog } from 'electron';
export async function askUserQuestion(window, title, question) {
const response = await dialog.showMessageBox(window, {
buttons: ['Yes', 'No'],
defaultId: 0,
cancelId: 1,
title,
message: question,
});
return response.response === 0; // true if 'Yes' was clicked, false if 'No'
}
+51
View File
@@ -0,0 +1,51 @@
import { BrowserWindow, ipcMain } from 'electron'
import { addHistory, displayHistory, getHistory } from '../serverJS/history.cjs';
import fs from 'fs';
import { findPath } from './paths.js';
import * as tabModule from '../serverJS/tabs_server.js';
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
/**
*
* @param {Electron.Session} customSession
*/
export default function init(customSession) {
logger.info('ipc initiated');
ipcMain.on('ping', (event) => {
logger.info(`server recieved ping from ${event.sender.id}`);
event.sender.send('pong');
});
ipcMain.on('display-history', (event, uid) => displayHistory(uid, event.sender));
ipcMain.handle('get-history', async (_, uid) => getHistory(uid));
ipcMain.on('tab-open', (e, id) => tabModule.openTab(e, id, customSession));
ipcMain.on('tab-close', (e, id) => tabModule.closeTab(e, id, customSession));
ipcMain.on('tab-new', (e, id, url) => tabModule.addTab(e, id, customSession, url));
// TODO: add logic here to save/return site perms
ipcMain.on('set-site-perms', (e, sitehostname) => console.log(sitehostname));
ipcMain.on('get-site-perms', (e, sitehostname) => {
console.log(sitehostname);
e.sender.send('site-perms', { popups: false });
});
}
const renderer = (fs.readFileSync(await findPath('renderer.js'), 'utf-8')),
optimize = (fs.readFileSync(await findPath('optimize.js'), 'utf-8'));
/**
* @param {BrowserWindow} mainWindow
*/
export async function startinject(mainWindow, uid) {
// execute the script in the renderer process
mainWindow.webContents.executeJavaScript(renderer);
mainWindow.webContents.executeJavaScript(optimize);
// mainWindow.webContents.executeJavaScript(tabs);
const title = await mainWindow.webContents.executeJavaScript('document.title');
addHistory(uid, mainWindow.webContents.getURL(), 200, title);
}
+26
View File
@@ -0,0 +1,26 @@
const { createLogger, transports, format } = require('winston');
const logger = createLogger({
level: 'info', // default log level
format: format.combine(
format.timestamp(), // include a timestamp
format.printf((info) => `${info.timestamp} [${info.level.toUpperCase()}]: ${info.message}`)
),
transports: [
new transports.Console({
format: format.combine(
format.colorize(), // make the console output colorful
format.simple() // simple log format for console
),
}),
new transports.File({
filename: 'logs/mainprocerror.log',
level: 'error', // log only errors to this file
}),
new transports.File({
filename: 'logs/combined.log', // log all levels to this file
}),
],
});
module.exports = { logger };
+67
View File
@@ -0,0 +1,67 @@
import { BrowserWindow } from 'electron';
import fs from 'fs';
import dns from 'dns';
import path from 'path';
const history = (fs.readFileSync(path.resolve(import.meta.dirname, '../CSS', 'history.css')).toString()),
tabs = (fs.readFileSync(path.resolve(import.meta.dirname, '../CSS', 'tabs.css')).toString());
export const isValidURL = (u) => {
try { return new URL(u); }
catch (err) { return false; }
}
/**
*
* @param {BrowserWindow} window
* @param {String} hostname
*/
export async function addEl(window, hostname) {
let src = '';
switch (hostname) {
case 'lite.duckduckgo.com': src = 'duckduckgo.css';
break;
case 'www.youtube.com': src = 'youtube.css';
break;
default: //logger.info(origin);
}
const p = path.resolve(import.meta.dirname, '../CSS', src);
console.log(p);
if (src && fs.existsSync(p)) {
const srccontent = fs.readFileSync(p).toString();
window.webContents.insertCSS(srccontent);
// window.webContents.executeJavaScript(`window.safeHTML.addStylesheet(undefined, \`${srccontent}\`)`);
}
window.webContents.insertCSS(history);
window.webContents.insertCSS(tabs);
// window.safdocument.addEventListener('')eHTML.addStylesheet(srccontent, `https://ion-local.${window.location.hostname}/${src}`);
// window.safeHTML.addStylesheet(history, `https://ion-local.${window.location.hostname}/history.css`);
// window.webContents.executeJavaScript(`window.safeHTML.addStylesheet(undefined, \`${history}\`)`);
// window.webContents.executeJavaScript(`window.safeHTML.addStylesheet(undefined, \`${tabs}\`)`);
}
export function checkInternetConnectivity() {
return new Promise((resolve) => {
// Check if a known domain can be resolved (e.g., Google DNS).
dns.lookup('8.8.8.8', async (err) => {
if (err && err.code === 'ENOTFOUND') {
resolve(false); // Domain couldn't be resolved
} else {
try {
resolve((await fetch('https://www.google.com')).ok);
}
catch (err) {
resolve(false);
}
}
});
});
}
+79
View File
@@ -0,0 +1,79 @@
import { join } from 'path';
import fs from 'fs';
import { exec } from 'child_process';
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
// was import.meta.dirname but was changed to better suit the app
export const __dirname = process.cwd();
/**
* returns the full path and performs cursory validation
* @returns {Promise<String>} the full pth
* @param {String} fname
*/
export const findPath = (fname, absolute = false) => {
try {
return new Promise((resolve, reject) => {
// use the find command to search for the file
exec(`find -type f -name "${fname}"`, (error, stdout, stderr) => {
if (error) {
return reject(`Error: ${stderr || error.message}`)
}
// clean up the output and resolve with the first result, or null if not found
const relativePath = stdout.trim().split('\n').filter(Boolean)[0] || null;
if (!relativePath) return;
const p = (absolute) ? join(__dirname, relativePath) : relativePath;
resolve(p);
});
});
}
catch (err) {
return logger.error(err);
}
}
/**
* returns the full path and performs cursory validation
* @returns {String} the full pth
* @param {String} fname
*/
const findPathOld = (fname, absolute = false) => {
try {
const ext = fname.match(/[^.]+$/)?.at(0);
const base = (absolute) ? __dirname : '';
switch (ext) {
case 'mhtml':
case 'html': {
let p = join(base, 'HTML', fname);
if (!fs.existsSync(p)) p = join(base, 'cache', 'tabCache', fname);
return p;
}
case 'scss':
case 'css': return join(base, 'CSS', fname);
case 'cjs':
case 'js': {
let p = join(base, 'JS', fname);
if (!fs.existsSync(p)) p = join(base, 'serverJS', fname);
if (!fs.existsSync(p)) p = join(base, 'utils', fname);
if (!fs.existsSync(p)) p = join(base, 'organization', fname);
if (fs.existsSync(p)) return p;
}
break;
default: return null;
}
}
catch (err) {
return logger.error(err);
}
}
+72
View File
@@ -0,0 +1,72 @@
import { WebContentsView } from "electron";
import { partitionName, agent, uid } from "../main.js";
import { findPath } from "./paths.js";
import { addEl, isValidURL } from "./misc.js";
import { startinject } from "./ipc.js";
import intercept, { noworker } from "../serverJS/intercept.js";
import loggermod from '../utils/logger.cjs';
const { logger } = loggermod;
/**
* @param {Electron.WebContents} contents
*/
export async function handleWebViewInit(contents) {
}
/**
* @param {*} tabId
* @param {Electron.BaseWindow} currentWindow
*/
export async function createWebview(tabId, currentWindow, customSession, preloadFname = 'preload.cjs') {
const preloadPath = await findPath(preloadFname, true);
logger.info(preloadFname, preloadPath);
const view = new WebContentsView({
webPreferences: {
nodeIntegration: true,
contextIsolation: true, // allow access to Node.js in renderer
javascript: true,
plugins: true,
// enableBlinkFeatures: "WebContentsForceDark",
partition: partitionName,
preload: preloadPath,
nodeIntegrationInWorker: true
}
});
view.id = tabId;
// set initial size
const resizeWebView = () => {
const { width: w, height: h } = currentWindow.getBounds();
if (tabId === -1) view.setBounds({ x: 0, y: 0, width: w, height: 35 });
else view.setBounds({ x: 0, y: 35, width: w, height: h - 35 });
};
// add the web view as a child of the window's content view
currentWindow.contentView.addChildView(view);
// update bounds on window resize
currentWindow.on('resize', resizeWebView);
view.webContents.setUserAgent(agent);
const { width: w, height: h } = currentWindow.getBounds();
view.setBounds({ x: 0, y: 0, width: w, height: h });
view.webContents.on('did-start-navigation', (e, newU) => {
const u = isValidURL(newU);
if (noworker.find(o => u.hostname.match(o)) && customSession.protocol.isProtocolHandled('https')) customSession.protocol.unhandle('https');
else if (!customSession.protocol.isProtocolHandled('https')) customSession.protocol.handle('https', (r) => intercept(r, uid));
});
view.webContents.on('did-navigate', (e, newU) => {
const u = isValidURL(newU);
addEl(view, u?.hostname);
startinject(view, uid);
});
view.webContents.setBackgroundThrottling(true);
resizeWebView();
return view;
}