Merge pull request 'Start using the pzp-sdk' (#9) from sdk into master

Reviewed-on: https://codeberg.org/pzp/zooboard/pulls/9
This commit is contained in:
Powersource 2024-05-20 15:45:26 +00:00
commit 992eb762d6
3 changed files with 236 additions and 260 deletions

435
main.js
View File

@ -2,8 +2,8 @@ const { app, BrowserWindow, ipcMain, shell, clipboard } = require('electron')
const Path = require('node:path') const Path = require('node:path')
const URL = require('node:url') const URL = require('node:url')
const p = require('node:util').promisify const p = require('node:util').promisify
const Keypair = require('pzp-keypair')
const awaitable = require('pull-awaitable') const awaitable = require('pull-awaitable')
const { createPeer } = require('pzp-sdk')
// WARNING monkey patch! ------------------------------------------------------- // WARNING monkey patch! -------------------------------------------------------
const na = require('sodium-native') const na = require('sodium-native')
@ -23,283 +23,218 @@ app.setPath('userData', process.env.ZOOBOARD_DATA)
const path = Path.resolve(app.getPath('userData'), 'pzp') const path = Path.resolve(app.getPath('userData'), 'pzp')
console.log("Appdata path:", process.env.ZOOBOARD_DATA) console.log("Appdata path:", process.env.ZOOBOARD_DATA)
const keypairPath = Path.join(path, 'keypair.json')
const keypair = Keypair.loadOrCreateSync(keypairPath)
let mainWindow let mainWindow
let globalAccountID = null
let globalAccountName = null let globalAccountName = null
const peer = require('secret-stack/bare')() createPeer({ path }).then(({ peer, account: globalAccountID }) => {
.use(require('secret-stack/plugins/net')) function createWindow() {
.use(require('secret-handshake-ext/secret-stack')) mainWindow = new BrowserWindow({
.use(require('pzp-net')) width: 1200,
.use(require('pzp-db')) height: 1200,
.use(require('pzp-set')) title: 'Zooboard',
.use(require('pzp-dict')) webPreferences: {
.use(require('pzp-goals')) preload: Path.join(__dirname, 'preload.js'),
.use(require('pzp-sync'))
.use(require('pzp-gc'))
.use(require('pzp-conductor'))
.use(require('pzp-hub-client'))
.use(require('pzp-promise'))
.use(require('pzp-invite'))
.call(null, {
shse: {
caps: require('pzp-caps'),
},
global: {
keypair,
path,
timers: {
inactivity: 10 * 60e3,
}, },
connections: {
incoming: {
tunnel: [{ transform: 'shse', scope: 'public' }],
},
outgoing: {
net: [{ transform: 'shse' }],
tunnel: [{ transform: 'shse' }],
},
},
},
net: {
autostart: false,
},
})
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 1200,
title: 'Zooboard',
webPreferences: {
preload: Path.join(__dirname, 'preload.js'),
},
})
const startUrl =
process.env.ELECTRON_START_URL ??
URL.format({
pathname: Path.join(__dirname, '/../build/index.html'),
protocol: 'file:',
slashes: true,
}) })
mainWindow.loadURL(startUrl)
mainWindow.webContents.openDevTools({ mode: 'bottom', activate: true }) const startUrl =
process.env.ELECTRON_START_URL ??
URL.format({
pathname: Path.join(__dirname, '/../build/index.html'),
protocol: 'file:',
slashes: true,
})
mainWindow.loadURL(startUrl)
// open Web URLs in the default system browser mainWindow.webContents.openDevTools({ mode: 'bottom', activate: true })
mainWindow.webContents.on('new-window', (ev, url) => {
ev.preventDefault()
shell.openExternal(url)
})
}
async function loadAccount() { // open Web URLs in the default system browser
if (globalAccountID !== null) { mainWindow.webContents.on('new-window', (ev, url) => {
return { id: globalAccountID, name: globalAccountName } ev.preventDefault()
} shell.openExternal(url)
// Kickstart
await peer.db.loaded()
const id = await p(peer.db.account.findOrCreate)({ subdomain: 'person' })
globalAccountID = id
await p(peer.set.load)(id)
await p(peer.dict.load)(id)
// Read profile
const profile = await p(peer.dict.read)(id, 'profile')
const name = profile?.name ?? ''
globalAccountName = name
await peer.net.start()
return { id, name }
}
async function setProfileName(ev, name) {
await p(peer.dict.update)('profile', { name })
return name
}
async function writeElements(ev, elements) {
if (globalAccountID === null) throw new Error('account not loaded')
for (const element of elements) {
await p(peer.db.feed.publish)({
account: globalAccountID,
domain: 'zooboardElements',
data: element,
}) })
} }
}
async function createInvite() { async function loadAccount() {
if (globalAccountID === null) throw new Error('account not loaded') if (globalAccountName !== null) {
let { url } = await p(peer.invite.createForFriend)({ return { id: globalAccountID, name: globalAccountName }
hubs: 1, }
id: globalAccountID,
})
// if the hub is on localhost, it's probably on the default port of 3000, so let's make things a bit easier for the user
if (url.indexOf('0.0.0.0') !== -1) url = url.replace("0.0.0.0", "0.0.0.0:3000")
return url
}
function copyToClipboard(ev, text) { // Read profile
clipboard.writeText(text) const profile = await p(peer.dict.read)(globalAccountID, 'profile')
} const name = profile?.name ?? ''
globalAccountName = name
let hasSubscribedToReadElements = false return { id: globalAccountID, name }
async function subscribeToReadElements() { }
if (hasSubscribedToReadElements) return
hasSubscribedToReadElements = true
// Load initial elements and inform renderer async function setProfileName(ev, name) {
const elementsByID = new Map() await p(peer.dict.update)('profile', { name })
const msgIDToElemID = new Map() return name
for await (const { id: msgID, msg } of peer.db.records()) { }
if (msg.data && msg.metadata.domain === 'zooboardElements') {
const { id: elemID, isDeleted } = msg.data async function writeElements(ev, elements) {
if (isDeleted) { if (globalAccountID === null) throw new Error('account not loaded')
elementsByID.delete(elemID) for (const element of elements) {
} else { await p(peer.db.feed.publish)({
msgIDToElemID.set(msgID, elemID) account: globalAccountID,
elementsByID.set(elemID, msg.data) domain: 'zooboardElements',
} data: element,
})
} }
} }
const initialElements = [...elementsByID.values()]
mainWindow.webContents.send('readElements', initialElements)
// Subscribe to new elements and inform renderer async function createInvite() {
peer.db.onRecordAdded(({ id: msgID, msg }) => { if (globalAccountID === null) throw new Error('account not loaded')
if (msg.data && msg.metadata.domain === 'zooboardElements') { let { url } = await p(peer.invite.createForFriend)({
const { id: elemID, isDeleted } = msg.data hubs: 1,
if (isDeleted) { id: globalAccountID,
elementsByID.delete(elemID) })
} else { // if the hub is on localhost, it's probably on the default port of 3000, so let's make things a bit easier for the user
msgIDToElemID.set(msgID, elemID) if (url.indexOf('0.0.0.0') !== -1) url = url.replace("0.0.0.0", "0.0.0.0:3000")
elementsByID.set(elemID, msg.data) return url
}
mainWindow.webContents.send('readElements', [msg.data])
}
})
// Subscribe to deleted elements and inform renderer
peer.db.onRecordDeletedOrErased((msgID) => {
const elemID = msgIDToElemID.get(msgID)
if (!elemID) return
msgIDToElemID.delete(msgID)
// Is there some other msgID that supports this elemID? If so, bail out
for (const [, remainingElemID] of msgIDToElemID) {
if (remainingElemID === elemID) {
return
}
}
// If not, delete the element
elementsByID.delete(elemID)
mainWindow.webContents.send('readElements', [
{ id: elemID, isDeleted: true },
])
})
// Finally safe to kickstart replication and garbage collection
setTimeout(() => {
peer.conductor.start(
globalAccountID,
[
['profile@dict', 'zooboardElements@newest-100', 'hubs@set'],
['profile@dict', 'zooboardElements@newest-100'],
],
64_000,
(err) => {
if (err) console.error('Starting conductor failed:', err)
}
)
}, 32)
}
let hasSubscribedToConnections = false
async function subscribeToConnections() {
if (hasSubscribedToConnections) return
hasSubscribedToConnections = true
for await (const connections of awaitable(peer.net.peers())) {
mainWindow.webContents.send('connections', connections)
} }
}
async function handlePZPUri(ev, uri) { function copyToClipboard(ev, text) {
if (!globalAccountID) { clipboard.writeText(text)
setTimeout(handlePZPUri, 100, null, uri)
return
} }
if (uri.startsWith("http:") || uri.startsWith("https://")) {
uri = decodeURIComponent(uri.split('/invite#')[1]) let hasSubscribedToReadElements = false
} async function subscribeToReadElements() {
if (!uri.startsWith('pzp://')) return console.log('Not a pzp invite URI', uri) if (hasSubscribedToReadElements) return
const commands = peer.invite.parse(uri) hasSubscribedToReadElements = true
for (const command of commands) {
console.log('Executing command', JSON.stringify(command)) // Load initial elements and inform renderer
switch (command.type) { const elementsByID = new Map()
case 'join': { const msgIDToElemID = new Map()
try { for await (const { id: msgID, msg } of peer.db.records()) {
await p(peer.hubClient.addHub)(command.multiaddr) if (msg.data && msg.metadata.domain === 'zooboardElements') {
} catch (err) { const { id: elemID, isDeleted } = msg.data
console.error('Failed to properly join hub', err) if (isDeleted) {
elementsByID.delete(elemID)
} else {
msgIDToElemID.set(msgID, elemID)
elementsByID.set(elemID, msg.data)
} }
break
} }
case 'follow': { }
await p(peer.set.add)('follows', command.id) const initialElements = [...elementsByID.values()]
break mainWindow.webContents.send('readElements', initialElements)
}
case 'promise.follow': { // Subscribe to new elements and inform renderer
const [issuerType, issuerPubkey] = command.issuer peer.db.onRecordAdded(({ id: msgID, msg }) => {
if (issuerType !== 'pubkey') { if (msg.data && msg.metadata.domain === 'zooboardElements') {
throw new Error(`Dont know how to claim a ${issuerType} promise`) const { id: elemID, isDeleted } = msg.data
if (isDeleted) {
elementsByID.delete(elemID)
} else {
msgIDToElemID.set(msgID, elemID)
elementsByID.set(elemID, msg.data)
} }
// eslint-disable-next-line no-loop-func mainWindow.webContents.send('readElements', [msg.data])
peer.addListener('rpc:connect', function onConnect(rpc) { }
if (rpc.shse.pubkey === issuerPubkey) { })
peer.removeListener('rpc:connect', onConnect)
rpc.promise.follow(command.token, globalAccountID, (err) => { // Subscribe to deleted elements and inform renderer
if (err) return console.error('Failed to use follow promise', err) peer.db.onRecordDeletedOrErased((msgID) => {
}) const elemID = msgIDToElemID.get(msgID)
if (!elemID) return
msgIDToElemID.delete(msgID)
// Is there some other msgID that supports this elemID? If so, bail out
for (const [, remainingElemID] of msgIDToElemID) {
if (remainingElemID === elemID) {
return
}
}
// If not, delete the element
elementsByID.delete(elemID)
mainWindow.webContents.send('readElements', [
{ id: elemID, isDeleted: true },
])
})
// Finally safe to kickstart replication and garbage collection
setTimeout(() => {
peer.conductor.start(
globalAccountID,
[
['profile@dict', 'zooboardElements@newest-100', 'hubs@set'],
['profile@dict', 'zooboardElements@newest-100'],
],
64_000,
(err) => {
if (err) console.error('Starting conductor failed:', err)
}
)
}, 32)
}
let hasSubscribedToConnections = false
async function subscribeToConnections() {
if (hasSubscribedToConnections) return
hasSubscribedToConnections = true
for await (const connections of awaitable(peer.net.peers())) {
mainWindow.webContents.send('connections', connections)
}
}
async function handlePZPUri(ev, uri) {
if (!globalAccountID) {
setTimeout(handlePZPUri, 100, null, uri)
return
}
if (uri.startsWith("http:") || uri.startsWith("https://")) {
uri = decodeURIComponent(uri.split('/invite#')[1])
}
if (!uri.startsWith('pzp://')) return console.log('Not a pzp invite URI', uri)
const commands = peer.invite.parse(uri)
for (const command of commands) {
console.log('Executing command', JSON.stringify(command))
switch (command.type) {
case 'join': {
try {
await p(peer.hubClient.addHub)(command.multiaddr)
} catch (err) {
console.error('Failed to properly join hub', err)
} }
}) break
break }
case 'follow': {
await p(peer.set.add)('follows', command.id)
break
}
case 'promise.follow': {
const [issuerType, issuerPubkey] = command.issuer
if (issuerType !== 'pubkey') {
throw new Error(`Dont know how to claim a ${issuerType} promise`)
}
// eslint-disable-next-line no-loop-func
peer.addListener('rpc:connect', function onConnect(rpc) {
if (rpc.shse.pubkey === issuerPubkey) {
peer.removeListener('rpc:connect', onConnect)
rpc.promise.follow(command.token, globalAccountID, (err) => {
if (err) return console.error('Failed to use follow promise', err)
})
}
})
break
}
default:
console.log('Unknown command type', command.type)
} }
default:
console.log('Unknown command type', command.type)
} }
} }
}
if (process.defaultApp) { if (process.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('pzp', process.execPath, [ app.setAsDefaultProtocolClient('pzp', process.execPath, [
Path.resolve(process.argv[1]), Path.resolve(process.argv[1]),
]) ])
}
} else {
app.setAsDefaultProtocolClient('pzp')
} }
} else {
app.setAsDefaultProtocolClient('pzp')
}
//const hasLock = app.requestSingleInstanceLock()
//if (!hasLock) {
// app.quit()
//} else {
// app.on('second-instance', (ev, argv, cwd, extraData) => {
// if (mainWindow) {
// if (mainWindow.isMinimized()) mainWindow.restore()
// mainWindow.focus()
// if (argv.length > 1) {
// handlePZPUri(null, argv[argv.length - 1])
// }
// }
// })
app.whenReady().then(() => { app.whenReady().then(() => {
ipcMain.handle('loadAccount', loadAccount) ipcMain.handle('loadAccount', loadAccount)
@ -322,4 +257,4 @@ if (process.defaultApp) {
if (process.platform !== 'darwin') app.quit() if (process.platform !== 'darwin') app.quit()
}) })
}) })
//} }).catch(err => console.error("Couldn't create peer:", err))

View File

@ -22,14 +22,15 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"debounce": "2.0", "debounce": "2.0",
"pzp-caps": "^1.0.0", "pzp-caps": "^1.0.0",
"pzp-db": "^1.0.1", "pzp-db": "^1.0.2",
"pzp-dict": "^1.0.0", "pzp-dict": "^1.0.0",
"pzp-gc": "^1.0.0", "pzp-gc": "^1.0.0",
"pzp-goals": "^1.0.0", "pzp-goals": "^1.0.0",
"pzp-keypair": "^1.0.0", "pzp-keypair": "^1.0.0",
"pzp-sdk": "^1.0.0",
"pzp-set": "^1.0.0", "pzp-set": "^1.0.0",
"pzp-sync": "^1.0.0", "pzp-sync": "^1.0.0",
"pzp-conductor": "^1.0.0", "pzp-conductor": "^1.0.2",
"pzp-hub-client": "^1.0.0", "pzp-hub-client": "^1.0.0",
"pzp-invite": "^1.0.0", "pzp-invite": "^1.0.0",
"pzp-net": "^1.0.1", "pzp-net": "^1.0.1",

View File

@ -26,11 +26,11 @@ dependencies:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
pzp-conductor: pzp-conductor:
specifier: ^1.0.0 specifier: ^1.0.2
version: 1.0.0 version: 1.0.2
pzp-db: pzp-db:
specifier: ^1.0.1 specifier: ^1.0.2
version: 1.0.1 version: 1.0.2
pzp-dict: pzp-dict:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -55,6 +55,9 @@ dependencies:
pzp-promise: pzp-promise:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
pzp-sdk:
specifier: ^1.0.0
version: 1.0.0(pzp-caps@1.0.0)(pzp-conductor@1.0.2)(pzp-db@1.0.2)(pzp-dict@1.0.0)(pzp-gc@1.0.0)(pzp-goals@1.0.0)(pzp-hub-client@1.0.0)(pzp-invite@1.0.0)(pzp-keypair@1.0.0)(pzp-net@1.0.1)(pzp-promise@1.0.0)(pzp-set@1.0.0)(pzp-sync@1.0.0)(secret-handshake-ext@0.0.11)(secret-stack@8.1.0)
pzp-set: pzp-set:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -9611,8 +9614,8 @@ packages:
resolution: {integrity: sha512-X/0lJFky3ztsypwwegrhjLODxe+ZFcQPWibBWD7SOuiG6+k01EhBFf7Tb9XvrYTncz9mbNXlaF4Iw0ihI55t0g==} resolution: {integrity: sha512-X/0lJFky3ztsypwwegrhjLODxe+ZFcQPWibBWD7SOuiG6+k01EhBFf7Tb9XvrYTncz9mbNXlaF4Iw0ihI55t0g==}
dev: false dev: false
/pzp-conductor@1.0.0: /pzp-conductor@1.0.2:
resolution: {integrity: sha512-FyZz446so7x5fRPhJZ70q1Icb4lVR7C/ovt/0fIPQf2UtxnqXaJtI1ptb/xmto6lg8ETc9OpPQMft/Ns6V8Rgw==} resolution: {integrity: sha512-jNIxodNp/XjQQtJpz7kXV4r7rLYvd0pB/PP3CDvSgxO8i3p53yWmv5tRfpdIExmVwkTdjFbxsFEFWy8j0JYvQA==}
engines: {node: '>=16'} engines: {node: '>=16'}
dependencies: dependencies:
debug: 4.3.4 debug: 4.3.4
@ -9620,8 +9623,8 @@ packages:
- supports-color - supports-color
dev: false dev: false
/pzp-db@1.0.1: /pzp-db@1.0.2:
resolution: {integrity: sha512-Bi3i4j9A49ElQ3yTGOl4a5eQ0p3vciaKUMZK+p2Rq42FrFI95BTZGYPFjgUDGgAstcnPsROEF3kCseaM46eoQg==} resolution: {integrity: sha512-BBFw/VdFtw3toEgHHTUhnXItwOVsTgeAZuVyHqPUGw79MjI9sngY0PTLZRo51TFFxQQpFCNds4ljqXqiz2y6Pg==}
engines: {node: '>=16'} engines: {node: '>=16'}
dependencies: dependencies:
'@alloc/quick-lru': 5.2.0 '@alloc/quick-lru': 5.2.0
@ -9743,6 +9746,43 @@ packages:
bs58: 5.0.0 bs58: 5.0.0
dev: false dev: false
/pzp-sdk@1.0.0(pzp-caps@1.0.0)(pzp-conductor@1.0.2)(pzp-db@1.0.2)(pzp-dict@1.0.0)(pzp-gc@1.0.0)(pzp-goals@1.0.0)(pzp-hub-client@1.0.0)(pzp-invite@1.0.0)(pzp-keypair@1.0.0)(pzp-net@1.0.1)(pzp-promise@1.0.0)(pzp-set@1.0.0)(pzp-sync@1.0.0)(secret-handshake-ext@0.0.11)(secret-stack@8.1.0):
resolution: {integrity: sha512-kpga5eN47PEsignEgY7cMCUl5dEnnCurvCj0c3dAH4T08MPsVRXzkC5yrBEnZbdXYjUAdZEEiUlMBsconmlxiQ==}
engines: {node: '>=18'}
peerDependencies:
pzp-caps: ^1.0.0
pzp-conductor: ^1.0.2
pzp-db: ^1.0.2
pzp-dict: ^1.0.0
pzp-gc: ^1.0.0
pzp-goals: ^1.0.0
pzp-hub-client: ^1.0.0
pzp-invite: ^1.0.0
pzp-keypair: ^1.0.0
pzp-net: ^1.0.1
pzp-promise: ^1.0.0
pzp-set: ^1.0.0
pzp-sync: ^1.0.0
secret-handshake-ext: 0.0.11
secret-stack: ~8.1.0
dependencies:
pzp-caps: 1.0.0
pzp-conductor: 1.0.2
pzp-db: 1.0.2
pzp-dict: 1.0.0
pzp-gc: 1.0.0
pzp-goals: 1.0.0
pzp-hub-client: 1.0.0
pzp-invite: 1.0.0
pzp-keypair: 1.0.0
pzp-net: 1.0.1
pzp-promise: 1.0.0
pzp-set: 1.0.0
pzp-sync: 1.0.0
secret-handshake-ext: 0.0.11
secret-stack: 8.1.0
dev: false
/pzp-set@1.0.0: /pzp-set@1.0.0:
resolution: {integrity: sha512-+BLssohjri/FgNSPSPffkcgBAXAxiycoS9W0kPZJtLeqSCwkhkyhPG3CHhOFhktUJ24wbTDFn0PnhOu0f9ZhbA==} resolution: {integrity: sha512-+BLssohjri/FgNSPSPffkcgBAXAxiycoS9W0kPZJtLeqSCwkhkyhPG3CHhOFhktUJ24wbTDFn0PnhOu0f9ZhbA==}
engines: {node: '>=16'} engines: {node: '>=16'}