📦 Add Linux packaging (AppImage/Flatpak/DEB)
🧹 Minor Codebase cleanup ⚡ Implement automatic starting of the relay
This commit is contained in:
parent
89fcaa9aa6
commit
f402ff04ab
19 changed files with 519 additions and 119 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,6 +4,8 @@ logs
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
out
|
||||||
|
extras
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
29
electron-builder.yaml
Normal file
29
electron-builder.yaml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
appId: com.arx-ccn.eve
|
||||||
|
productName: Eve
|
||||||
|
executableName: Eve
|
||||||
|
icon: public/icon512x512.png
|
||||||
|
linux:
|
||||||
|
category: Network
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- flatpak
|
||||||
|
- deb
|
||||||
|
desktop:
|
||||||
|
desktopActions: {}
|
||||||
|
extraFiles:
|
||||||
|
- from: extras/linux/relay
|
||||||
|
to: usr/bin/eve-relay
|
||||||
|
flatpak:
|
||||||
|
runtimeVersion: "24.08"
|
||||||
|
license: "LICENSE"
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
51
electron.vite.config.ts
Normal file
51
electron.vite.config.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, "src/electron/main.ts"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, "src/electron/preload.ts"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
root: resolve(__dirname, "src"),
|
||||||
|
build: {
|
||||||
|
target: "es2024",
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, "src/index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@utils": fileURLToPath(new URL("./src/utils", import.meta.url)),
|
||||||
|
"@routes": fileURLToPath(new URL("./src/routes", import.meta.url)),
|
||||||
|
"@styles": fileURLToPath(new URL("./src/styles", import.meta.url)),
|
||||||
|
"@widgets": fileURLToPath(
|
||||||
|
new URL("./src/components/Widgets", import.meta.url)
|
||||||
|
),
|
||||||
|
"@components": fileURLToPath(
|
||||||
|
new URL("./src/components", import.meta.url)
|
||||||
|
),
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// server: {
|
||||||
|
// port: 5173,
|
||||||
|
// open: true,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
});
|
35
package.json
35
package.json
|
@ -1,16 +1,31 @@
|
||||||
{
|
{
|
||||||
"name": "eve",
|
"name": "eve",
|
||||||
"private": true,
|
"description": "Closed Community Networks",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"browserslist": ["not dead"],
|
"license": "AGPL-3.0-only",
|
||||||
|
"browserslist": [
|
||||||
|
"electron >= 22.0.0"
|
||||||
|
],
|
||||||
|
"main": "./out/main/main.js",
|
||||||
|
"homepage": "https://arx-ccn.com/eve",
|
||||||
|
"author": {
|
||||||
|
"name": "arx-ccn",
|
||||||
|
"email": "developers@arx-ccn.com"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"build": "tsc && electron-vite build",
|
||||||
"build": "tsc && vite build",
|
"build:linux": "bun run build && electron-builder --linux",
|
||||||
"preview": "vite preview"
|
"start": "electron-vite preview",
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"prebuild": "electron-vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
"electron-builder": "^25.1.8",
|
||||||
|
"electron-vite": "^3.0.0",
|
||||||
|
"electron": "^34.2.0",
|
||||||
"@tsconfig/node22": "^22.0.0",
|
"@tsconfig/node22": "^22.0.0",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
|
@ -21,11 +36,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lit-labs/motion": "^1.0.8",
|
"@lit-labs/motion": "^1.0.8",
|
||||||
|
"@noble/ciphers": "^1.2.1",
|
||||||
"@nostr-dev-kit/ndk": "^2.10.7",
|
"@nostr-dev-kit/ndk": "^2.10.7",
|
||||||
|
"@nostr/tools": "npm:@jsr/nostr__tools",
|
||||||
"@open-wc/lit-helpers": "^0.7.0",
|
"@open-wc/lit-helpers": "^0.7.0",
|
||||||
|
"@std/encoding": "npm:@jsr/std__encoding",
|
||||||
"iconify-icon": "^2.2.0",
|
"iconify-icon": "^2.2.0",
|
||||||
"lit": "^3.2.1",
|
"lit": "^3.2.1",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0"
|
||||||
"nostr-tools": "^2.10.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
public/icon512x512.png
Normal file
BIN
public/icon512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
|
@ -1,12 +1,14 @@
|
||||||
import { LitElement, html, css } from "lit";
|
import { LitElement, html, css } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { animate } from "@lit-labs/motion";
|
import { animate } from "@lit-labs/motion";
|
||||||
import * as nostrTools from "nostr-tools/pure";
|
import * as nostrTools from "@nostr/tools/pure";
|
||||||
import * as nip06 from "nostr-tools/nip06";
|
import * as nip06 from "@nostr/tools/nip06";
|
||||||
import * as nip19 from "nostr-tools/nip19";
|
import * as nip19 from "@nostr/tools/nip19";
|
||||||
import * as nip49 from "nostr-tools/nip49";
|
import * as nip49 from "@nostr/tools/nip49";
|
||||||
import { ndk, setSigner } from "@/ndk";
|
import { ndk, setSigner } from "@/ndk";
|
||||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { encodeBase64 } from "@std/encoding/base64";
|
||||||
|
import { randomBytes } from "@noble/ciphers/webcrypto";
|
||||||
|
|
||||||
@customElement("arx-initial-setup")
|
@customElement("arx-initial-setup")
|
||||||
export class InitialSetup extends LitElement {
|
export class InitialSetup extends LitElement {
|
||||||
|
@ -17,6 +19,15 @@ export class InitialSetup extends LitElement {
|
||||||
@state() private profileImage = "";
|
@state() private profileImage = "";
|
||||||
@state() private lightningAddress = "";
|
@state() private lightningAddress = "";
|
||||||
|
|
||||||
|
get encryptionPassphrase() {
|
||||||
|
let encryptionPassphrase = localStorage.getItem("encryption_key");
|
||||||
|
if (!encryptionPassphrase) {
|
||||||
|
encryptionPassphrase = encodeBase64(randomBytes(32));
|
||||||
|
localStorage.setItem("encryption_key", encryptionPassphrase);
|
||||||
|
}
|
||||||
|
return encryptionPassphrase;
|
||||||
|
}
|
||||||
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -409,32 +420,9 @@ export class InitialSetup extends LitElement {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSetupCode() {
|
private async startRelay() {
|
||||||
const userAgent = navigator.userAgent.toLowerCase();
|
await window.relay.writeSeed(this.seedPhrase);
|
||||||
if (userAgent.includes("mac")) {
|
await window.relay.start(this.encryptionPassphrase);
|
||||||
return `
|
|
||||||
mkdir -p ~/.config/arx/eve && cd ~/.config/arx/eve
|
|
||||||
echo "${this.seedPhrase}" > ccn.seed
|
|
||||||
launchctl load ~/Library/LaunchAgents/com.user.eve-relay.plist
|
|
||||||
launchctl start com.user.eve-relay
|
|
||||||
`
|
|
||||||
.split("\n")
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userAgent.includes("linux")) {
|
|
||||||
return `
|
|
||||||
mkdir -p ~/.config/arx/eve && cd ~/.config/arx/eve
|
|
||||||
echo "${this.seedPhrase}" > ccn.seed
|
|
||||||
systemctl --user enable eve-relay.service
|
|
||||||
systemctl --user start eve-relay.service
|
|
||||||
`
|
|
||||||
.split("\n")
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
return "Unsupported OS";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPageThree() {
|
private renderPageThree() {
|
||||||
|
@ -446,11 +434,10 @@ export class InitialSetup extends LitElement {
|
||||||
During this alpha phase, manual relay configuration is required.
|
During this alpha phase, manual relay configuration is required.
|
||||||
This process will be automated in future releases.
|
This process will be automated in future releases.
|
||||||
</p>
|
</p>
|
||||||
<p>Open your terminal and run following commands:</p>
|
<p>Please press the button below to start the relay.</p>
|
||||||
<pre>
|
<button @click=${() => this.startRelay()} class="button primary">
|
||||||
<code>${this.getSetupCode()}</code>
|
Start Relay
|
||||||
</pre
|
</button>
|
||||||
>
|
|
||||||
<p>
|
<p>
|
||||||
Having trouble? Our team is here to help if you encounter any
|
Having trouble? Our team is here to help if you encounter any
|
||||||
issues.
|
issues.
|
||||||
|
@ -586,15 +573,11 @@ export class InitialSetup extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async goToFinalStep() {
|
private async goToFinalStep() {
|
||||||
let encryptionPassphrase = localStorage.getItem("encryption_key");
|
|
||||||
if (!encryptionPassphrase) {
|
|
||||||
encryptionPassphrase =
|
|
||||||
Math.random().toString(36).substring(2, 15) +
|
|
||||||
Math.random().toString(36).substring(2, 15);
|
|
||||||
localStorage.setItem("encryption_key", encryptionPassphrase);
|
|
||||||
}
|
|
||||||
const randomPrivateKey = nostrTools.generateSecretKey();
|
const randomPrivateKey = nostrTools.generateSecretKey();
|
||||||
const encryptedNsec = nip49.encrypt(randomPrivateKey, encryptionPassphrase);
|
const encryptedNsec = nip49.encrypt(
|
||||||
|
randomPrivateKey,
|
||||||
|
this.encryptionPassphrase
|
||||||
|
);
|
||||||
const npub = nip19.npubEncode(nostrTools.getPublicKey(randomPrivateKey));
|
const npub = nip19.npubEncode(nostrTools.getPublicKey(randomPrivateKey));
|
||||||
|
|
||||||
if (!this.lightningAddress) this.lightningAddress = `${npub}@npub.cash`;
|
if (!this.lightningAddress) this.lightningAddress = `${npub}@npub.cash`;
|
||||||
|
@ -602,6 +585,7 @@ export class InitialSetup extends LitElement {
|
||||||
localStorage.setItem("ncryptsec", encryptedNsec);
|
localStorage.setItem("ncryptsec", encryptedNsec);
|
||||||
|
|
||||||
setSigner(new NDKPrivateKeySigner(randomPrivateKey));
|
setSigner(new NDKPrivateKeySigner(randomPrivateKey));
|
||||||
|
await ndk.connect(5000);
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
const event = new NDKEvent(ndk);
|
||||||
event.kind = NDKKind.Metadata;
|
event.kind = NDKKind.Metadata;
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { css, LitElement } from 'lit';
|
import { css, LitElement } from "lit";
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
import MarkdownIt, { type StateCore, type Token } from 'markdown-it';
|
import MarkdownIt, { type StateCore, type Token } from "markdown-it";
|
||||||
|
|
||||||
function nostrPlugin(md: MarkdownIt): void {
|
function nostrPlugin(md: MarkdownIt): void {
|
||||||
const npubRegex = /(npub[0-9a-zA-Z]{59})/g;
|
const npubRegex = /(npub[0-9a-zA-Z]{59})/g;
|
||||||
|
|
||||||
md.core.ruler.after('inline', 'nostr_npub', (state: StateCore): boolean => {
|
md.core.ruler.after("inline", "nostr_npub", (state: StateCore): boolean => {
|
||||||
for (const token of state.tokens) {
|
for (const token of state.tokens) {
|
||||||
if (token.type === 'inline' && token.children) {
|
if (token.type === "inline" && token.children) {
|
||||||
for (let i = 0; i < token.children.length; i++) {
|
for (let i = 0; i < token.children.length; i++) {
|
||||||
const child = token.children[i];
|
const child = token.children[i];
|
||||||
|
|
||||||
if (child.type === 'text') {
|
if (child.type === "text") {
|
||||||
const matches = child.content.match(npubRegex);
|
const matches = child.content.match(npubRegex);
|
||||||
if (!matches) continue;
|
if (!matches) continue;
|
||||||
|
|
||||||
|
@ -21,28 +21,29 @@ function nostrPlugin(md: MarkdownIt): void {
|
||||||
|
|
||||||
child.content.replace(
|
child.content.replace(
|
||||||
npubRegex,
|
npubRegex,
|
||||||
|
// @ts-ignore this is an issue with the types
|
||||||
(match: string, npub: string, offset: number) => {
|
(match: string, npub: string, offset: number) => {
|
||||||
if (offset > lastIndex) {
|
if (offset > lastIndex) {
|
||||||
const textToken = new state.Token('text', '', 0);
|
const textToken = new state.Token("text", "", 0);
|
||||||
textToken.content = child.content.slice(lastIndex, offset);
|
textToken.content = child.content.slice(lastIndex, offset);
|
||||||
newTokens.push(textToken);
|
newTokens.push(textToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkOpen = new state.Token('link_open', 'a', 1);
|
const linkOpen = new state.Token("link_open", "a", 1);
|
||||||
linkOpen.attrs = [['href', `nostr:${npub}`]];
|
linkOpen.attrs = [["href", `nostr:${npub}`]];
|
||||||
|
|
||||||
const text = new state.Token('text', '', 0);
|
const text = new state.Token("text", "", 0);
|
||||||
text.content = npub;
|
text.content = npub;
|
||||||
|
|
||||||
const linkClose = new state.Token('link_close', 'a', -1);
|
const linkClose = new state.Token("link_close", "a", -1);
|
||||||
|
|
||||||
newTokens.push(linkOpen, text, linkClose);
|
newTokens.push(linkOpen, text, linkClose);
|
||||||
lastIndex = offset + match.length;
|
lastIndex = offset + match.length;
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (lastIndex < child.content.length) {
|
if (lastIndex < child.content.length) {
|
||||||
const textToken = new state.Token('text', '', 0);
|
const textToken = new state.Token("text", "", 0);
|
||||||
textToken.content = child.content.slice(lastIndex);
|
textToken.content = child.content.slice(lastIndex);
|
||||||
newTokens.push(textToken);
|
newTokens.push(textToken);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +59,7 @@ function nostrPlugin(md: MarkdownIt): void {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement('arx-markdown-content')
|
@customElement("arx-markdown-content")
|
||||||
export class MarkdownContent extends LitElement {
|
export class MarkdownContent extends LitElement {
|
||||||
private md = new MarkdownIt({
|
private md = new MarkdownIt({
|
||||||
html: false,
|
html: false,
|
||||||
|
@ -68,7 +69,7 @@ export class MarkdownContent extends LitElement {
|
||||||
});
|
});
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
content = '';
|
content = "";
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
css`
|
css`
|
||||||
|
@ -94,7 +95,16 @@ export class MarkdownContent extends LitElement {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, code, ul, ol, blockquote {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
code,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
77
src/electron/main.ts
Normal file
77
src/electron/main.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { app, shell, BrowserWindow, ipcMain } from "electron";
|
||||||
|
import { optimizer, is } from "@electron-toolkit/utils";
|
||||||
|
import { RelayManager } from "./relayManager";
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
const relay = new RelayManager();
|
||||||
|
|
||||||
|
ipcMain.handle("relay:writeSeed", (_, ...args: any) => {
|
||||||
|
if (!args[0]) throw new Error("No seed provided");
|
||||||
|
const seed = args[0] as string;
|
||||||
|
const configPath = path.join(os.homedir(), ".config", "arx", "Eve");
|
||||||
|
const seedPath = path.join(configPath, "ccn.seed");
|
||||||
|
fs.mkdirSync(configPath, { recursive: true });
|
||||||
|
fs.writeFileSync(seedPath, seed);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("relay:start", (_, ...args: any) => {
|
||||||
|
if (!args[0]) throw new Error("No encryption key provided");
|
||||||
|
const encryptionKey = args[0] as string;
|
||||||
|
return relay.start(encryptionKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("relay:stop", () => {
|
||||||
|
return relay.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("relay:status", () => {
|
||||||
|
return {
|
||||||
|
running: relay.isRunning,
|
||||||
|
pid: relay.pid,
|
||||||
|
logs: relay.getLogs(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("relay:getLogs", () => {
|
||||||
|
return relay.getLogs();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createWindow(): void {
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: 1024,
|
||||||
|
height: 768,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/preload.mjs"),
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on("ready-to-show", () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"])
|
||||||
|
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
||||||
|
else mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
app.quit();
|
||||||
|
});
|
21
src/electron/preload.ts
Normal file
21
src/electron/preload.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
import { electronAPI } from "@electron-toolkit/preload";
|
||||||
|
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||||
|
contextBridge.exposeInMainWorld("relay", {
|
||||||
|
writeSeed: (seed: string) => ipcRenderer.invoke("relay:writeSeed", seed),
|
||||||
|
start: (encryptionKey: string) =>
|
||||||
|
ipcRenderer.invoke("relay:start", encryptionKey),
|
||||||
|
stop: () => ipcRenderer.invoke("relay:stop"),
|
||||||
|
getStatus: () => ipcRenderer.invoke("relay:status"),
|
||||||
|
getLogs: () => ipcRenderer.invoke("relay:logs"),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.electron = electronAPI;
|
||||||
|
}
|
207
src/electron/relayManager.ts
Normal file
207
src/electron/relayManager.ts
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
import { spawn, ChildProcess } from "child_process";
|
||||||
|
import { join } from "path";
|
||||||
|
import { is } from "@electron-toolkit/utils";
|
||||||
|
|
||||||
|
type PackageEnvironment = "flatpak" | "appimage" | "system" | "dev";
|
||||||
|
|
||||||
|
export class RelayManager {
|
||||||
|
private process: ChildProcess | null;
|
||||||
|
private readonly relayPath: string;
|
||||||
|
private isShuttingDown: boolean;
|
||||||
|
private restartAttempts: number;
|
||||||
|
private readonly maxRestartAttempts: number;
|
||||||
|
private readonly restartDelay: number;
|
||||||
|
private restartTimeout: NodeJS.Timeout | null;
|
||||||
|
private relayLogs: string[];
|
||||||
|
private readonly maxLogs: number;
|
||||||
|
private encryptionKey: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the relay is currently running.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if the relay is running, otherwise false.
|
||||||
|
*/
|
||||||
|
get isRunning(): boolean {
|
||||||
|
return !!this.process;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the process identifier (PID) of the running relay process.
|
||||||
|
*
|
||||||
|
* @returns {number | undefined} The PID of the relay process if it is running, otherwise undefined.
|
||||||
|
*/
|
||||||
|
|
||||||
|
get pid(): number | undefined {
|
||||||
|
return this.process?.pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(maxRestartAttempts = 5, restartDelay = 1000, maxLogs = 100) {
|
||||||
|
this.process = null;
|
||||||
|
this.relayPath = this.getRelayPath();
|
||||||
|
this.isShuttingDown = false;
|
||||||
|
this.restartAttempts = 0;
|
||||||
|
this.maxRestartAttempts = maxRestartAttempts;
|
||||||
|
this.restartDelay = restartDelay;
|
||||||
|
this.restartTimeout = null;
|
||||||
|
this.relayLogs = [];
|
||||||
|
this.maxLogs = maxLogs;
|
||||||
|
this.encryptionKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectEnvironment(): PackageEnvironment {
|
||||||
|
if (is.dev) return "dev";
|
||||||
|
if (process.env.FLATPAK_ID) return "flatpak";
|
||||||
|
if (process.env.APPIMAGE) return "appimage";
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRelayPath(): string {
|
||||||
|
const environment = this.detectEnvironment();
|
||||||
|
|
||||||
|
switch (environment) {
|
||||||
|
case "dev":
|
||||||
|
return join(__dirname, "../../extras/linux/relay");
|
||||||
|
case "flatpak":
|
||||||
|
return "/app/lib/com.arx_ccn.eve/usr/bin/eve-relay";
|
||||||
|
case "appimage":
|
||||||
|
return join(process.env.APPDIR || "", "usr/bin/eve-relay");
|
||||||
|
case "system":
|
||||||
|
return "/usr/bin/eve-relay";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleProcessExit(code: number | null): void {
|
||||||
|
this.process = null;
|
||||||
|
console.log(`Relay exited with code ${code}`);
|
||||||
|
|
||||||
|
if (!this.isShuttingDown) this.restartProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
private restartProcess(): void {
|
||||||
|
if (this.restartAttempts >= this.maxRestartAttempts) {
|
||||||
|
console.error(
|
||||||
|
`Failed to restart relay after ${this.maxRestartAttempts} attempts`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restartAttempts++;
|
||||||
|
console.log(
|
||||||
|
`Attempting restart #${this.restartAttempts} in ${this.restartDelay}ms...`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.restartTimeout) clearTimeout(this.restartTimeout);
|
||||||
|
|
||||||
|
this.restartTimeout = setTimeout(() => {
|
||||||
|
this.start(this.encryptionKey!);
|
||||||
|
}, this.restartDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addLog(data: string): void {
|
||||||
|
this.relayLogs.push(data);
|
||||||
|
if (this.relayLogs.length > this.maxLogs) {
|
||||||
|
this.relayLogs = this.relayLogs.slice(-this.maxLogs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the Eve Relay.
|
||||||
|
*
|
||||||
|
* If the process is already running, do nothing.
|
||||||
|
*
|
||||||
|
* Logs from the Relay process are captured and can be retrieved with the
|
||||||
|
* `getLogs()` method.
|
||||||
|
*
|
||||||
|
* If the process exits unexpectedly, it will be restarted according to the
|
||||||
|
* configured restart policy.
|
||||||
|
*
|
||||||
|
* @param {string} encryptionKey - The key to use for encrypting data saved in
|
||||||
|
* the Relay.
|
||||||
|
*/
|
||||||
|
public start(encryptionKey: string): void {
|
||||||
|
if (this.process) return;
|
||||||
|
|
||||||
|
this.encryptionKey = encryptionKey;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.process = spawn(this.relayPath, [], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH,
|
||||||
|
PATH: process.env.PATH,
|
||||||
|
ENCRYPTION_KEY: encryptionKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.process.stdout) {
|
||||||
|
this.process.stdout.on("data", (data: Buffer) => {
|
||||||
|
const logLine = data.toString().trim();
|
||||||
|
this.addLog(logLine);
|
||||||
|
console.log(logLine);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.process.stderr) {
|
||||||
|
this.process.stderr.on("data", (data: Buffer) => {
|
||||||
|
const logLine = data.toString().trim();
|
||||||
|
this.addLog(logLine);
|
||||||
|
console.error(logLine);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.process.on("error", (err: Error) => {
|
||||||
|
console.error(`Failed to start Relay: ${err.message}`);
|
||||||
|
this.process = null;
|
||||||
|
this.restartProcess();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process.on("exit", this.handleProcessExit.bind(this));
|
||||||
|
|
||||||
|
if (this.process.pid) {
|
||||||
|
this.restartAttempts = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error starting Relay: ${
|
||||||
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
this.restartProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Eve Relay.
|
||||||
|
*
|
||||||
|
* If the Relay process is currently running, it will be terminated.
|
||||||
|
*
|
||||||
|
* If the process is not running, do nothing.
|
||||||
|
*
|
||||||
|
* All pending restarts will be cancelled.
|
||||||
|
*/
|
||||||
|
public stop(): void {
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
|
||||||
|
if (this.restartTimeout) {
|
||||||
|
clearTimeout(this.restartTimeout);
|
||||||
|
this.restartTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.process) {
|
||||||
|
this.process.kill();
|
||||||
|
this.process = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of all the log lines the Relay has written to date.
|
||||||
|
*
|
||||||
|
* The returned array is a copy of the internal log state, and will not be
|
||||||
|
* modified by future log activity.
|
||||||
|
*
|
||||||
|
* @returns A copy of all log lines from the Relay since it was last started.
|
||||||
|
*/
|
||||||
|
public getLogs(): string[] {
|
||||||
|
return [...this.relayLogs];
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<script src="/src/main.ts" type="module"></script>
|
<script src="main.ts" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
16
src/main.ts
16
src/main.ts
|
@ -7,8 +7,22 @@ import "@components/NostrProfile";
|
||||||
import "@components/Breadcrumbs";
|
import "@components/Breadcrumbs";
|
||||||
import "@components/Header";
|
import "@components/Header";
|
||||||
import "@routes/router";
|
import "@routes/router";
|
||||||
|
import "@components/LoadingView";
|
||||||
import type EveRouter from "@routes/router";
|
import type EveRouter from "@routes/router";
|
||||||
|
import { sleep } from "./utils/sleep";
|
||||||
|
|
||||||
|
async function startRelay() {
|
||||||
|
if (localStorage.getItem("ncryptsec")) {
|
||||||
|
const loadingIndicator = document.createElement("arx-loading-view");
|
||||||
|
document.body.appendChild(loadingIndicator);
|
||||||
|
await window.relay.start(localStorage.getItem("encryption_key")!);
|
||||||
|
await sleep(5000);
|
||||||
|
loadingIndicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startRelay().then(() => {
|
||||||
const router = document.createElement("arx-eve-router") as EveRouter;
|
const router = document.createElement("arx-eve-router") as EveRouter;
|
||||||
router.ccnSetup = localStorage.getItem("ncryptsec");
|
router.ccnSetup = !!localStorage.getItem("ncryptsec");
|
||||||
document.body.appendChild(router);
|
document.body.appendChild(router);
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import NDK, { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import NDK, { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
import * as nip49 from "nostr-tools/nip49";
|
import * as nip49 from "@nostr/tools/nip49";
|
||||||
|
|
||||||
export const ndk = new NDK({
|
export const ndk = new NDK({
|
||||||
explicitRelayUrls: ["ws://localhost:6942"],
|
explicitRelayUrls: ["ws://localhost:6942"],
|
||||||
|
@ -13,8 +13,9 @@ export async function getSigner() {
|
||||||
await ndk.connect();
|
await ndk.connect();
|
||||||
if (ndk.signer) return;
|
if (ndk.signer) return;
|
||||||
const encryptionPassphrase = localStorage.getItem("encryption_key");
|
const encryptionPassphrase = localStorage.getItem("encryption_key");
|
||||||
|
if (!encryptionPassphrase) throw new Error("Encryption passphrase not found");
|
||||||
const signer = new NDKPrivateKeySigner(
|
const signer = new NDKPrivateKeySigner(
|
||||||
nip49.decrypt(localStorage.getItem("ncryptsec"), encryptionPassphrase)
|
nip49.decrypt(localStorage.getItem("ncryptsec")!, encryptionPassphrase)
|
||||||
);
|
);
|
||||||
setSigner(signer);
|
setSigner(signer);
|
||||||
}
|
}
|
||||||
|
|
21
src/relayManager.d.ts
vendored
Normal file
21
src/relayManager.d.ts
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
interface RelayStatus {
|
||||||
|
running: boolean;
|
||||||
|
pid: number | null;
|
||||||
|
logs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelayBridge {
|
||||||
|
writeSeed: (seed: string) => Promise<void>;
|
||||||
|
start: (encryptionKey: string) => Promise<void>;
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
getStatus: () => Promise<RelayStatus>;
|
||||||
|
getLogs: () => Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
relay: RelayBridge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
|
@ -1,7 +1,6 @@
|
||||||
import { LitElement, html, css } from "lit";
|
import { LitElement, html, css } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { when } from "lit/directives/when.js";
|
import { when } from "lit/directives/when.js";
|
||||||
import { styleMap } from "lit/directives/style-map.js";
|
|
||||||
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
|
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||||
import { getUserProfile } from "../ndk";
|
import { getUserProfile } from "../ndk";
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default class EveRouter extends LitElement {
|
||||||
private currentIndex = -1;
|
private currentIndex = -1;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
private ccnSetup = false;
|
public ccnSetup = false;
|
||||||
|
|
||||||
private beforeEachGuards: ((to: Route, from: Route | null) => boolean)[] = [];
|
private beforeEachGuards: ((to: Route, from: Route | null) => boolean)[] = [];
|
||||||
private afterEachHooks: ((to: Route, from: Route | null) => void)[] = [];
|
private afterEachHooks: ((to: Route, from: Route | null) => void)[] = [];
|
||||||
|
@ -116,6 +116,8 @@ export default class EveRouter extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.initializeRouter();
|
this.initializeRouter();
|
||||||
|
if (this.ccnSetup)
|
||||||
|
window.relay.start(localStorage.getItem("encryption_key")!);
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback(): void {
|
override connectedCallback(): void {
|
||||||
|
@ -276,7 +278,7 @@ export default class EveRouter extends LitElement {
|
||||||
?canGoBack=${this.currentIndex > 0}
|
?canGoBack=${this.currentIndex > 0}
|
||||||
?canGoForward=${this.currentIndex < this.history.length - 1}
|
?canGoForward=${this.currentIndex < this.history.length - 1}
|
||||||
url="eve://${this.currentPath}"
|
url="eve://${this.currentPath}"
|
||||||
@navigate=${(e) => this.navigate(e.detail)}
|
@navigate=${(e: CustomEvent) => this.navigate(e.detail)}
|
||||||
@go-back=${this.goBack}
|
@go-back=${this.goBack}
|
||||||
@go-forward=${this.goForward}
|
@go-forward=${this.goForward}
|
||||||
title="Eve"
|
title="Eve"
|
||||||
|
|
3
src/utils/sleep.ts
Normal file
3
src/utils/sleep.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
|
@ -31,5 +31,6 @@
|
||||||
"@components/*": ["./src/components/*"],
|
"@components/*": ["./src/components/*"],
|
||||||
"@widgets/*": ["./src/components/Widgets/*"]
|
"@widgets/*": ["./src/components/Widgets/*"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"include": ["./src/**/*"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { fileURLToPath, URL } from "node:url";
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import { resolve } from "path";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
target: "es2024",
|
|
||||||
outDir: "dist",
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
main: resolve(__dirname, "index.html"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@utils": fileURLToPath(new URL("./src/utils", import.meta.url)),
|
|
||||||
"@routes": fileURLToPath(new URL("./src/routes", import.meta.url)),
|
|
||||||
"@styles": fileURLToPath(new URL("./src/styles", import.meta.url)),
|
|
||||||
"@widgets": fileURLToPath(
|
|
||||||
new URL("./src/components/Widgets", import.meta.url)
|
|
||||||
),
|
|
||||||
"@components": fileURLToPath(
|
|
||||||
new URL("./src/components", import.meta.url)
|
|
||||||
),
|
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 5173,
|
|
||||||
open: true,
|
|
||||||
},
|
|
||||||
optimizeDeps: {
|
|
||||||
esbuildOptions: {
|
|
||||||
target: "es2024",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
Loading…
Add table
Reference in a new issue