use rxjs for bakery connection

This commit is contained in:
hzrd149 2025-03-08 21:48:18 +00:00
parent d0b11e9081
commit 789de46c56
60 changed files with 386 additions and 1503 deletions

View File

@ -1,3 +0,0 @@
# noStrudel server
This is meant to be a simple nodejs server that proxies requests to bypass CORS or access TOR or I2P

View File

@ -1,52 +0,0 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY ./package*.json .
COPY ./pnpm-lock.yaml .
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
ENV VITE_COMMIT_HASH=""
ENV VITE_APP_VERSION="Custom"
COPY tsconfig.json .
COPY public ./public
COPY src ./src
RUN pnpm build
# FROM nginx:stable-alpine-slim AS main
FROM base AS main
# install tor
# copied from https://github.com/klemmchr/tor-alpine/blob/master/Dockerfile
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && \
apk -U upgrade && \
apk -v add tor@edge torsocks@edge
# remove tmp files
RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/*
WORKDIR /app
COPY --from=builder /app/dist /usr/share/nginx/html
# copy server
COPY server/ /app/server/
RUN cd /app/server/ && npm install
# setup entrypoint
ADD ./docker-entrypoint.sh docker-entrypoint.sh
RUN chmod a+x docker-entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -1,151 +0,0 @@
#!/bin/sh
set -e
PROXY_PASS_BLOCK=""
# start tor if set to true
if [ "$TOR_PROXY" = "true" ]; then
echo "Starting tor socks proxy"
tor &
tor_process=$!
TOR_PROXY="127.0.0.1:9050"
fi
# inject request proxy
if [ -n "$REQUEST_PROXY" ]; then
REQUEST_PROXY_URL="$REQUEST_PROXY"
if [ "$REQUEST_PROXY" = "true" ]; then
REQUEST_PROXY_URL="127.0.0.1:8080"
fi
echo "Request proxy set to $REQUEST_PROXY"
sed -i 's/REQUEST_PROXY = ""/REQUEST_PROXY = "\/request-proxy"/g' /usr/share/nginx/html/index.html
PROXY_PASS_BLOCK="$PROXY_PASS_BLOCK
location /request-proxy/ {
proxy_pass http://$REQUEST_PROXY_URL;
rewrite ^/request-proxy/(.*) /\$1 break;
}
"
if [ -n "$PROXY_FIRST" ]; then
echo "Telling app to use request proxy first"
sed -i 's/PROXY_FIRST = false/PROXY_FIRST = true/g' /usr/share/nginx/html/index.html
fi
else
echo "No request proxy set"
fi
# inject cache relay URL
if [ -n "$CACHE_RELAY" ]; then
echo "Cache relay set to $CACHE_RELAY"
sed -i 's/CACHE_RELAY_ENABLED = false/CACHE_RELAY_ENABLED = true/g' /usr/share/nginx/html/index.html
PROXY_PASS_BLOCK="$PROXY_PASS_BLOCK
location /local-relay {
proxy_pass http://$CACHE_RELAY/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
"
else
echo "No cache relay set"
fi
# inject image proxy URL
if [ -n "$IMAGE_PROXY" ]; then
echo "Image proxy set to $IMAGE_PROXY"
sed -i 's/IMAGE_PROXY_PATH = ""/IMAGE_PROXY_PATH = "\/imageproxy"/g' /usr/share/nginx/html/index.html
PROXY_PASS_BLOCK="$PROXY_PASS_BLOCK
location /imageproxy/ {
proxy_pass http://$IMAGE_PROXY;
rewrite ^/imageproxy/(.*) /\$1 break;
}
"
else
echo "No Image proxy set"
fi
CONF_FILE="/etc/nginx/conf.d/default.conf"
NGINX_CONF="
server {
listen 80;
server_name localhost;
merge_slashes off;
$PROXY_PASS_BLOCK
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# Gzip settings
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/x-javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/xhtml+xml
application/xml
font/eot
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
"
echo "$NGINX_CONF" > $CONF_FILE
_term() {
echo "Caught SIGTERM signal!"
# stop node server
if [ "$REQUEST_PROXY" = "true" ]; then
kill -SIGTERM "$node_process" 2>/dev/null
fi
# stop tor if started
if [ "$TOR_PROXY" = "true" ]; then
kill -SIGTERM "$tor_process" 2>/dev/null
fi
# stop nginx
kill -SIGTERM "$nginx_process" 2>/dev/null
}
if [ "$REQUEST_PROXY" = "true" ]; then
echo "Starting local request proxy"
node server/index.js &
node_process=$!
fi
nginx -g 'daemon off;' &
nginx_process=$!
trap _term SIGTERM
wait $nginx_process

View File

@ -1,52 +0,0 @@
var cors_proxy = require("cors-anywhere");
var { PacProxyAgent } = require("pac-proxy-agent");
const { TOR_PROXY, I2P_PROXY } = process.env;
if (TOR_PROXY) console.log("Tor Proxy:", TOR_PROXY);
if (I2P_PROXY) console.log("I2P Proxy:", I2P_PROXY);
const I2pConfig = I2P_PROXY
? `
if (shExpMatch(host, "*.i2p"))
{
return "PROXY ${I2P_PROXY}";
}`.trim()
: "";
const TorConfig = TOR_PROXY
? `
if (shExpMatch(host, "*.onion"))
{
return "SOCKS5 ${TOR_PROXY}";
}`.trim()
: "";
const PACFile = `
// SPDX-License-Identifier: CC0-1.0
function FindProxyForURL(url, host)
{
${I2pConfig}
${TorConfig}
return "DIRECT";
}
`.trim();
const PACURI = "pac+data:application/x-ns-proxy-autoconfig;base64," + btoa(PACFile);
var host = "127.0.0.1";
var port = 8080;
cors_proxy
.createServer({
requireHeader: [],
removeHeaders: ["cookie", "cookie2"],
redirectSameOrigin: true,
httpProxyOptions: {
xfwd: false,
agent: new PacProxyAgent(PACURI),
},
})
.listen(port, host, () => {
console.log("Running HTTP request proxy on " + host + ":" + port);
});

View File

@ -1,11 +0,0 @@
{
"name": "server",
"version": "1.0.0",
"private": true,
"main": "index.js",
"license": "MIT",
"dependencies": {
"cors-anywhere": "^0.4.4",
"pac-proxy-agent": "^7.0.1"
}
}

335
server/pnpm-lock.yaml generated
View File

@ -1,335 +0,0 @@
lockfileVersion: "9.0"
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
cors-anywhere:
specifier: ^0.4.4
version: 0.4.4
pac-proxy-agent:
specifier: ^7.0.1
version: 7.0.2
packages:
"@tootallnate/quickjs-emscripten@0.23.0":
resolution:
{ integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== }
agent-base@7.1.1:
resolution:
{ integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== }
engines: { node: ">= 14" }
ast-types@0.13.4:
resolution:
{ integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== }
engines: { node: ">=4" }
basic-ftp@5.0.5:
resolution:
{ integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== }
engines: { node: ">=10.0.0" }
cors-anywhere@0.4.4:
resolution:
{ integrity: sha512-8OBFwnzMgR4mNrAeAyOLB2EruS2z7u02of2bOu7i9kKYlZG+niS7CTHLPgEXKWW2NAOJWRry9RRCaL9lJRjNqg== }
engines: { node: ">=0.10.0" }
data-uri-to-buffer@6.0.2:
resolution:
{ integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== }
engines: { node: ">= 14" }
debug@4.3.7:
resolution:
{ integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== }
engines: { node: ">=6.0" }
peerDependencies:
supports-color: "*"
peerDependenciesMeta:
supports-color:
optional: true
degenerator@5.0.1:
resolution:
{ integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== }
engines: { node: ">= 14" }
escodegen@2.1.0:
resolution:
{ integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== }
engines: { node: ">=6.0" }
hasBin: true
esprima@4.0.1:
resolution:
{ integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== }
engines: { node: ">=4" }
hasBin: true
estraverse@5.3.0:
resolution:
{ integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== }
engines: { node: ">=4.0" }
esutils@2.0.3:
resolution:
{ integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== }
engines: { node: ">=0.10.0" }
eventemitter3@1.2.0:
resolution:
{ integrity: sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA== }
fs-extra@11.2.0:
resolution:
{ integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== }
engines: { node: ">=14.14" }
get-uri@6.0.3:
resolution:
{ integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== }
engines: { node: ">= 14" }
graceful-fs@4.2.11:
resolution:
{ integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== }
http-proxy-agent@7.0.2:
resolution:
{ integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== }
engines: { node: ">= 14" }
http-proxy@1.11.1:
resolution:
{ integrity: sha512-qz7jZarkVG3G6GMq+4VRJPSN4NkIjL4VMTNhKGd8jc25BumeJjWWvnY3A7OkCGa8W1TTxbaK3dcE0ijFalITVA== }
engines: { node: ">=0.10.0" }
https-proxy-agent@7.0.5:
resolution:
{ integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== }
engines: { node: ">= 14" }
ip-address@9.0.5:
resolution:
{ integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== }
engines: { node: ">= 12" }
jsbn@1.1.0:
resolution:
{ integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== }
jsonfile@6.1.0:
resolution:
{ integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== }
ms@2.1.3:
resolution:
{ integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== }
netmask@2.0.2:
resolution:
{ integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== }
engines: { node: ">= 0.4.0" }
pac-proxy-agent@7.0.2:
resolution:
{ integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== }
engines: { node: ">= 14" }
pac-resolver@7.0.1:
resolution:
{ integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== }
engines: { node: ">= 14" }
proxy-from-env@0.0.1:
resolution:
{ integrity: sha512-B9Hnta3CATuMS0q6kt5hEezOPM+V3dgaRewkFtFoaRQYTVNsHqUvFXmndH06z3QO1ZdDnRELv5vfY6zAj/gG7A== }
requires-port@0.0.1:
resolution:
{ integrity: sha512-AzPDCliPoWDSvEVYRQmpzuPhGGEnPrQz9YiOEvn+UdB9ixBpw+4IOZWtwctmpzySLZTy7ynpn47V14H4yaowtA== }
smart-buffer@4.2.0:
resolution:
{ integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== }
engines: { node: ">= 6.0.0", npm: ">= 3.0.0" }
socks-proxy-agent@8.0.4:
resolution:
{ integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== }
engines: { node: ">= 14" }
socks@2.8.3:
resolution:
{ integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== }
engines: { node: ">= 10.0.0", npm: ">= 3.0.0" }
source-map@0.6.1:
resolution:
{ integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== }
engines: { node: ">=0.10.0" }
sprintf-js@1.1.3:
resolution:
{ integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== }
tslib@2.7.0:
resolution:
{ integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== }
universalify@2.0.1:
resolution:
{ integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== }
engines: { node: ">= 10.0.0" }
snapshots:
"@tootallnate/quickjs-emscripten@0.23.0": {}
agent-base@7.1.1:
dependencies:
debug: 4.3.7
transitivePeerDependencies:
- supports-color
ast-types@0.13.4:
dependencies:
tslib: 2.7.0
basic-ftp@5.0.5: {}
cors-anywhere@0.4.4:
dependencies:
http-proxy: 1.11.1
proxy-from-env: 0.0.1
data-uri-to-buffer@6.0.2: {}
debug@4.3.7:
dependencies:
ms: 2.1.3
degenerator@5.0.1:
dependencies:
ast-types: 0.13.4
escodegen: 2.1.0
esprima: 4.0.1
escodegen@2.1.0:
dependencies:
esprima: 4.0.1
estraverse: 5.3.0
esutils: 2.0.3
optionalDependencies:
source-map: 0.6.1
esprima@4.0.1: {}
estraverse@5.3.0: {}
esutils@2.0.3: {}
eventemitter3@1.2.0: {}
fs-extra@11.2.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
get-uri@6.0.3:
dependencies:
basic-ftp: 5.0.5
data-uri-to-buffer: 6.0.2
debug: 4.3.7
fs-extra: 11.2.0
transitivePeerDependencies:
- supports-color
graceful-fs@4.2.11: {}
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.1
debug: 4.3.7
transitivePeerDependencies:
- supports-color
http-proxy@1.11.1:
dependencies:
eventemitter3: 1.2.0
requires-port: 0.0.1
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.1
debug: 4.3.7
transitivePeerDependencies:
- supports-color
ip-address@9.0.5:
dependencies:
jsbn: 1.1.0
sprintf-js: 1.1.3
jsbn@1.1.0: {}
jsonfile@6.1.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
ms@2.1.3: {}
netmask@2.0.2: {}
pac-proxy-agent@7.0.2:
dependencies:
"@tootallnate/quickjs-emscripten": 0.23.0
agent-base: 7.1.1
debug: 4.3.7
get-uri: 6.0.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5
pac-resolver: 7.0.1
socks-proxy-agent: 8.0.4
transitivePeerDependencies:
- supports-color
pac-resolver@7.0.1:
dependencies:
degenerator: 5.0.1
netmask: 2.0.2
proxy-from-env@0.0.1: {}
requires-port@0.0.1: {}
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.4:
dependencies:
agent-base: 7.1.1
debug: 4.3.7
socks: 2.8.3
transitivePeerDependencies:
- supports-color
socks@2.8.3:
dependencies:
ip-address: 9.0.5
smart-buffer: 4.2.0
source-map@0.6.1:
optional: true
sprintf-js@1.1.3: {}
tslib@2.7.0: {}
universalify@2.0.1: {}

View File

@ -1,133 +0,0 @@
import { BehaviorSubject, Subject } from "rxjs";
import { EventTemplate, Relay, VerifiedEvent } from "nostr-tools";
import { ControlMessage, ControlResponse } from "@satellite-earth/core/types";
import { createDefer, Deferred } from "applesauce-core/promise";
import { logger } from "../../helpers/debug";
export default class BakeryRelay extends Relay {
log = logger.extend("BakeryConnection");
isFirstConnection$ = new BehaviorSubject(true);
isFirstAuthentication$ = new BehaviorSubject(true);
connected$ = new BehaviorSubject(false);
authenticated$ = new BehaviorSubject(false);
controlResponse$ = new Subject<ControlResponse>();
constructor(url: string) {
super(url);
// override _connected property
Object.defineProperty(this, "_connected", {
get: () => this.connected$.value,
set: (v) => {
this.connected$.next(v);
if (v && this.isFirstConnection$.value) this.isFirstConnection$.next(false);
},
});
}
sentAuthId = "";
authPromise: Deferred<string> | null = null;
onChallenge = new Subject<string>();
authenticate(auth: string | ((evt: EventTemplate) => Promise<VerifiedEvent>)) {
if (!this.connected) throw new Error("Not connected");
if (!this.authenticated$.value && !this.authPromise) {
this.authPromise = createDefer<string>();
if (this.isFirstAuthentication$.value) this.authPromise.then(() => this.isFirstAuthentication$.next(false));
// CONTROL auth
if (typeof auth === "string") {
this.sendControlMessage(["CONTROL", "AUTH", "CODE", auth]);
return this.authPromise;
}
// NIP-42 auth
this.auth(auth)
.then((response) => {
this.authenticated$.next(true);
this.authPromise?.resolve(response);
this.authPromise = null;
})
.catch((err) => {
this.authPromise?.reject(err);
this.authPromise = null;
});
}
return this.authPromise;
}
_onauth = (challenge: string) => {
this.onChallenge.next(challenge);
};
_onmessage(message: MessageEvent<string>) {
try {
// Parse control message(s) received from node
const data = JSON.parse(message.data);
switch (data[0]) {
case "CONTROL":
// const payload = Array.isArray(data[1]) ? data[1] : [data[1]];
this.handleControlResponse(data as ControlResponse);
return;
}
} catch (err) {
console.log(err);
}
// use default relay message handling
super._onmessage(message);
}
onclose = () => {
this.authenticated$.next(false);
// @ts-expect-error
this.connectionPromise = undefined;
};
close(): void {
super.close();
this.authenticated$.next(false);
// @ts-expect-error
this.connectionPromise = undefined;
}
// Send control message to node
sendControlMessage(message: ControlMessage) {
return this.send(JSON.stringify(message));
}
// handle control response
handleControlResponse(response: ControlResponse) {
switch (response[1]) {
case "AUTH":
if (response[2] === "SUCCESS") {
this.authenticated$.next(true);
this.authPromise?.resolve("Success");
} else if (response[2] === "INVALID") {
this.authPromise?.reject(new Error(response[3]));
}
this.authPromise = null;
break;
default:
this.controlResponse$.next(response);
break;
}
}
/** @deprecated use controlApi instead */
clearDatabase() {
this.sendControlMessage(["CONTROL", "DATABASE", "CLEAR"]);
}
/** @deprecated use controlApi instead */
exportDatabase() {
this.sendControlMessage(["CONTROL", "DATABASE", "EXPORT"]);
}
}

View File

@ -0,0 +1,104 @@
import {
firstValueFrom,
map,
MonoTypeOperatorFunction,
Observable,
ReplaySubject,
scan,
share,
shareReplay,
skip,
tap,
timer,
} from "rxjs";
import { PrivateNodeConfig } from "@satellite-earth/core/types";
import hash_sum from "hash-sum";
import BakeryRelay from "./bakery-relay";
import { LogEntry, NetworkStateResult } from "./types";
import { scanToArray } from "../../helpers/observable";
export default class BakeryControlApi {
queries = new Map<string, Observable<any>>();
config: Observable<PrivateNodeConfig>;
network: Observable<NetworkStateResult>;
services: Observable<string[]>;
constructor(public bakery: BakeryRelay) {
this.config = this.query<PrivateNodeConfig>("config", {}).pipe(shareReplay(1));
this.network = this.query<NetworkStateResult>("network-status", {}).pipe(shareReplay(1));
this.services = this.query<{ id: string }>("services", {}).pipe(
scan((arr, service) => (arr.includes(service.id) ? arr : [...arr, service.id]), [] as string[]),
shareReplay(1),
);
}
query<T extends unknown = unknown, R extends unknown = T>(
type: string,
args: any,
modify: (source: Observable<T>) => Observable<R>,
): Observable<R>;
query<T extends unknown = unknown>(type: string, args: any): Observable<T>;
query<T extends unknown = unknown, R extends unknown = T>(
type: string,
args: any,
modify?: (source: Observable<T>) => Observable<R>,
): Observable<R> {
const id = hash_sum([type, args]);
const existing = this.queries.get(id);
if (existing) return existing;
let query = this.bakery.socket$
.multiplex(
() => ["QRY", "OPEN", type, id, args],
() => ["QRY", "CLOSE", id],
(m) => m[0] === "QRY" && (m[1] === "DATA" || m[1] === "ERR") && m[2] === id,
)
.pipe(
map((message) => {
// throw error
if (message[1] === "ERR") throw new Error(message[2]);
// return data
else return message[3];
}),
// cleanup when query is complete
tap({
complete: () => {
// cleanup query
this.queries.delete(id);
},
}),
);
if (modify) query = modify(query);
// share the observable
query = query.pipe(shareReplay(1));
this.queries.set(id, query);
return query;
}
/** gets longs for a service */
logs(filter: { service?: string; limit?: number }) {
return this.query<LogEntry, LogEntry[]>("logs", filter, (source) => source.pipe(scanToArray()));
}
async setConfigField<T extends keyof PrivateNodeConfig>(field: T, value: PrivateNodeConfig[T]) {
await this.bakery.socket$.next(["CONTROL", "CONFIG", "SET", field, value]);
// wait for the next change to config
await firstValueFrom(this.config.pipe(skip(1)));
}
async setConfigFields(config: Partial<PrivateNodeConfig>) {
for (const [field, value] of Object.entries(config)) {
await this.bakery.socket$.next(["CONTROL", "CONFIG", "SET", field, value]);
}
// wait for the next change to config
await firstValueFrom(this.config.pipe(skip(1)));
}
}

View File

@ -0,0 +1,122 @@
import {
BehaviorSubject,
filter,
map,
merge,
NEVER,
Observable,
of,
OperatorFunction,
shareReplay,
take,
takeWhile,
tap,
timeout,
} from "rxjs";
import { Filter, NostrEvent } from "nostr-tools";
import { webSocket, WebSocketSubject } from "rxjs/webSocket";
import { logger } from "../../helpers/debug";
import { simpleTimeout } from "applesauce-core/observable";
export type RequestResponse = { type: "EOSE"; id: string } | { type: "EVENT"; id: string; event: NostrEvent };
/** Filter request responses and only return the events */
export function filterEvents(): OperatorFunction<RequestResponse, NostrEvent> {
return (source) =>
source.pipe(
filter((r) => r.type === "EVENT"),
map((r) => r.event),
);
}
export default class BakeryRelay {
log = logger.extend("Bakery");
public socket$: WebSocketSubject<any[]>;
connected$ = new BehaviorSubject(false);
challenge$: Observable<string>;
authenticated$ = new BehaviorSubject(false);
constructor(public url: string) {
this.socket$ = webSocket({
url,
openObserver: {
next: () => {
this.log("Connected");
this.connected$.next(true);
this.authenticated$.next(false);
},
},
closeObserver: {
next: () => {
this.log("Disconnected");
this.connected$.next(false);
this.authenticated$.next(false);
},
},
});
// create an observable for listening for AUTH
this.challenge$ = this.socket$.pipe(
filter((message) => message[0] === "AUTH"),
map((m) => m[1]),
shareReplay(1),
);
}
req(id: string, filters: Filter[]): Observable<RequestResponse> {
return this.socket$
.multiplex(
() => ["REQ", id, ...filters],
() => ["CLOSE", id],
(message) => (message[0] === "EVENT" || message[0] === "CLOSE" || message[0] === "EOSE") && message[1] === id,
)
.pipe(
// complete when CLOSE is sent
takeWhile((m) => m[0] !== "CLOSE"),
// pick event out of EVENT messages
map<any[], RequestResponse>((message) => {
if (message[0] === "EOSE") return { type: "EOSE", id: message[1] };
else return { type: "EVENT", id: message[1], event: message[2] };
}),
// if no events are seen in 10s, emit EOSE
timeout({
first: 10_000,
with: () => merge(of<RequestResponse>({ type: "EOSE", id }), NEVER),
}),
);
}
protected listenForOk(id: string) {
return this.socket$.pipe(
// look for OK message for event
filter((m) => m[0] === "OK" && m[1] === id),
// format OK message
map((m) => ({ ok: m[2], message: m[3] })),
// complete on first value
take(1),
);
}
/** send an Event message */
event(event: NostrEvent): Observable<{ ok: boolean; message?: string }> {
this.socket$.next(["EVENT", event]);
return this.listenForOk(event.id).pipe(
// Throw timeout error if OK is not seen in 10s
simpleTimeout(10_000, "Timeout"),
);
}
/** send and AUTH message */
auth(event: NostrEvent): Observable<{ ok: boolean; message?: string }> {
this.socket$.next(["AUTH", event]);
return this.listenForOk(event.id).pipe(
// update authenticated
tap((result) => this.authenticated$.next(result.ok)),
// timeout after 5s for AUTH messages
simpleTimeout(5_000, "Timeout"),
);
}
}

View File

@ -1,75 +0,0 @@
import { BehaviorSubject, Subject } from "rxjs";
import { ControlMessage, ControlResponse } from "@satellite-earth/core/types";
import { PrivateNodeConfig } from "@satellite-earth/core/types";
import { DatabaseStats } from "@satellite-earth/core/types/control-api/database.js";
import EventEmitter from "eventemitter3";
import BakeryRelay from "./bakery-connection";
type EventMap = {
message: [ControlResponse];
authenticated: [boolean];
};
export default class BakeryControlApi extends EventEmitter<EventMap> {
node: BakeryRelay;
config = new BehaviorSubject<PrivateNodeConfig | undefined>(undefined);
/** @deprecated this should be a report */
databaseStats = new Subject<DatabaseStats>();
vapidKey = new BehaviorSubject<string | undefined>(undefined);
constructor(node: BakeryRelay) {
super();
this.node = node;
this.node.authenticated$.subscribe((authenticated) => {
this.emit("authenticated", authenticated);
if (authenticated) {
this.node.sendControlMessage(["CONTROL", "CONFIG", "SUBSCRIBE"]);
this.node.sendControlMessage(["CONTROL", "DATABASE", "SUBSCRIBE"]);
this.node.sendControlMessage(["CONTROL", "REMOTE-AUTH", "SUBSCRIBE"]);
}
});
this.node.controlResponse$.subscribe(this.handleControlResponse.bind(this));
}
handleControlResponse(response: ControlResponse) {
this.emit("message", response);
switch (response[1]) {
case "CONFIG":
if (response[2] === "CHANGED") this.config.next(response[3]);
break;
case "DATABASE":
if (response[2] === "STATS") this.databaseStats.next(response[3]);
break;
case "NOTIFICATIONS":
if (response[2] === "VAPID-KEY") this.vapidKey.next(response[3]);
break;
default:
break;
}
}
send(message: ControlMessage) {
if (this.node.connected) this.node.send(JSON.stringify(message));
}
async setConfigField<T extends keyof PrivateNodeConfig>(field: T, value: PrivateNodeConfig[T]) {
if (this.config.value === undefined) throw new Error("Config not synced");
await this.send(["CONTROL", "CONFIG", "SET", field, value]);
return new Promise<PrivateNodeConfig>((res) => {
const sub = this.config.subscribe((config) => {
if (config) res(config);
sub.unsubscribe();
});
});
}
}

View File

@ -1,20 +0,0 @@
import { ReportResults } from "@satellite-earth/core/types";
import { BehaviorSubject } from "rxjs";
import Report from "./report";
export default class ConversationsReport extends Report<"CONVERSATIONS"> {
readonly type = "CONVERSATIONS";
value = new BehaviorSubject<ReportResults["CONVERSATIONS"][]>([]);
handleResult(response: ReportResults["CONVERSATIONS"]): void {
// remove duplicates
const next = this.value.value?.filter((r) => r.pubkey !== response.pubkey).concat(response) ?? [response];
const sorted = next.sort(
(a, b) => Math.max(b.lastReceived ?? 0, b.lastSent ?? 0) - Math.max(a.lastReceived ?? 0, a.lastSent ?? 0),
);
this.value.next(sorted);
}
}

View File

@ -1,51 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
import { getDMRecipient, getDMSender } from "../../../helpers/nostr/dms";
function sortPubkeys(a: string, b: string): [string, string] {
if (a < b) return [a, b];
else return [b, a];
}
export type ConversationResult = {
id: string;
pubkeys: [string, string];
results: ReportResults["DM_SEARCH"][];
};
export default class DMSearchReport extends Report<"DM_SEARCH"> {
readonly type = "DM_SEARCH";
results = new BehaviorSubject<ReportResults["DM_SEARCH"][]>([]);
conversations = new BehaviorSubject<ConversationResult[]>([]);
onFire() {
this.results.next([]);
this.conversations.next([]);
}
handleResult(result: ReportResults["DM_SEARCH"]) {
this.results.next([...this.results.value, result]);
// add to conversations
const sender = getDMSender(result.event);
const recipient = getDMRecipient(result.event);
const pubkeys = sortPubkeys(sender, recipient);
const id = pubkeys.join(":");
if (this.conversations.value.some((c) => c.id === id)) {
// replace the conversation object
this.conversations.next(
this.conversations.value.map((c) => {
if (c.id === id) return { id, pubkeys, results: [...c.results, result] };
return c;
}),
);
} else {
// add new conversation
this.conversations.next([...this.conversations.value, { id, pubkeys, results: [result] }]);
}
}
}

View File

@ -1,18 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class EventsSummaryReport extends Report<"EVENTS_SUMMARY"> {
readonly type = "EVENTS_SUMMARY";
events = new BehaviorSubject<ReportResults["EVENTS_SUMMARY"][]>([]);
onFire(): void {
this.events.next([]);
}
handleResult(result: ReportResults["EVENTS_SUMMARY"]): void {
if (this.events.value) this.events.next([...this.events.value, result]);
else this.events.next([result]);
}
}

View File

@ -1,41 +0,0 @@
import { ReportArguments } from "@satellite-earth/core/types";
import Report from "./report";
import OverviewReport from "./overview.js";
import ConversationsReport from "./conversations.js";
import LogsReport from "./logs.js";
import ServicesReport from "./services.js";
import DMSearchReport from "./dm-search.js";
import ScrapperStatusReport from "./scrapper-status.js";
import ReceiverStatusReport from "./receiver-status.js";
import NetworkStatusReport from "./network-status.js";
import NotificationChannelsReport from "./notification-channels.js";
import EventsSummaryReport from "./events-summary.js";
export const ReportClasses: {
[k in keyof ReportArguments]?: typeof Report<k>;
} = {
OVERVIEW: OverviewReport,
CONVERSATIONS: ConversationsReport,
LOGS: LogsReport,
SERVICES: ServicesReport,
DM_SEARCH: DMSearchReport,
SCRAPPER_STATUS: ScrapperStatusReport,
RECEIVER_STATUS: ReceiverStatusReport,
NETWORK_STATUS: NetworkStatusReport,
NOTIFICATION_CHANNELS: NotificationChannelsReport,
EVENTS_SUMMARY: EventsSummaryReport,
} as const;
export type ReportTypes = {
OVERVIEW: OverviewReport;
CONVERSATIONS: ConversationsReport;
LOGS: LogsReport;
SERVICES: ServicesReport;
DM_SEARCH: DMSearchReport;
SCRAPPER_STATUS: ScrapperStatusReport;
RECEIVER_STATUS: ReceiverStatusReport;
NETWORK_STATUS: NetworkStatusReport;
NOTIFICATION_CHANNELS: NotificationChannelsReport;
EVENTS_SUMMARY: EventsSummaryReport;
};

View File

@ -1,22 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class LogsReport extends Report<"LOGS"> {
readonly type = "LOGS";
ids = new Set<string>();
entries = new BehaviorSubject<ReportResults["LOGS"][]>([]);
handleResult(result: ReportResults["LOGS"]) {
if (this.ids.has(result.id)) return;
this.ids.add(result.id);
this.entries.next(this.entries.value.concat(result).sort((a, b) => b.timestamp - a.timestamp));
}
clear() {
this.entries.next([]);
}
}

View File

@ -1,13 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class NetworkStatusReport extends Report<"NETWORK_STATUS"> {
readonly type = "NETWORK_STATUS";
status = new BehaviorSubject<ReportResults["NETWORK_STATUS"] | undefined>(undefined);
handleResult(response: ReportResults["NETWORK_STATUS"]): void {
this.status.next(response);
}
}

View File

@ -1,29 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import { NotificationChannel } from "@satellite-earth/core/types/control-api/notifications.js";
import Report from "./report";
export default class NotificationChannelsReport extends Report<"NOTIFICATION_CHANNELS"> {
readonly type = "NOTIFICATION_CHANNELS";
channels = new BehaviorSubject<Record<string, NotificationChannel> | undefined>(undefined);
refresh() {
this.channels.next({});
this.fire();
}
handleResult(channel: ReportResults["NOTIFICATION_CHANNELS"]): void {
if (Array.isArray(channel)) {
const id = channel[1];
const next = { ...this.channels.value };
delete next[id];
this.channels.next(next);
} else
this.channels.next({
...this.channels.value,
[channel.id]: channel,
});
}
}

View File

@ -1,17 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class OverviewReport extends Report<"OVERVIEW"> {
type = "OVERVIEW" as const;
value = new BehaviorSubject<ReportResults["OVERVIEW"][]>([]);
handleResult(response: ReportResults["OVERVIEW"]): void {
// remove duplicates
const next = this.value.value?.filter((r) => r.pubkey !== response.pubkey).concat(response) ?? [response];
const sorted = next.sort((a, b) => b.events - a.events);
this.value.next(sorted);
}
}

View File

@ -1,13 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class ReceiverStatusReport extends Report<"RECEIVER_STATUS"> {
readonly type = "RECEIVER_STATUS";
status = new BehaviorSubject<ReportResults["RECEIVER_STATUS"] | undefined>(undefined);
handleResult(response: ReportResults["RECEIVER_STATUS"]): void {
this.status.next(response);
}
}

View File

@ -1,49 +0,0 @@
import { ReportArguments, ReportResults } from "@satellite-earth/core/types";
import _throttle from "lodash.throttle";
import { nanoid } from "nanoid";
import { Debugger } from "debug";
import BakeryControlApi from "../control-api";
import { logger } from "../../../helpers/debug";
export default class Report<T extends keyof ReportArguments> {
id: string;
args: ReportArguments[T];
running = false;
log: Debugger;
error: string | undefined;
control: BakeryControlApi;
constructor(id: string = nanoid(), args: ReportArguments[T], control: BakeryControlApi) {
this.id = id;
this.args = args;
this.control = control;
this.log = logger.extend(this.type + ":" + id);
}
// override
// @ts-expect-error
readonly type: T = "unset";
onFire(args: ReportArguments[T]) {}
handleResult(result: ReportResults[T]) {}
handleError(message: string) {
this.error = message;
}
// public api
fireThrottle = _throttle(this.fire.bind(this), 10, { leading: false });
fire() {
this.onFire(this.args);
// @ts-expect-error
this.control.send(["CONTROL", "REPORT", "SUBSCRIBE", this.id, this.type, this.args]);
this.running = true;
}
setArgs(args: ReportArguments[T]) {
this.args = args;
}
close() {
this.control.send(["CONTROL", "REPORT", "CLOSE", this.id]);
this.running = false;
}
}

View File

@ -1,13 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class ScrapperStatusReport extends Report<"SCRAPPER_STATUS"> {
readonly type = "SCRAPPER_STATUS";
status = new BehaviorSubject<ReportResults["SCRAPPER_STATUS"] | undefined>(undefined);
handleResult(response: ReportResults["SCRAPPER_STATUS"]): void {
this.status.next(response);
}
}

View File

@ -1,13 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { ReportResults } from "@satellite-earth/core/types";
import Report from "./report";
export default class ServicesReport extends Report<"SERVICES"> {
readonly type = "SERVICES";
services = new BehaviorSubject<ReportResults["SERVICES"][]>([]);
handleResult(result: ReportResults["SERVICES"]) {
this.services.next(this.services.value.filter((s) => s.id !== result.id).concat(result));
}
}

View File

@ -0,0 +1,27 @@
export type NetworkOutboundState = {
available: boolean;
running?: boolean;
error?: string;
};
export type NetworkInboundState = {
available: boolean;
running?: boolean;
error?: string;
address?: string;
};
export type NetworkState = {
inbound: NetworkInboundState;
outbound: NetworkInboundState;
};
export type NetworkStateResult = {
tor: NetworkState;
hyper: NetworkState;
i2p: NetworkState;
};
export type LogEntry = {
id: string;
service: string;
timestamp: number;
message: string;
};

View File

@ -9,10 +9,9 @@ import { bakery$ } from "../../services/bakery";
export default function RequireBakeryAuth({ children }: PropsWithChildren) {
const location = useLocation();
const bakery = useObservable(bakery$);
const isFirstAuthentication = useObservable(bakery?.isFirstAuthentication$);
const connected = useObservable(bakery?.connected$);
const authenticated = useObservable(bakery?.authenticated$);
const challenge = useObservable(bakery?.onChallenge);
const challenge = useObservable(bakery?.challenge$);
const { requestSignature } = useSigningContext();
const navigate = useNavigate();
@ -24,16 +23,16 @@ export default function RequireBakeryAuth({ children }: PropsWithChildren) {
if (loading.current) return;
loading.current = true;
bakery
.authenticate((draft) => requestSignature(draft))
?.catch(() => {
navigate("/bakery/connect/auth", { state: { back: (location.state?.back ?? location) satisfies To } });
})
.finally(() => (loading.current = false));
// bakery
// .authenticate((draft) => requestSignature(draft))
// ?.catch(() => {
// navigate("/bakery/connect/auth", { state: { back: (location.state?.back ?? location) satisfies To } });
// })
// .finally(() => (loading.current = false));
}, [connected, authenticated, challenge, bakery]);
// initial auth UI
if (!authenticated && isFirstAuthentication && connected)
if (!authenticated && connected)
return (
<Flex direction="column" gap="2" alignItems="center" justifyContent="center" h="full">
<Flex gap="2" alignItems="center">

View File

@ -39,7 +39,6 @@ export default function RequireBakery({ children }: PropsWithChildren & { requir
const location = useLocation();
const bakery = useObservable(bakery$);
const connected = useObservable(bakery?.connected$);
const isFirstConnection = useObservable(bakery?.isFirstConnection$);
// if there is no node connection, setup a connection
if (!bakery)
@ -51,7 +50,7 @@ export default function RequireBakery({ children }: PropsWithChildren & { requir
/>
);
if (bakery && isFirstConnection && connected === false) return <InitialConnectionOverlay />;
if (bakery && connected === false) return <InitialConnectionOverlay />;
return <>{children}</>;
}

View File

@ -0,0 +1,5 @@
import { OperatorFunction, scan } from "rxjs";
export function scanToArray<T extends unknown>(): OperatorFunction<T, T[]> {
return (source) => source.pipe(scan((arr, value) => [...arr, value], [] as T[]));
}

View File

@ -1,13 +0,0 @@
import { useActiveAccount, useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useConversationsReport() {
const account = useActiveAccount();
const pubkey = account?.pubkey;
// hardcode the report id to 'overview' so there is only ever one
const report = useReport("CONVERSATIONS", pubkey ? "conversations" : undefined, pubkey ? { pubkey } : undefined);
return useObservable(report?.value);
}

View File

@ -1,21 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useDMSearchReport(
query: string,
filter?: { conversation?: [string, string]; order?: "rank" | "created_at" },
) {
const enabled = query.length >= 3;
const report = useReport(
"DM_SEARCH",
enabled ? `dn-search-${query}` : undefined,
enabled ? { query, conversation: filter?.conversation, order: filter?.order } : undefined,
);
const messages = useObservable(report?.results);
const conversations = useObservable(report?.conversations);
return { messages, conversations };
}

View File

@ -1,10 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import { ReportArguments } from "@satellite-earth/core/types";
import useReport from "../use-report";
export default function useEventsSummaryReport(id: string, args: ReportArguments["EVENTS_SUMMARY"]) {
const report = useReport("EVENTS_SUMMARY", id, args);
return useObservable(report?.events);
}

View File

@ -1,10 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useLogsReport(service?: string) {
const report = useReport("LOGS", `logs-${service || "all"}`, { service });
const logs = useObservable(report?.entries);
return { report, logs };
}

View File

@ -1,9 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useNetworkOverviewReport() {
const report = useReport("NETWORK_STATUS", "network-status", {});
return useObservable(report?.status);
}

View File

@ -1,10 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useNotificationChannelsReport() {
const report = useReport("NOTIFICATION_CHANNELS", "notification-channels", {});
const channels = useObservable(report?.channels);
return { channels, report };
}

View File

@ -1,10 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useOverviewReport() {
// hardcode the report id to 'overview' so there is only ever one
const report = useReport("OVERVIEW", "overview", {});
return useObservable(report?.value);
}

View File

@ -1,9 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useReceiverStatusReport() {
const report = useReport("RECEIVER_STATUS", "receiver-status", {});
return useObservable(report?.status);
}

View File

@ -1,9 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useScrapperOverviewReport() {
const report = useReport("SCRAPPER_STATUS", "scrapper-status", {});
return useObservable(report?.status);
}

View File

@ -1,9 +0,0 @@
import { useObservable } from "applesauce-react/hooks";
import useReport from "../use-report";
export default function useServicesReport() {
const report = useReport("SERVICES", `services`, {});
return useObservable(report?.services);
}

View File

@ -0,0 +1,6 @@
import { useObservable } from "applesauce-react/hooks";
import { controlApi$ } from "../services/bakery";
export default function useBakeryControl() {
return useObservable(controlApi$);
}

View File

@ -13,7 +13,7 @@ export default function useReconnectAction() {
const connect = useCallback(async () => {
try {
await bakery?.connect();
// await bakery?.connect();
} catch (error) {
if (error instanceof Error) setError(error);
setCount(steps[Math.min(tries, steps.length - 1)]);

View File

@ -1,34 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import { ReportArguments } from "@satellite-earth/core/types";
import { nanoid } from "nanoid";
import { useObservable } from "applesauce-react/hooks";
import reportManager$ from "../services/reports";
export default function useReport<T extends keyof ReportArguments>(type: T, id?: string, args?: ReportArguments[T]) {
const [hookId] = useState(() => nanoid());
const argsKey = JSON.stringify(args);
const reportManager = useObservable(reportManager$);
const report = useMemo(() => {
if (id && args) return reportManager?.getOrCreateReport(type, id, args);
}, [type, id, argsKey, reportManager]);
useEffect(() => {
if (args && report) {
// @ts-expect-error
report.setArgs(args);
report.fireThrottle();
}
}, [argsKey, report]);
useEffect(() => {
if (report) {
reportManager?.addDependency(hookId, report);
return () => reportManager?.removeDependency(hookId, report);
}
}, [report, reportManager]);
return report;
}

View File

@ -1,9 +1,9 @@
import { BehaviorSubject, filter, mergeMap } from "rxjs";
import { BehaviorSubject, combineLatest, filter, lastValueFrom, map, of, shareReplay, switchMap } from "rxjs";
import { nip42 } from "nostr-tools";
import { logger } from "../helpers/debug";
import BakeryRelay from "../classes/bakery/bakery-connection";
import BakeryControlApi from "../classes/bakery/control-api";
import signingService from "./signing";
import BakeryRelay from "../classes/bakery/bakery-relay";
import BakeryControlApi from "../classes/bakery/bakery-control";
import localSettings from "./local-settings";
import accounts from "./accounts";
@ -18,13 +18,12 @@ export function clearBakeryURL() {
export const bakery$ = new BehaviorSubject<BakeryRelay | null>(null);
// connect to the bakery when the URL changes
localSettings.bakeryURL.subscribe((url) => {
if (!URL.canParse(url)) return bakery$.next(null);
try {
const bakery = new BakeryRelay(localSettings.bakeryURL.value);
bakery$.next(bakery);
bakery$.next(new BakeryRelay(localSettings.bakeryURL.value));
} catch (err) {
log("Failed to create bakery connection, clearing storage");
localSettings.bakeryURL.clear();
@ -34,31 +33,32 @@ localSettings.bakeryURL.subscribe((url) => {
// automatically authenticate with bakery
bakery$
.pipe(
filter((r) => r !== null),
mergeMap((r) => r.onChallenge),
// ignore when bakery is not created
filter((b) => b !== null),
// watch for auth challenge and account
switchMap((b) => combineLatest([of(b), b.challenge$, accounts.active$])),
)
.subscribe(async (challenge) => {
if (!challenge) return;
const bakery = bakery$.value;
if (!bakery) return;
const account = accounts.active;
.subscribe(async ([bakery, challenge, account]) => {
if (!account) return;
try {
await bakery.authenticate((draft) => signingService.requestSignature(draft, account));
const draft = nip42.makeAuthEvent(bakery.url, challenge);
const result = await lastValueFrom(bakery.auth(await account.signEvent(draft)));
console.log("Authenticated to relay", result);
} catch (err) {
console.log("Failed to authenticate with bakery", err);
}
});
export const controlApi$ = new BehaviorSubject<BakeryControlApi | null>(null);
// create the bakery control api
export const controlApi$ = bakery$.pipe(
filter((b) => !!b),
map((bakery) => new BakeryControlApi(bakery)),
shareReplay(1),
);
// create a control api for the bakery
bakery$.subscribe((relay) => {
if (!relay) return controlApi$.next(null);
else controlApi$.next(new BakeryControlApi(relay));
controlApi$.pipe(switchMap((api) => api.config)).subscribe((config) => {
console.log("config", config);
});
if (import.meta.env.DEV) {
@ -67,10 +67,3 @@ if (import.meta.env.DEV) {
// @ts-expect-error
window.controlApi$ = controlApi$;
}
export function getControlApi() {
return controlApi$.value;
}
export function getBakery() {
return bakery$.value;
}

View File

@ -1,102 +0,0 @@
import { ReportArguments, ControlResponse } from "@satellite-earth/core/types";
import { BehaviorSubject } from "rxjs";
import _throttle from "lodash.throttle";
import BakeryControlApi from "../classes/bakery/control-api";
import { controlApi$ } from "./bakery";
import Report from "../classes/bakery/reports/report";
import SuperMap from "../classes/super-map";
import { logger } from "../helpers/debug";
import { ReportClasses, ReportTypes } from "../classes/bakery/reports";
class ReportManager {
log = logger.extend("ReportManager");
control: BakeryControlApi;
reports = new Map<string, Report<any>>();
constructor(control: BakeryControlApi) {
this.control = control;
this.control.on("message", this.handleMessage.bind(this));
}
private handleMessage(message: ControlResponse) {
if (message[1] === "REPORT") {
const id = message[3];
const report = this.reports.get(id);
switch (message[2]) {
case "RESULT":
if (report) report.handleResult(message[4]);
break;
case "ERROR":
if (report) report.handleError(message[4]);
break;
default:
break;
}
}
}
// public api
getOrCreateReport<T extends keyof ReportArguments>(type: T, id: string, args: ReportArguments[T]) {
let report = this.getReport(type, id);
if (!report) report = this.createReport(type, id, args);
return report;
}
createReport<T extends keyof ReportArguments>(type: T, id: string, args: ReportArguments[T]): ReportTypes[T] {
const ReportClass = ReportClasses[type];
if (!ReportClass) throw new Error(`Failed to create report ${type}`);
const report = new ReportClass(id, args, this.control);
this.reports.set(id, report);
// @ts-expect-error
return report as ReportTypes[T];
}
getReport<T extends keyof ReportArguments>(type: T, id: string) {
return this.reports.get(id) as ReportTypes[T] | undefined;
}
removeReport<T extends keyof ReportArguments>(type: T, id: string) {
const report = this.reports.get(id) as ReportTypes[T] | undefined;
if (report && report.running) {
report.close();
this.reports.delete(id);
}
}
dependencies = new SuperMap<Report<any>, Set<string>>(() => new Set());
addDependency(id: string, report: Report<any>) {
const set = this.dependencies.get(report);
set.add(id);
report.fireThrottle();
}
removeDependency(id: string, report: Report<any>) {
const set = this.dependencies.get(report);
set.delete(id);
this.closeUnusedReportsThrottle();
}
private closeUnusedReportsThrottle = _throttle(this.closeUnusedReports.bind(this), 1000, { leading: false });
private closeUnusedReports() {
for (const [report, dependencies] of this.dependencies) {
if (report.running && dependencies.size === 0) {
this.log(`Closing ${report.type} ${report.id}`);
report.close();
}
}
}
}
const reportManager$ = new BehaviorSubject<ReportManager | null>(null);
controlApi$.subscribe((api) => {
if (api) reportManager$.next(new ReportManager(api));
else reportManager$.next(null);
});
if (import.meta.env.DEV) {
// @ts-expect-error
window.reportManager$ = reportManager$;
}
export default reportManager$;

View File

@ -39,8 +39,8 @@ export function BakeryAuthPage() {
if (!bakery) return;
try {
if (!bakery.connected) await bakery.connect();
await bakery.authenticate(auth);
// if (!bakery.connected) await bakery.connect();
// await bakery.authenticate(auth);
navigate(location.state?.back || "/", { replace: true });
} catch (error) {

View File

@ -21,9 +21,11 @@ function BakeryGeneralSettingsPage() {
const submit = handleSubmit(async (values) => {
if (!controlApi) return;
await controlApi?.send(["CONTROL", "CONFIG", "SET", "name", values.name]);
await controlApi?.send(["CONTROL", "CONFIG", "SET", "description", values.description]);
await controlApi?.send(["CONTROL", "CONFIG", "SET", "hyperEnabled", values.hyperEnabled]);
await controlApi.setConfigFields({
name: values.name,
description: values.description,
hyperEnabled: values.hyperEnabled,
});
// wait for control api to send config back
await firstValueFrom(controlApi?.config);

View File

@ -1,10 +1,12 @@
import { Alert, AlertIcon } from "@chakra-ui/react";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import PanelItemString from "../../../../components/dashboard/panel-item-string";
import useBakeryControl from "../../../../hooks/use-bakery-control";
import { useObservable } from "applesauce-react/hooks";
export default function HyperInboundStatus() {
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const status = useObservable(control?.network);
if (status === undefined) return null;
else if (!status.hyper.inbound.available) {

View File

@ -2,13 +2,12 @@ import { ReactNode } from "react";
import { Alert, AlertIcon, FormControl, FormHelperText, Switch } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import { controlApi$ } from "../../../../services/bakery";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function HyperOutboundStatus() {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const config = useObservable(control?.config);
const status = useObservable(control?.network);
let content: ReactNode = null;
if (status === undefined) content = null;
@ -40,9 +39,7 @@ export default function HyperOutboundStatus() {
<FormControl>
<Switch
isChecked={config?.enableHyperConnections}
onChange={(e) =>
controlApi?.send(["CONTROL", "CONFIG", "SET", "enableHyperConnections", e.currentTarget.checked])
}
onChange={(e) => control?.setConfigField("enableHyperConnections", e.currentTarget.checked)}
>
Connect to hyper relays
</Switch>

View File

@ -2,15 +2,14 @@ import { ReactNode } from "react";
import { Alert, Button, Code, Flex, Heading, Link, Spinner, Switch } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import HyperInboundStatus from "./hyper-inbound";
import HyperOutboundStatus from "./hyper-outbound";
import { controlApi$ } from "../../../../services/bakery";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function HyperNetworkStatus() {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const config = useObservable(control?.config);
const status = useObservable(control?.network);
let content: ReactNode = null;
@ -19,11 +18,7 @@ export default function HyperNetworkStatus() {
content = (
<Alert status="info" whiteSpace="pre-wrap">
Enable HyperDHT in order to connect to <Code>.hyper</Code> relays
<Button
variant="ghost"
onClick={() => controlApi?.send(["CONTROL", "CONFIG", "SET", "hyperEnabled", true])}
ml="auto"
>
<Button variant="ghost" onClick={() => control?.setConfigField("hyperEnabled", true)} ml="auto">
Enable
</Button>
</Alert>
@ -43,7 +38,7 @@ export default function HyperNetworkStatus() {
{config !== undefined && (
<Switch
isChecked={config?.hyperEnabled}
onChange={(e) => controlApi?.send(["CONTROL", "CONFIG", "SET", "hyperEnabled", e.currentTarget.checked])}
onChange={(e) => control?.setConfigField("hyperEnabled", e.currentTarget.checked)}
>
Enabled
</Switch>

View File

@ -1,10 +1,12 @@
import { Alert, AlertIcon, Spinner } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import PanelItemString from "../../../../components/dashboard/panel-item-string";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function I2PInboundStatus() {
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const status = useObservable(control?.network);
if (status === undefined) return <Spinner />;
else if (!status.i2p.inbound.available) {

View File

@ -2,13 +2,12 @@ import { ReactNode } from "react";
import { Alert, AlertIcon, FormControl, FormHelperText, Switch } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { controlApi$ } from "../../../../services/bakery";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function I2POutboundStatus() {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const config = useObservable(control?.config);
const status = useObservable(control?.network);
let content: ReactNode = null;
if (status === undefined) content = null;
@ -41,7 +40,7 @@ export default function I2POutboundStatus() {
<FormControl>
<Switch
isChecked={config?.enableI2PConnections}
onChange={(e) => controlApi?.setConfigField("enableI2PConnections", e.currentTarget.checked)}
onChange={(e) => control?.setConfigField("enableI2PConnections", e.currentTarget.checked)}
>
Connect to i2p relays
</Switch>

View File

@ -4,13 +4,12 @@ import { useObservable } from "applesauce-react/hooks";
import I2POutboundStatus from "./i2p-outbound";
import I2PInboundStatus from "./i2p-inbound";
import { controlApi$ } from "../../../../services/bakery";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function I2PNetworkStatus() {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const config = useObservable(control?.config);
const status = useObservable(control?.network);
let content: ReactNode = null;

View File

@ -1,4 +1,4 @@
import { Box, Divider, Flex } from "@chakra-ui/react";
import { Box, Divider } from "@chakra-ui/react";
import HyperNetworkStatus from "./hyper";
import TorNetworkStatus from "./tor";

View File

@ -1,10 +1,12 @@
import { Alert, AlertIcon, Spinner } from "@chakra-ui/react";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import PanelItemString from "../../../../components/dashboard/panel-item-string";
import useBakeryControl from "../../../../hooks/use-bakery-control";
import { useObservable } from "applesauce-react/hooks";
export default function TorInboundStatus() {
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const status = useObservable(control?.network);
if (status === undefined) return <Spinner />;
else if (!status.tor.inbound.available) {

View File

@ -2,13 +2,12 @@ import { ReactNode } from "react";
import { Alert, AlertIcon, FormControl, FormHelperText, Switch } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { controlApi$ } from "../../../../services/bakery";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function TorOutboundStatus() {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const config = useObservable(control?.config);
const status = useObservable(control?.network);
let content: ReactNode = null;
if (status === undefined) content = null;
@ -41,7 +40,7 @@ export default function TorOutboundStatus() {
<FormControl>
<Switch
isChecked={config?.enableTorConnections}
onChange={(e) => controlApi?.setConfigField("enableTorConnections", e.currentTarget.checked)}
onChange={(e) => control?.setConfigField("enableTorConnections", e.currentTarget.checked)}
>
Connect to tor relays
</Switch>
@ -52,7 +51,7 @@ export default function TorOutboundStatus() {
<FormControl>
<Switch
isChecked={config?.routeAllTrafficThroughTor}
onChange={(e) => controlApi?.setConfigField("routeAllTrafficThroughTor", e.currentTarget.checked)}
onChange={(e) => control?.setConfigField("routeAllTrafficThroughTor", e.currentTarget.checked)}
>
Route all traffic through tor proxy
</Switch>

View File

@ -4,13 +4,12 @@ import { useObservable } from "applesauce-react/hooks";
import TorOutboundStatus from "./tor-outbound";
import TorInboundStatus from "./tor-inbound";
import { controlApi$ } from "../../../../services/bakery";
import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report";
import useBakeryControl from "../../../../hooks/use-bakery-control";
export default function TorNetworkStatus() {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const status = useNetworkOverviewReport();
const control = useBakeryControl();
const config = useObservable(control?.config);
const status = useObservable(control?.network);
let content: ReactNode = null;

View File

@ -23,7 +23,7 @@ function EmailForm() {
}, [config]);
const submit = handleSubmit((values) => {
controlApi?.send(["CONTROL", "CONFIG", "SET", "notificationEmail", values.email]);
controlApi?.setConfigField("notificationEmail", values.email);
});
return (

View File

@ -1,13 +1,13 @@
import { useState } from "react";
import { Alert, Button, Code, Flex, Heading, Link, Spinner, Text } from "@chakra-ui/react";
import { nanoid } from "nanoid";
import { kinds, NostrEvent } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { useObservable } from "applesauce-react/hooks";
import { NotificationChannel } from "@satellite-earth/core/types/control-api/notifications.js";
import { useActiveAccount } from "applesauce-react/hooks";
import { bakery$, controlApi$ } from "../../../../services/bakery";
import localSettings from "../../../../services/local-settings";
import useNotificationChannelsReport from "../../../../hooks/reports/use-notification-channels";
import { CopyIconButton } from "../../../../components/copy-icon-button";
import { ExternalLinkIcon } from "../../../../components/icons";
@ -19,7 +19,8 @@ export default function NtfyNotificationSettings() {
const device = useObservable(localSettings.deviceId);
const topic = useObservable(localSettings.ntfyTopic);
const server = useObservable(localSettings.ntfyServer);
const { channels } = useNotificationChannelsReport();
// const { channels } = useNotificationChannelsReport();
const channels: Record<string, NotificationChannel> = {};
const channel = Object.values(channels || {}).find((c) => c.device === device && c.type === "ntfy");
@ -28,18 +29,18 @@ export default function NtfyNotificationSettings() {
// generate a new random id
localSettings.ntfyTopic.next(topic);
controlApi?.send([
"CONTROL",
"NOTIFICATIONS",
"REGISTER",
{ id: `ntfy:${topic}`, server, topic, type: "ntfy", device },
]);
// controlApi?.send([
// "CONTROL",
// "NOTIFICATIONS",
// "REGISTER",
// { id: `ntfy:${topic}`, server, topic, type: "ntfy", device },
// ]);
};
const disable = () => {
if (!channel) return;
controlApi?.send(["CONTROL", "NOTIFICATIONS", "UNREGISTER", channel.id]);
// controlApi?.send(["CONTROL", "NOTIFICATIONS", "UNREGISTER", channel.id]);
};
const [testing, setTesting] = useState(false);
@ -48,18 +49,18 @@ export default function NtfyNotificationSettings() {
setTesting(true);
const events: NostrEvent[] = [];
await new Promise<void>((res) => {
const sub = bakery?.subscribe([{ kinds: [kinds.EncryptedDirectMessage], limit: 10, "#p": [account.pubkey] }], {
onevent: (event) => {
events.push(event);
},
oneose: () => {
const random = events[Math.round((events.length - 1) * Math.random())];
controlApi?.send(["CONTROL", "NOTIFICATIONS", "NOTIFY", random.id]);
res();
},
});
});
// await new Promise<void>((res) => {
// const sub = bakery?.subscribe([{ kinds: [kinds.EncryptedDirectMessage], limit: 10, "#p": [account.pubkey] }], {
// onevent: (event) => {
// events.push(event);
// },
// oneose: () => {
// const random = events[Math.round((events.length - 1) * Math.random())];
// controlApi?.send(["CONTROL", "NOTIFICATIONS", "NOTIFY", random.id]);
// res();
// },
// });
// });
setTesting(false);
};

View File

@ -1,10 +1,9 @@
import { ReactNode } from "react";
import { Badge, Button, Flex, Heading, Text } from "@chakra-ui/react";
import { NotificationChannel } from "@satellite-earth/core/types/control-api/notifications.js";
import { useObservable } from "applesauce-react/hooks";
import { controlApi$ } from "../../../../services/bakery";
import useNotificationChannelsReport from "../../../../hooks/reports/use-notification-channels";
import { useObservable } from "applesauce-react/hooks";
function Channel({ channel }: { channel: NotificationChannel }) {
const controlApi = useObservable(controlApi$);
@ -30,7 +29,7 @@ function Channel({ channel }: { channel: NotificationChannel }) {
ml="auto"
size="xs"
colorScheme="red"
onClick={() => controlApi?.send(["CONTROL", "NOTIFICATIONS", "UNREGISTER", channel.id])}
// onClick={() => controlApi?.send(["CONTROL", "NOTIFICATIONS", "UNREGISTER", channel.id])}
>
Remove
</Button>
@ -41,7 +40,7 @@ function Channel({ channel }: { channel: NotificationChannel }) {
}
export default function OtherSubscriptions() {
const { channels, report } = useNotificationChannelsReport();
const channels: Record<string, NotificationChannel> = {};
if (!channels || Object.keys(channels).length === 0) return null;

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Alert, AlertIcon, Button, Code, Flex, Heading, Link, Text, useToast } from "@chakra-ui/react";
import { Alert, AlertIcon, Button, Code, Flex, Heading, useToast } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { serviceWorkerRegistration } from "../../../../services/worker";
@ -76,7 +76,7 @@ function WebPushNotificationStatus() {
export default function WebPushNotificationSettings() {
const controlApi = useObservable(controlApi$);
useEffect(() => {
controlApi?.send(["CONTROL", "NOTIFICATIONS", "GET-VAPID-KEY"]);
// controlApi?.send(["CONTROL", "NOTIFICATIONS", "GET-VAPID-KEY"]);
}, [controlApi]);
return (

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
Box,
Button,
@ -16,18 +16,17 @@ import {
import Convert from "ansi-to-html";
import { useObservable } from "applesauce-react/hooks";
import useLogsReport from "../../../../hooks/reports/use-logs-report";
import Timestamp from "../../../../components/timestamp";
import SimpleView from "../../../../components/layout/presets/simple-view";
import { controlApi$ } from "../../../../services/bakery";
import ServicesTree from "./service-tree";
import useBakeryControl from "../../../../hooks/use-bakery-control";
const convert = new Convert();
export default function BakeryServiceLogsView() {
const controlApi = useObservable(controlApi$);
const control = useBakeryControl();
const [service, setService] = useState<string | undefined>(undefined);
const { report, logs } = useLogsReport(service);
const logs = useObservable(control?.logs({ service })) ?? [];
const raw = useDisclosure();
const drawer = useDisclosure();
@ -55,16 +54,6 @@ export default function BakeryServiceLogsView() {
<Button onClick={drawer.onOpen} hideFrom="2xl">
Select Service
</Button>
<Button
onClick={() => {
if (controlApi) {
controlApi?.send(service ? ["CONTROL", "LOGS", "CLEAR", service] : ["CONTROL", "LOGS", "CLEAR"]);
report?.clear();
}
}}
>
Clear
</Button>
<Spacer />
<Switch isChecked={raw.isOpen} onChange={raw.onToggle}>
Show Raw

View File

@ -1,6 +1,8 @@
import { Button, ButtonGroup, Flex, FlexProps, IconButton, useDisclosure } from "@chakra-ui/react";
import useServicesReport from "../../../../hooks/reports/use-services-report";
import { useObservable } from "applesauce-react/hooks";
import { ChevronDownIcon, ChevronRightIcon } from "../../../../components/icons";
import useBakeryControl from "../../../../hooks/use-bakery-control";
type Service = {
id: string;
@ -74,11 +76,12 @@ export default function ServicesTree({
selected,
...props
}: Omit<FlexProps, "children"> & { select: (service: string) => void; selected?: string }) {
const services = useServicesReport() ?? [];
const control = useBakeryControl();
const services = useObservable(control?.services) ?? [];
const servicesById = new Map<string, Service>();
for (const service of services) {
getOrCreate(servicesById, service.id.split(":"));
getOrCreate(servicesById, service.split(":"));
}
const rootServices = Array.from(servicesById.values()).filter((service) => !service.parent);

View File

@ -66,11 +66,8 @@ export default function BakerySetupView() {
}
if (!pubkey) throw new Error("Unable to find nostr public key");
if (!authParam.value) throw new Error("Missing auth code");
await bakery.authenticate(authParam.value);
controlApi.send(["CONTROL", "CONFIG", "SET", "owner", pubkey]);
controlApi.setConfigField("owner", pubkey);
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}