mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
add electron
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# docs https://github.com/marketplace/actions/create-release
|
||||
# docs https://github.com/ncipollo/release-action
|
||||
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# electron build
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
# make sure the ipfs executable is executable (linux only)
|
||||
- run: node electron/downloadIpfs && sudo chmod +x bin/linux/ipfs
|
||||
- run: yarn build
|
||||
- run: yarn electron:build:linux
|
||||
- run: ls dist
|
||||
|
||||
# publish version release
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: 'dist/plebchan*.AppImage,dist/plebchan-html*.zip'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
body: "Progressive web app mirrors:\n- https://plebchan.eth.limo\n- https://plebchan.eth.link\n- https://cloudflare-ipfs.com/ipns/plebchan.eth (insecure, no subdomain isolation)\n\nCLI client:\n- https://github.com/plebbit/plebbit-cli/releases/latest"
|
||||
allowUpdates: true
|
||||
|
||||
mac:
|
||||
runs-on: macOS-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# electron build
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn electron:build:mac
|
||||
- run: ls dist
|
||||
|
||||
# publish version release
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: 'dist/plebchan*.dmg'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
body: "Progressive web app mirrors:\n- https://plebchan.eth.limo\n- https://plebchan.eth.link\n- https://cloudflare-ipfs.com/ipns/plebchan.eth (insecure, no subdomain isolation)\n\nCLI client:\n- https://github.com/plebbit/plebbit-cli/releases/latest"
|
||||
allowUpdates: true
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# electron build
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn electron:build:windows
|
||||
- run: dir dist
|
||||
|
||||
# publish version release
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: 'dist/plebchan*.exe'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
body: "Progressive web app mirrors:\n- https://plebchan.eth.limo\n- https://plebchan.eth.link\n- https://cloudflare-ipfs.com/ipns/plebchan.eth (insecure, no subdomain isolation)\n\nCLI client:\n- https://github.com/plebbit/plebbit-cli/releases/latest"
|
||||
allowUpdates: true
|
||||
|
||||
# android:
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: write
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions/setup-java@v3
|
||||
# with:
|
||||
# distribution: 'zulu'
|
||||
# java-version: '11'
|
||||
# - uses: gradle/gradle-build-action@v2
|
||||
# with:
|
||||
# gradle-version: 7.6
|
||||
# - uses: actions/setup-node@v2
|
||||
# with:
|
||||
# node-version: 16
|
||||
# - run: sudo apt install -y apksigner zipalign
|
||||
|
||||
# # build react app
|
||||
# - run: yarn install --frozen-lockfile
|
||||
# - run: yarn build
|
||||
# # build apk
|
||||
# - run: npx cap update
|
||||
# - run: npx cap copy
|
||||
# - run: cd android && gradle bundle
|
||||
# - run: cd android && ./gradlew assembleRelease
|
||||
# # optimize apk
|
||||
# - run: cd android/app/build/outputs/apk/release && zipalign 4 app-release-unsigned.apk app-release-unsigned-zip.apk
|
||||
# # sign apk
|
||||
# # to create keystore: keytool -genkey -v -keystore plebbit.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias release
|
||||
# - run: cd android/app/build/outputs/apk/release && apksigner sign --ks ../../../../../plebbit.keystore --ks-pass pass:${{ secrets.PLEBBIT_REACT_KEYSTORE_PASSWORD }} --ks-key-alias release --out app-release-signed.apk app-release-unsigned-zip.apk
|
||||
# # move apk to dist folder
|
||||
# - run: mkdir -p dist && mv android/app/build/outputs/apk/release/app-release-signed.apk dist/plebbit-$(node -e "console.log(require('./package.json').version)").apk
|
||||
# - run: ls dist
|
||||
|
||||
# # publish version release
|
||||
# - uses: ncipollo/release-action@v1
|
||||
# with:
|
||||
# artifacts: 'dist/plebbit*.apk'
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# replacesArtifacts: true
|
||||
# body: "Progressive web app mirrors:\n- https://plebchan.eth.limo\n- https://plebchan.eth.link\n- https://cloudflare-ipfs.com/ipns/plebchan.eth (insecure, no subdomain isolation)\n\nCLI client:\n- https://github.com/plebbit/plebbit-cli/releases/latest"
|
||||
# allowUpdates: true
|
||||
+6
-1
@@ -1,4 +1,8 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# electron builder binaries
|
||||
/bin
|
||||
|
||||
# data created by plebbit-js
|
||||
.plebbit
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@@ -10,6 +14,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// hook that runs after electron-build
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const rootPath = path.resolve(__dirname, '..');
|
||||
const distFolderPath = path.resolve(rootPath, 'dist');
|
||||
|
||||
const addPortableToPortableExecutableFileName = () => {
|
||||
const files = fs.readdirSync(distFolderPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.exe') && !file.match('Setup')) {
|
||||
const filePath = path.resolve(distFolderPath, file);
|
||||
const renamedFilePath = path.resolve(
|
||||
distFolderPath,
|
||||
file.replace('plebchan', 'plebchan Portable')
|
||||
);
|
||||
fs.moveSync(filePath, renamedFilePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createHtmlArchive = () => {
|
||||
if (process.platform !== 'linux') {
|
||||
return;
|
||||
}
|
||||
const { version } = require('../package.json');
|
||||
const zipBinPath = path.resolve(rootPath, 'node_modules', '7zip-bin', 'linux', 'x64', '7za');
|
||||
const plebchanHtmlFolderName = `plebchan-html-${version}`;
|
||||
const outputFile = path.resolve(distFolderPath, `${plebchanHtmlFolderName}.zip`);
|
||||
const inputFolder = path.resolve(rootPath, 'build');
|
||||
try {
|
||||
// will break if node_modules/7zip-bin changes
|
||||
execSync(`${zipBinPath} a ${outputFile} ${inputFolder}`);
|
||||
// rename 'build' folder to 'plebchan-html-version' inside the archive
|
||||
execSync(`${zipBinPath} rn -r ${outputFile} build ${plebchanHtmlFolderName}`);
|
||||
} catch (e) {
|
||||
e.message = 'electron build createHtmlArchive error: ' + e.message;
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
exports.default = async (buildResult) => {
|
||||
addPortableToPortableExecutableFileName();
|
||||
createHtmlArchive();
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
// download the ipfs binaries before building the electron clients
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const ProgressBar = require('progress');
|
||||
const https = require('https');
|
||||
const decompress = require('decompress');
|
||||
const path = require('path');
|
||||
const ipfsClientsPath = path.join(__dirname, '..', 'bin');
|
||||
const ipfsClientWindowsPath = path.join(ipfsClientsPath, 'win');
|
||||
const ipfsClientMacPath = path.join(ipfsClientsPath, 'mac');
|
||||
const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux');
|
||||
|
||||
// download links https://docs.ipfs.io/install/command-line/#official-distributions
|
||||
const ipfsClientVersion = '0.18.1';
|
||||
const ipfsClientWindowsUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_windows-amd64.zip`;
|
||||
const ipfsClientMacUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_darwin-amd64.tar.gz`;
|
||||
const ipfsClientLinuxPUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-amd64.tar.gz`;
|
||||
|
||||
const downloadWithProgress = (url) =>
|
||||
new Promise((resolve) => {
|
||||
const split = url.split('/');
|
||||
const fileName = split[split.length - 1];
|
||||
const chunks = [];
|
||||
const req = https.request(url);
|
||||
req.on('response', (res) => {
|
||||
const len = parseInt(res.headers['content-length'], 10);
|
||||
console.log();
|
||||
const bar = new ProgressBar(` ${fileName} [:bar] :rate/bps :percent :etas`, {
|
||||
complete: '=',
|
||||
incomplete: ' ',
|
||||
width: 20,
|
||||
total: len,
|
||||
});
|
||||
res.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
bar.tick(chunk.length);
|
||||
});
|
||||
res.on('end', () => {
|
||||
console.log('\n');
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
|
||||
const download = async (url, destinationPath) => {
|
||||
let binName = 'ipfs';
|
||||
if (destinationPath.endsWith('win')) {
|
||||
binName += '.exe';
|
||||
}
|
||||
const binPath = path.join(destinationPath, binName);
|
||||
if (fs.pathExistsSync(binPath)) {
|
||||
return;
|
||||
}
|
||||
const split = url.split('/');
|
||||
const fileName = split[split.length - 1];
|
||||
const dowloadPath = path.join(destinationPath, fileName);
|
||||
const file = await downloadWithProgress(url);
|
||||
fs.ensureDirSync(destinationPath);
|
||||
await fs.writeFile(dowloadPath, file);
|
||||
await decompress(dowloadPath, destinationPath);
|
||||
const extractedPath = path.join(destinationPath, 'kubo');
|
||||
const extractedBinPath = path.join(extractedPath, binName);
|
||||
fs.moveSync(extractedBinPath, binPath);
|
||||
fs.removeSync(extractedPath);
|
||||
fs.removeSync(dowloadPath);
|
||||
};
|
||||
|
||||
const downloadIpfsClients = async () => {
|
||||
await download(ipfsClientWindowsUrl, ipfsClientWindowsPath);
|
||||
await download(ipfsClientMacUrl, ipfsClientMacPath);
|
||||
await download(ipfsClientLinuxPUrl, ipfsClientLinuxPath);
|
||||
};
|
||||
|
||||
exports.default = async (context) => {
|
||||
await downloadIpfsClients();
|
||||
};
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
root_path=$(cd `dirname $0` && cd .. && pwd)
|
||||
cd "$root_path"
|
||||
|
||||
# install node_modules
|
||||
if [[ ! -f node_modules ]]
|
||||
then
|
||||
yarn || { echo "Error: failed installing 'node_modules' with 'yarn'" ; exit 1; }
|
||||
fi
|
||||
|
||||
# download ipfs clients
|
||||
node electron/downloadIpfs || { echo "Error: failed script 'node electron/downloadIpfs'" ; exit 1; }
|
||||
|
||||
dockerfile='
|
||||
FROM electronuserland/builder:16
|
||||
|
||||
# install node_modules
|
||||
WORKDIR /usr/src/plebchan
|
||||
COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
RUN yarn
|
||||
|
||||
# build native dependencies like sqlite3
|
||||
RUN electron-builder install-app-deps
|
||||
|
||||
# copy source files
|
||||
COPY ./bin ./bin
|
||||
COPY ./electron ./electron
|
||||
COPY ./src ./src
|
||||
COPY ./public ./public
|
||||
|
||||
# required or yarn build fails
|
||||
COPY ./.eslintrc.json ./.eslintrc.json
|
||||
COPY ./.prettierrc ./.prettierrc
|
||||
|
||||
# react build
|
||||
RUN yarn build
|
||||
'
|
||||
|
||||
# build electron-builder docker image
|
||||
# temporary .dockerignore to save build time
|
||||
echo $'node_modules\ndist' > .dockerignore
|
||||
echo "$dockerfile" | sudo docker build \
|
||||
. \
|
||||
--tag plebchan-electron-builder \
|
||||
--file -
|
||||
rm .dockerignore
|
||||
|
||||
# build linux binary
|
||||
sudo docker run \
|
||||
--name plebchan-electron-builder \
|
||||
--volume "$root_path"/dist:/usr/src/plebchan/dist \
|
||||
--rm \
|
||||
plebchan-electron-builder \
|
||||
yarn electron:build:linux
|
||||
|
||||
# build windows binary
|
||||
sudo docker run \
|
||||
--name plebchan-electron-builder \
|
||||
--volume "$root_path"/dist:/usr/src/plebchan/dist \
|
||||
--rm \
|
||||
plebchan-electron-builder \
|
||||
yarn electron:build:windows
|
||||
@@ -0,0 +1,2 @@
|
||||
const downloadIpfsClients = require('./beforePack').default;
|
||||
downloadIpfsClients();
|
||||
@@ -0,0 +1,56 @@
|
||||
// require this file to log to file in case there's a crash
|
||||
|
||||
const envPaths = require('env-paths').default('plebchan', { suffix: false });
|
||||
const util = require('util');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
// previous version created a file instead of folder
|
||||
// we should remove this at some point
|
||||
try {
|
||||
if (fs.lstatSync(envPaths.log).isFile()) {
|
||||
fs.removeSync(envPaths.log);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const logFilePath = path.join(envPaths.log, new Date().toISOString().substring(0, 7));
|
||||
fs.ensureFileSync(logFilePath);
|
||||
const logFile = fs.createWriteStream(logFilePath, { flags: 'a' });
|
||||
const writeLog = (...args) => {
|
||||
logFile.write(new Date().toISOString() + ' ');
|
||||
for (const arg of args) {
|
||||
logFile.write(util.format(arg) + ' ');
|
||||
}
|
||||
logFile.write('\r\n');
|
||||
};
|
||||
|
||||
const consoleLog = console.log;
|
||||
console.log = (...args) => {
|
||||
writeLog(...args);
|
||||
consoleLog(...args);
|
||||
};
|
||||
const consoleError = console.error;
|
||||
console.error = (...args) => {
|
||||
writeLog(...args);
|
||||
consoleError(...args);
|
||||
};
|
||||
const consoleWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
writeLog(...args);
|
||||
consoleWarn(...args);
|
||||
};
|
||||
const consoleDebug = console.debug;
|
||||
console.debug = (...args) => {
|
||||
// don't add date for debug because it's usually already included
|
||||
for (const arg of args) {
|
||||
logFile.write(util.format(arg) + ' ');
|
||||
}
|
||||
logFile.write('\r\n');
|
||||
consoleDebug(...args);
|
||||
};
|
||||
|
||||
// errors aren't console logged
|
||||
process.on('uncaughtException', console.error);
|
||||
process.on('unhandledRejection', console.error);
|
||||
|
||||
console.log(envPaths);
|
||||
@@ -0,0 +1,262 @@
|
||||
require('./log');
|
||||
const {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tray,
|
||||
screen: electronScreen,
|
||||
shell,
|
||||
dialog,
|
||||
} = require('electron');
|
||||
const isDev = require('electron-is-dev');
|
||||
const path = require('path');
|
||||
const startIpfs = require('./startIpfs');
|
||||
const { URL } = require('node:url');
|
||||
|
||||
let startIpfsError;
|
||||
startIpfs().catch((e) => {
|
||||
startIpfsError = e;
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
// add right click menu
|
||||
const contextMenu = require('electron-context-menu');
|
||||
contextMenu({
|
||||
// prepend custom buttons to top
|
||||
prepend: (defaultActions, parameters, browserWindow) => [
|
||||
{
|
||||
label: 'Back',
|
||||
visible: parameters.mediaType === 'none',
|
||||
enabled: browserWindow?.webContents?.canGoBack(),
|
||||
click: () => browserWindow?.webContents?.goBack(),
|
||||
},
|
||||
{
|
||||
label: 'Forward',
|
||||
visible: parameters.mediaType === 'none',
|
||||
enabled: browserWindow?.webContents?.canGoForward(),
|
||||
click: () => browserWindow?.webContents?.goForward(),
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
visible: parameters.mediaType === 'none',
|
||||
click: () => browserWindow?.webContents?.reload(),
|
||||
},
|
||||
],
|
||||
showLookUpSelection: false,
|
||||
showCopyImage: true,
|
||||
showCopyImageAddress: true,
|
||||
showSaveImageAs: true,
|
||||
showSaveLinkAs: true,
|
||||
showInspectElement: true,
|
||||
showServices: false,
|
||||
showSearchWithGoogle: false,
|
||||
});
|
||||
|
||||
const createMainWindow = () => {
|
||||
let mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 600,
|
||||
show: false,
|
||||
backgroundColor: 'white',
|
||||
webPreferences: {
|
||||
// fix cors error for Chain providers
|
||||
webSecurity: false,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
devTools: true, // TODO: change to isDev when no bugs left
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
});
|
||||
const startURL = isDev
|
||||
? 'http://localhost:3000'
|
||||
: `file://${path.join(__dirname, '../build/index.html')}`;
|
||||
|
||||
mainWindow.loadURL(startURL);
|
||||
|
||||
mainWindow.once('ready-to-show', async () => {
|
||||
// make sure back button is disabled on launch
|
||||
mainWindow.webContents.clearHistory();
|
||||
|
||||
mainWindow.show();
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.openDevTools();
|
||||
}
|
||||
|
||||
if (startIpfsError) {
|
||||
dialog.showErrorBox('IPFS error', startIpfsError.message);
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// use common user agent instead of electron
|
||||
// https://www.whatismybrowser.com/guides/the-latest-version/chrome
|
||||
// https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
|
||||
mainWindow.webContents.userAgent =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36';
|
||||
|
||||
// don't open new windows
|
||||
mainWindow.webContents.on('new-window', (event, url) => {
|
||||
event.preventDefault();
|
||||
mainWindow.loadURL(url);
|
||||
});
|
||||
|
||||
// open links in external browser
|
||||
// do not open links in plebchan or will lead to remote execution
|
||||
mainWindow.webContents.on('will-navigate', (e, originalUrl) => {
|
||||
if (originalUrl != mainWindow.webContents.getURL()) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
// do not let the user open any url with shell.openExternal
|
||||
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
|
||||
|
||||
// only open valid https urls to prevent remote execution
|
||||
// will throw if url isn't valid
|
||||
const validatedUrl = new URL(originalUrl);
|
||||
if (validatedUrl.protocol !== 'https:') {
|
||||
throw Error(`can't open url '${originalUrl}' not https`);
|
||||
}
|
||||
|
||||
// do not open http protocol, not private
|
||||
// do not open any other protocol, will lead to remote execution
|
||||
|
||||
// open serialized url to prevent remote execution
|
||||
const serializedUrl = validatedUrl.toString();
|
||||
shell.openExternal(serializedUrl);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// open links (with target="_blank") in external browser
|
||||
// do not open links in plebchan or will lead to remote execution
|
||||
mainWindow.webContents.setWindowOpenHandler(({url}) => {
|
||||
const originalUrl = url
|
||||
try {
|
||||
// do not let the user open any url with shell.openExternal
|
||||
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
|
||||
|
||||
// only open valid https urls to prevent remote execution
|
||||
// will throw if url isn't valid
|
||||
const validatedUrl = new URL(originalUrl);
|
||||
if (validatedUrl.protocol !== 'https:') {
|
||||
throw Error(`can't open url '${originalUrl}' not https`);
|
||||
}
|
||||
|
||||
// do not open http protocol, not private
|
||||
// do not open any other protocol, will lead to remote execution
|
||||
|
||||
// open serialized url to prevent remote execution
|
||||
const serializedUrl = validatedUrl.toString();
|
||||
shell.openExternal(serializedUrl);
|
||||
setImmediate(() => {
|
||||
shell.openExternal(serializedUrl)
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
return {action: 'deny'}
|
||||
})
|
||||
|
||||
// deny permissions like location, notifications, etc https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
|
||||
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
// deny all permissions
|
||||
return callback(false)
|
||||
})
|
||||
|
||||
// deny attaching webview https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
|
||||
mainWindow.webContents.on('will-attach-webview', (e, webPreferences, params) => {
|
||||
// deny all
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
// tray
|
||||
const trayIconPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
isDev ? 'public' : 'build',
|
||||
'electron-tray-icon.png'
|
||||
);
|
||||
const tray = new Tray(trayIconPath);
|
||||
tray.setToolTip('plebchan');
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Open plebchan',
|
||||
click: () => {
|
||||
mainWindow.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Quit plebchan',
|
||||
click: () => {
|
||||
mainWindow.destroy();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
tray.setContextMenu(trayMenu);
|
||||
|
||||
// show/hide on tray right click
|
||||
tray.on('right-click', () => {
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
});
|
||||
|
||||
// close to tray
|
||||
if (!isDev) {
|
||||
let isQuiting = false;
|
||||
app.on('before-quit', () => {
|
||||
isQuiting = true;
|
||||
});
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isQuiting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
event.returnValue = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// application menu
|
||||
// hide useless electron help menu
|
||||
const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter(
|
||||
(item) => item.role !== 'help'
|
||||
);
|
||||
const appMenuBack = new MenuItem({
|
||||
label: '←',
|
||||
enabled: mainWindow?.webContents?.canGoBack(),
|
||||
click: () => mainWindow?.webContents?.goBack(),
|
||||
});
|
||||
const appMenuForward = new MenuItem({
|
||||
label: '→',
|
||||
enabled: mainWindow?.webContents?.canGoForward(),
|
||||
click: () => mainWindow?.webContents?.goForward(),
|
||||
});
|
||||
const appMenuReload = new MenuItem({
|
||||
label: '⟳',
|
||||
role: 'reload',
|
||||
click: () => mainWindow?.webContents?.reload(),
|
||||
});
|
||||
const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp];
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu));
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createMainWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (!BrowserWindow.getAllWindows().length) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
// force IpfsHttpClient to use node-fetch to fix max 6 connections
|
||||
// temporary, eventually we will fork ipfs-http-client and replace
|
||||
// 'native-fetch'
|
||||
const mock = require('mock-require');
|
||||
const fetch = require('node-fetch');
|
||||
mock('ipfs-utils/src/env.js', {
|
||||
isTest: false,
|
||||
isElectron: false,
|
||||
isElectronMain: false,
|
||||
isElectronRenderer: false,
|
||||
isNode: true,
|
||||
isBrowser: false,
|
||||
isWebWorker: false,
|
||||
isEnvWithDom: false,
|
||||
isReactNative: false,
|
||||
});
|
||||
mock('native-fetch', {
|
||||
default: fetch.default,
|
||||
Headers: fetch.Headers,
|
||||
Request: fetch.Request,
|
||||
Response: fetch.Response,
|
||||
});
|
||||
|
||||
const envPaths = require('env-paths').default('plebchan', { suffix: false });
|
||||
const { contextBridge } = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
// dev uses http://localhost, prod uses file://...index.html
|
||||
const isDev = window.location.protocol === 'http:';
|
||||
|
||||
const defaultPlebbitOptions = {
|
||||
// find the user's OS data path
|
||||
dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'),
|
||||
ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'] || undefined,
|
||||
// TODO: having to define pubsubHttpClientOptions and ipfsHttpClientOptions is a bug with plebbit-js
|
||||
pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'] || undefined,
|
||||
// electron starts the local ipfs gateway on port 11028 because 8080 is too common
|
||||
ipfsGatewayUrls: ['http://localhost:11028'] || undefined,
|
||||
};
|
||||
|
||||
// expose plebbit-js native functions into electron's renderer
|
||||
contextBridge.exposeInMainWorld('plebbitJsNativeFunctions', require('@plebbit/plebbit-js').nativeFunctions.node)
|
||||
contextBridge.exposeInMainWorld('defaultPlebbitOptions', defaultPlebbitOptions)
|
||||
|
||||
// try/catch localStorage.debug because causes unknown error sometimes
|
||||
try {
|
||||
if (isDev) localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*';
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// use this proxy server to debug ipfs api requests made by electron
|
||||
|
||||
const http = require('http');
|
||||
const httpProxy = require('http-proxy');
|
||||
|
||||
// start proxy
|
||||
const proxy = httpProxy.createProxyServer({});
|
||||
|
||||
// rewrite the request
|
||||
proxy.on('proxyReq', function (proxyReq, req, res, options) {
|
||||
// remove headers that could potentially cause an ipfs 403 error
|
||||
proxyReq.removeHeader('X-Forwarded-For');
|
||||
proxyReq.removeHeader('X-Forwarded-Proto');
|
||||
proxyReq.removeHeader('sec-ch-ua');
|
||||
proxyReq.removeHeader('sec-ch-ua-mobile');
|
||||
proxyReq.removeHeader('user-agent');
|
||||
proxyReq.removeHeader('origin');
|
||||
proxyReq.removeHeader('sec-fetch-site');
|
||||
proxyReq.removeHeader('sec-fetch-mode');
|
||||
proxyReq.removeHeader('sec-fetch-dest');
|
||||
proxyReq.removeHeader('referer');
|
||||
});
|
||||
proxy.on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
// start server
|
||||
const start = ({ proxyPort, targetPort } = {}) => {
|
||||
const server = http.createServer();
|
||||
|
||||
// never timeout the keep alive connection
|
||||
server.keepAliveTimeout = 0;
|
||||
|
||||
server.on('request', async (req, res) => {
|
||||
console.log(new Date().toISOString(), req.method, req.url, req.rawHeaders);
|
||||
|
||||
// fix cors error from dev url localhost:3000
|
||||
// should not be necessary in production build using file url
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
proxy.web(req, res, { target: `http://localhost:${targetPort}` });
|
||||
});
|
||||
server.on('error', console.error);
|
||||
server.listen(proxyPort);
|
||||
console.log(`proxy server listening on port ${proxyPort}`);
|
||||
};
|
||||
|
||||
module.exports = { start };
|
||||
@@ -0,0 +1,110 @@
|
||||
const isDev = require('electron-is-dev');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs-extra');
|
||||
const envPaths = require('env-paths').default('plebchan', { suffix: false });
|
||||
const ps = require('node:process');
|
||||
const proxyServer = require('./proxyServer');
|
||||
|
||||
// use this custom function instead of spawnSync for better logging
|
||||
// also spawnSync might have been causing crash on start on windows
|
||||
const spawnAsync = (...args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const spawedProcess = spawn(...args);
|
||||
spawedProcess.on('exit', (exitCode, signal) => {
|
||||
if (exitCode === 0) resolve();
|
||||
else
|
||||
reject(
|
||||
Error(
|
||||
`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`
|
||||
)
|
||||
);
|
||||
});
|
||||
spawedProcess.stderr.on('data', (data) => console.error(data.toString()));
|
||||
spawedProcess.stdin.on('data', (data) => console.log(data.toString()));
|
||||
spawedProcess.stdout.on('data', (data) => console.log(data.toString()));
|
||||
spawedProcess.on('error', (data) => console.error(data.toString()));
|
||||
});
|
||||
|
||||
const startIpfs = async () => {
|
||||
const ipfsFileName = process.platform == 'win32' ? 'ipfs.exe' : 'ipfs';
|
||||
let ipfsPath = path.join(process.resourcesPath, 'bin', ipfsFileName);
|
||||
let ipfsDataPath = path.join(envPaths.data, 'ipfs');
|
||||
|
||||
// test launching the ipfs binary in dev mode
|
||||
// they must be downloaded first using `yarn electron:build`
|
||||
if (isDev) {
|
||||
let binFolderName = 'win';
|
||||
if (process.platform === 'linux') {
|
||||
binFolderName = 'linux';
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
binFolderName = 'mac';
|
||||
}
|
||||
ipfsPath = path.join(__dirname, '..', 'bin', binFolderName, ipfsFileName);
|
||||
ipfsDataPath = path.join(__dirname, '..', '.plebbit', 'ipfs');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(ipfsPath)) {
|
||||
throw Error(`ipfs binary '${ipfsPath}' doesn't exist`);
|
||||
}
|
||||
|
||||
console.log({ ipfsPath, ipfsDataPath });
|
||||
|
||||
fs.ensureDirSync(ipfsDataPath);
|
||||
const env = { IPFS_PATH: ipfsDataPath };
|
||||
// init ipfs client on first launch
|
||||
try {
|
||||
await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true });
|
||||
} catch (e) {}
|
||||
|
||||
// dont use 8080 port because it's too common
|
||||
await spawnAsync(ipfsPath, ['config', 'Addresses.Gateway', '/ip4/127.0.0.1/tcp/11028'], {
|
||||
env,
|
||||
hideWindows: true,
|
||||
});
|
||||
|
||||
// use different port with proxy for debugging during env
|
||||
let apiAddress = '/ip4/127.0.0.1/tcp/5001';
|
||||
if (isDev) {
|
||||
apiAddress = apiAddress.replace('5001', '5002');
|
||||
proxyServer.start({ proxyPort: 5001, targetPort: 5002 });
|
||||
}
|
||||
await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const ipfsProcess = spawn(
|
||||
ipfsPath,
|
||||
['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'],
|
||||
{ env, hideWindows: true }
|
||||
);
|
||||
console.log(`ipfs daemon process started with pid ${ipfsProcess.pid}`);
|
||||
let lastError;
|
||||
ipfsProcess.stderr.on('data', (data) => {
|
||||
lastError = data.toString();
|
||||
console.error(data.toString());
|
||||
});
|
||||
ipfsProcess.stdin.on('data', (data) => console.log(data.toString()));
|
||||
ipfsProcess.stdout.on('data', (data) => console.log(data.toString()));
|
||||
ipfsProcess.on('error', (data) => console.error(data.toString()));
|
||||
ipfsProcess.on('exit', () => {
|
||||
console.error(`ipfs process with pid ${ipfsProcess.pid} exited`);
|
||||
reject(Error(lastError));
|
||||
});
|
||||
process.on('exit', () => {
|
||||
try {
|
||||
ps.kill(ipfsProcess.pid);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
try {
|
||||
// sometimes ipfs doesnt exit unless we kill pid +1
|
||||
ps.kill(ipfsProcess.pid + 1);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = startIpfs;
|
||||
+91
-35
@@ -3,45 +3,58 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/plugin-syntax-flow": "^7.18.6",
|
||||
"@babel/plugin-transform-react-jsx": "^7.21.0",
|
||||
"@babel/core": "7.21.3",
|
||||
"@babel/plugin-syntax-flow": "7.18.6",
|
||||
"@babel/plugin-transform-react-jsx": "7.21.0",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#97b538693972e26f7c0f0f13ec6afab03b0e2e57",
|
||||
"@testing-library/dom": "^9.0.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/node": "^18.15.5",
|
||||
"@types/react": "^18.0.30",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"dompurify": "^3.0.1",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"ext-name": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.21",
|
||||
"react": "^18.2.0",
|
||||
"react-content-loader": "^6.2.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-is": "^18.2.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"@testing-library/dom": "9.0.1",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/node": "18.15.5",
|
||||
"@types/react": "18.0.30",
|
||||
"babel-plugin-styled-components": "2.0.7",
|
||||
"dompurify": "3.0.1",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"ext-name": "5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"postcss": "8.4.21",
|
||||
"electron-context-menu": "3.3.0",
|
||||
"electron-is-dev": "2.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-content-loader": "6.2.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-draggable": "4.4.5",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-infinite-scroller": "1.2.6",
|
||||
"react-is": "18.2.0",
|
||||
"react-markdown": "8.0.6",
|
||||
"mock-require": "3.0.3",
|
||||
"react-modal": "3.16.1",
|
||||
"react-router-dom": "6.9.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^9.1.1",
|
||||
"react-tooltip": "^5.10.0",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"styled-components": "^5.3.9",
|
||||
"web-vitals": "^3.3.0",
|
||||
"zustand": "^4.3.6"
|
||||
"react-toastify": "9.1.1",
|
||||
"react-tooltip": "5.10.0",
|
||||
"remark-breaks": "3.0.2",
|
||||
"styled-components": "5.3.9",
|
||||
"web-vitals": "3.3.0",
|
||||
"zustand": "4.3.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test"
|
||||
"test": "craco test",
|
||||
"electron": "yarn electron:before && electron .",
|
||||
"electron:no-delete-data": "yarn electron:before:download-ipfs && electron .",
|
||||
"electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"",
|
||||
"electron:start:no-delete-data": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron:no-delete-data\"",
|
||||
"electron:build:linux": "electron-builder build --publish never -l",
|
||||
"electron:build:windows": "electron-builder build --publish never -w",
|
||||
"electron:build:mac": "electron-builder build --publish never -m",
|
||||
"electron:before": "yarn electron:before:download-ipfs && yarn electron:before:delete-data",
|
||||
"electron:before:download-ipfs": "node electron/downloadIpfs",
|
||||
"electron:before:delete-data": "rimraf .plebbit"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -61,9 +74,52 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"main": "electron/main.js",
|
||||
"build": {
|
||||
"appId": "plebchan.desktop",
|
||||
"productName": "plebchan",
|
||||
"beforePack": "electron/beforePack.js",
|
||||
"afterAllArtifactBuild": "electron/afterAllArtifactBuild.js",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "bin/${os}",
|
||||
"to": "bin",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"electron/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extends": null,
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"category": "public.app-category.social-networking",
|
||||
"type": "distribution"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"portable",
|
||||
"nsis"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"category": "Network"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"typescript": "^5.0.2"
|
||||
"@craco/craco": "7.1.0",
|
||||
"concurrently": "8.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"decompress": "4.2.1",
|
||||
"electron": "19.0.1",
|
||||
"electron-builder": "23.0.9",
|
||||
"typescript": "5.0.2",
|
||||
"wait-on": "7.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"styled-components": "^5"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# deploy html to a server and then add html to ipfs
|
||||
|
||||
# go to current folder
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# add env vars
|
||||
if [ -f ../.env ]; then
|
||||
export $(echo $(cat ../.env | sed 's/#.*//g'| xargs) | envsubst)
|
||||
fi
|
||||
|
||||
# check creds
|
||||
if [ -z "${DEPLOY_HOST+xxx}" ]; then echo "DEPLOY_HOST not set" && exit; fi
|
||||
if [ -z "${DEPLOY_USER+xxx}" ]; then echo "DEPLOY_USER not set" && exit; fi
|
||||
if [ -z "${DEPLOY_PASSWORD+xxx}" ]; then echo "DEPLOY_PASSWORD not set" && exit; fi
|
||||
|
||||
# save version
|
||||
PLEBCHAN_VERSION=$(node -e "console.log(require('../package.json').version)")
|
||||
PLEBCHAN_HTML_NAME="plebchan-html-$PLEBCHAN_VERSION"
|
||||
|
||||
SCRIPT="
|
||||
# download html
|
||||
cd ~
|
||||
rm $PLEBCHAN_HTML_NAME.zip
|
||||
rm -fr $PLEBCHAN_HTML_NAME
|
||||
wget https://github.com/plebbit/plebchan/releases/download/v$PLEBCHAN_VERSION/$PLEBCHAN_HTML_NAME.zip || exit
|
||||
|
||||
# extract html
|
||||
unzip $PLEBCHAN_HTML_NAME.zip || exit
|
||||
rm $PLEBCHAN_HTML_NAME.zip || exit
|
||||
|
||||
# add to ipfs
|
||||
CID=\`ipfs add --recursive --pin --quieter $PLEBCHAN_HTML_NAME | tail -n 1\`
|
||||
ipfs pin add --recursive \"\$CID\"
|
||||
|
||||
# start ipfs daemon if not started
|
||||
ipfs init
|
||||
nohup ipfs daemon &
|
||||
|
||||
# the CID of plebchan html, add this CID to ENS
|
||||
sleep 3
|
||||
echo \"\"
|
||||
echo $PLEBCHAN_HTML_NAME \"CID: \$CID\"
|
||||
echo \"\"
|
||||
"
|
||||
|
||||
# execute script over ssh
|
||||
echo "$SCRIPT" | sshpass -p "$DEPLOY_PASSWORD" ssh "$DEPLOY_USER"@"$DEPLOY_HOST"
|
||||
@@ -4,6 +4,13 @@ import App from './App';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import PlebbitJs from '@plebbit/plebbit-js';
|
||||
// inject native functions into renderer
|
||||
// https://github.com/plebbit/plebbit-js/blob/master/docs/cross-platform-native-functions.md
|
||||
if (window.plebbitJsNativeFunctions) {
|
||||
PlebbitJs.setNativeFunctions(window.plebbitJsNativeFunctions);
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
|
||||
Reference in New Issue
Block a user