diff --git a/server/README.md b/server/README.md deleted file mode 100644 index 1c2c9c8ea..000000000 --- a/server/README.md +++ /dev/null @@ -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 diff --git a/server/dockerfile b/server/dockerfile deleted file mode 100644 index 35969b9ba..000000000 --- a/server/dockerfile +++ /dev/null @@ -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"] diff --git a/server/entrypoint.sh b/server/entrypoint.sh deleted file mode 100755 index 3d81087eb..000000000 --- a/server/entrypoint.sh +++ /dev/null @@ -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 diff --git a/server/index.js b/server/index.js deleted file mode 100644 index 329bd0438..000000000 --- a/server/index.js +++ /dev/null @@ -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); - }); diff --git a/server/package.json b/server/package.json deleted file mode 100644 index 4242b2598..000000000 --- a/server/package.json +++ /dev/null @@ -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" - } -} diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml deleted file mode 100644 index 8cbf72093..000000000 --- a/server/pnpm-lock.yaml +++ /dev/null @@ -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: {} diff --git a/src/classes/bakery/bakery-connection.ts b/src/classes/bakery/bakery-connection.ts deleted file mode 100644 index 3fda88e9e..000000000 --- a/src/classes/bakery/bakery-connection.ts +++ /dev/null @@ -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(); - - 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 | null = null; - - onChallenge = new Subject(); - - authenticate(auth: string | ((evt: EventTemplate) => Promise)) { - if (!this.connected) throw new Error("Not connected"); - - if (!this.authenticated$.value && !this.authPromise) { - this.authPromise = createDefer(); - - 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) { - 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"]); - } -} diff --git a/src/classes/bakery/bakery-control.ts b/src/classes/bakery/bakery-control.ts new file mode 100644 index 000000000..0ba88cf0d --- /dev/null +++ b/src/classes/bakery/bakery-control.ts @@ -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>(); + + config: Observable; + network: Observable; + services: Observable; + + constructor(public bakery: BakeryRelay) { + this.config = this.query("config", {}).pipe(shareReplay(1)); + this.network = this.query("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( + type: string, + args: any, + modify: (source: Observable) => Observable, + ): Observable; + query(type: string, args: any): Observable; + query( + type: string, + args: any, + modify?: (source: Observable) => Observable, + ): Observable { + 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("logs", filter, (source) => source.pipe(scanToArray())); + } + + async setConfigField(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) { + 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))); + } +} diff --git a/src/classes/bakery/bakery-relay.ts b/src/classes/bakery/bakery-relay.ts new file mode 100644 index 000000000..6fd6b90a1 --- /dev/null +++ b/src/classes/bakery/bakery-relay.ts @@ -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 { + return (source) => + source.pipe( + filter((r) => r.type === "EVENT"), + map((r) => r.event), + ); +} + +export default class BakeryRelay { + log = logger.extend("Bakery"); + public socket$: WebSocketSubject; + + connected$ = new BehaviorSubject(false); + challenge$: Observable; + 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 { + 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((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({ 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"), + ); + } +} diff --git a/src/classes/bakery/control-api.ts b/src/classes/bakery/control-api.ts deleted file mode 100644 index beb7afcbb..000000000 --- a/src/classes/bakery/control-api.ts +++ /dev/null @@ -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 { - node: BakeryRelay; - - config = new BehaviorSubject(undefined); - /** @deprecated this should be a report */ - databaseStats = new Subject(); - vapidKey = new BehaviorSubject(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(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((res) => { - const sub = this.config.subscribe((config) => { - if (config) res(config); - sub.unsubscribe(); - }); - }); - } -} diff --git a/src/classes/bakery/reports/conversations.ts b/src/classes/bakery/reports/conversations.ts deleted file mode 100644 index 8488b9547..000000000 --- a/src/classes/bakery/reports/conversations.ts +++ /dev/null @@ -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([]); - - 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); - } -} diff --git a/src/classes/bakery/reports/dm-search.ts b/src/classes/bakery/reports/dm-search.ts deleted file mode 100644 index c020922c6..000000000 --- a/src/classes/bakery/reports/dm-search.ts +++ /dev/null @@ -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([]); - conversations = new BehaviorSubject([]); - - 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] }]); - } - } -} diff --git a/src/classes/bakery/reports/events-summary.ts b/src/classes/bakery/reports/events-summary.ts deleted file mode 100644 index 09057809e..000000000 --- a/src/classes/bakery/reports/events-summary.ts +++ /dev/null @@ -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([]); - - 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]); - } -} diff --git a/src/classes/bakery/reports/index.ts b/src/classes/bakery/reports/index.ts deleted file mode 100644 index de0410410..000000000 --- a/src/classes/bakery/reports/index.ts +++ /dev/null @@ -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; -} = { - 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; -}; diff --git a/src/classes/bakery/reports/logs.ts b/src/classes/bakery/reports/logs.ts deleted file mode 100644 index a1b1bbdaa..000000000 --- a/src/classes/bakery/reports/logs.ts +++ /dev/null @@ -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(); - entries = new BehaviorSubject([]); - - 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([]); - } -} diff --git a/src/classes/bakery/reports/network-status.ts b/src/classes/bakery/reports/network-status.ts deleted file mode 100644 index b85279634..000000000 --- a/src/classes/bakery/reports/network-status.ts +++ /dev/null @@ -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(undefined); - - handleResult(response: ReportResults["NETWORK_STATUS"]): void { - this.status.next(response); - } -} diff --git a/src/classes/bakery/reports/notification-channels.ts b/src/classes/bakery/reports/notification-channels.ts deleted file mode 100644 index 6f140b448..000000000 --- a/src/classes/bakery/reports/notification-channels.ts +++ /dev/null @@ -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 | 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, - }); - } -} diff --git a/src/classes/bakery/reports/overview.ts b/src/classes/bakery/reports/overview.ts deleted file mode 100644 index 9d0152ee3..000000000 --- a/src/classes/bakery/reports/overview.ts +++ /dev/null @@ -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([]); - - 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); - } -} diff --git a/src/classes/bakery/reports/receiver-status.ts b/src/classes/bakery/reports/receiver-status.ts deleted file mode 100644 index 4ebd351aa..000000000 --- a/src/classes/bakery/reports/receiver-status.ts +++ /dev/null @@ -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(undefined); - - handleResult(response: ReportResults["RECEIVER_STATUS"]): void { - this.status.next(response); - } -} diff --git a/src/classes/bakery/reports/report.ts b/src/classes/bakery/reports/report.ts deleted file mode 100644 index 718dcd65c..000000000 --- a/src/classes/bakery/reports/report.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/classes/bakery/reports/scrapper-status.ts b/src/classes/bakery/reports/scrapper-status.ts deleted file mode 100644 index 20f910f6a..000000000 --- a/src/classes/bakery/reports/scrapper-status.ts +++ /dev/null @@ -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(undefined); - - handleResult(response: ReportResults["SCRAPPER_STATUS"]): void { - this.status.next(response); - } -} diff --git a/src/classes/bakery/reports/services.ts b/src/classes/bakery/reports/services.ts deleted file mode 100644 index 75ef601c3..000000000 --- a/src/classes/bakery/reports/services.ts +++ /dev/null @@ -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([]); - - handleResult(result: ReportResults["SERVICES"]) { - this.services.next(this.services.value.filter((s) => s.id !== result.id).concat(result)); - } -} diff --git a/src/classes/bakery/types.ts b/src/classes/bakery/types.ts new file mode 100644 index 000000000..1d6ad5ebc --- /dev/null +++ b/src/classes/bakery/types.ts @@ -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; +}; diff --git a/src/components/router/require-bakery-auth.tsx b/src/components/router/require-bakery-auth.tsx index 3d47b9e77..1c90a9b40 100644 --- a/src/components/router/require-bakery-auth.tsx +++ b/src/components/router/require-bakery-auth.tsx @@ -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 ( diff --git a/src/components/router/require-bakery.tsx b/src/components/router/require-bakery.tsx index 17dfd2dfb..ba3107da5 100644 --- a/src/components/router/require-bakery.tsx +++ b/src/components/router/require-bakery.tsx @@ -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 ; + if (bakery && connected === false) return ; return <>{children}; } diff --git a/src/helpers/observable.ts b/src/helpers/observable.ts new file mode 100644 index 000000000..7b946d42d --- /dev/null +++ b/src/helpers/observable.ts @@ -0,0 +1,5 @@ +import { OperatorFunction, scan } from "rxjs"; + +export function scanToArray(): OperatorFunction { + return (source) => source.pipe(scan((arr, value) => [...arr, value], [] as T[])); +} diff --git a/src/hooks/reports/use-conversations-report.ts b/src/hooks/reports/use-conversations-report.ts deleted file mode 100644 index 4e176e02c..000000000 --- a/src/hooks/reports/use-conversations-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/reports/use-dm-search-report.ts b/src/hooks/reports/use-dm-search-report.ts deleted file mode 100644 index f33e659c5..000000000 --- a/src/hooks/reports/use-dm-search-report.ts +++ /dev/null @@ -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 }; -} diff --git a/src/hooks/reports/use-events-summary-report.ts b/src/hooks/reports/use-events-summary-report.ts deleted file mode 100644 index 6b7bd150f..000000000 --- a/src/hooks/reports/use-events-summary-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/reports/use-logs-report.ts b/src/hooks/reports/use-logs-report.ts deleted file mode 100644 index 01ff7c256..000000000 --- a/src/hooks/reports/use-logs-report.ts +++ /dev/null @@ -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 }; -} diff --git a/src/hooks/reports/use-network-status-report.ts b/src/hooks/reports/use-network-status-report.ts deleted file mode 100644 index cde1ea8a0..000000000 --- a/src/hooks/reports/use-network-status-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/reports/use-notification-channels.ts b/src/hooks/reports/use-notification-channels.ts deleted file mode 100644 index 534947ac9..000000000 --- a/src/hooks/reports/use-notification-channels.ts +++ /dev/null @@ -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 }; -} diff --git a/src/hooks/reports/use-overview-report.ts b/src/hooks/reports/use-overview-report.ts deleted file mode 100644 index 30d3dac55..000000000 --- a/src/hooks/reports/use-overview-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/reports/use-receiver-status-report.ts b/src/hooks/reports/use-receiver-status-report.ts deleted file mode 100644 index 07ef0837a..000000000 --- a/src/hooks/reports/use-receiver-status-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/reports/use-scrapper-status-report.ts b/src/hooks/reports/use-scrapper-status-report.ts deleted file mode 100644 index 347295b0b..000000000 --- a/src/hooks/reports/use-scrapper-status-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/reports/use-services-report.ts b/src/hooks/reports/use-services-report.ts deleted file mode 100644 index 32568b81e..000000000 --- a/src/hooks/reports/use-services-report.ts +++ /dev/null @@ -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); -} diff --git a/src/hooks/use-bakery-control.ts b/src/hooks/use-bakery-control.ts new file mode 100644 index 000000000..a1a3eeee4 --- /dev/null +++ b/src/hooks/use-bakery-control.ts @@ -0,0 +1,6 @@ +import { useObservable } from "applesauce-react/hooks"; +import { controlApi$ } from "../services/bakery"; + +export default function useBakeryControl() { + return useObservable(controlApi$); +} diff --git a/src/hooks/use-reconnect-action.ts b/src/hooks/use-reconnect-action.ts index 278588cd9..21d01d618 100644 --- a/src/hooks/use-reconnect-action.ts +++ b/src/hooks/use-reconnect-action.ts @@ -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)]); diff --git a/src/hooks/use-report.ts b/src/hooks/use-report.ts deleted file mode 100644 index fd7cfde63..000000000 --- a/src/hooks/use-report.ts +++ /dev/null @@ -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(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; -} diff --git a/src/services/bakery.ts b/src/services/bakery.ts index a08572c79..05acfbb0e 100644 --- a/src/services/bakery.ts +++ b/src/services/bakery.ts @@ -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(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(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; -} diff --git a/src/services/reports.ts b/src/services/reports.ts deleted file mode 100644 index 5f3f22a65..000000000 --- a/src/services/reports.ts +++ /dev/null @@ -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>(); - - 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(type: T, id: string, args: ReportArguments[T]) { - let report = this.getReport(type, id); - if (!report) report = this.createReport(type, id, args); - return report; - } - createReport(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(type: T, id: string) { - return this.reports.get(id) as ReportTypes[T] | undefined; - } - removeReport(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, Set>(() => new Set()); - addDependency(id: string, report: Report) { - const set = this.dependencies.get(report); - set.add(id); - report.fireThrottle(); - } - removeDependency(id: string, report: Report) { - 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(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$; diff --git a/src/views/settings/bakery/connect/auth.tsx b/src/views/settings/bakery/connect/auth.tsx index a008f69d6..62b063c7f 100644 --- a/src/views/settings/bakery/connect/auth.tsx +++ b/src/views/settings/bakery/connect/auth.tsx @@ -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) { diff --git a/src/views/settings/bakery/general-settings/index.tsx b/src/views/settings/bakery/general-settings/index.tsx index e3f9e7856..8823a4e03 100644 --- a/src/views/settings/bakery/general-settings/index.tsx +++ b/src/views/settings/bakery/general-settings/index.tsx @@ -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); diff --git a/src/views/settings/bakery/network/hyper-inbound.tsx b/src/views/settings/bakery/network/hyper-inbound.tsx index a12c6bef8..51e5ed62e 100644 --- a/src/views/settings/bakery/network/hyper-inbound.tsx +++ b/src/views/settings/bakery/network/hyper-inbound.tsx @@ -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) { diff --git a/src/views/settings/bakery/network/hyper-outbound.tsx b/src/views/settings/bakery/network/hyper-outbound.tsx index f83c9bfaf..5f884abc9 100644 --- a/src/views/settings/bakery/network/hyper-outbound.tsx +++ b/src/views/settings/bakery/network/hyper-outbound.tsx @@ -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() { - controlApi?.send(["CONTROL", "CONFIG", "SET", "enableHyperConnections", e.currentTarget.checked]) - } + onChange={(e) => control?.setConfigField("enableHyperConnections", e.currentTarget.checked)} > Connect to hyper relays diff --git a/src/views/settings/bakery/network/hyper.tsx b/src/views/settings/bakery/network/hyper.tsx index b332671ac..f0eaf132c 100644 --- a/src/views/settings/bakery/network/hyper.tsx +++ b/src/views/settings/bakery/network/hyper.tsx @@ -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 = ( Enable HyperDHT in order to connect to .hyper relays - @@ -43,7 +38,7 @@ export default function HyperNetworkStatus() { {config !== undefined && ( controlApi?.send(["CONTROL", "CONFIG", "SET", "hyperEnabled", e.currentTarget.checked])} + onChange={(e) => control?.setConfigField("hyperEnabled", e.currentTarget.checked)} > Enabled diff --git a/src/views/settings/bakery/network/i2p-inbound.tsx b/src/views/settings/bakery/network/i2p-inbound.tsx index 156981f1f..42406779c 100644 --- a/src/views/settings/bakery/network/i2p-inbound.tsx +++ b/src/views/settings/bakery/network/i2p-inbound.tsx @@ -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 ; else if (!status.i2p.inbound.available) { diff --git a/src/views/settings/bakery/network/i2p-outbound.tsx b/src/views/settings/bakery/network/i2p-outbound.tsx index 389c316e0..a5eab9f72 100644 --- a/src/views/settings/bakery/network/i2p-outbound.tsx +++ b/src/views/settings/bakery/network/i2p-outbound.tsx @@ -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() { controlApi?.setConfigField("enableI2PConnections", e.currentTarget.checked)} + onChange={(e) => control?.setConfigField("enableI2PConnections", e.currentTarget.checked)} > Connect to i2p relays diff --git a/src/views/settings/bakery/network/i2p.tsx b/src/views/settings/bakery/network/i2p.tsx index fae03fda8..8c405cd71 100644 --- a/src/views/settings/bakery/network/i2p.tsx +++ b/src/views/settings/bakery/network/i2p.tsx @@ -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; diff --git a/src/views/settings/bakery/network/index.tsx b/src/views/settings/bakery/network/index.tsx index b798a9156..8ce946a52 100644 --- a/src/views/settings/bakery/network/index.tsx +++ b/src/views/settings/bakery/network/index.tsx @@ -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"; diff --git a/src/views/settings/bakery/network/tor-inbound.tsx b/src/views/settings/bakery/network/tor-inbound.tsx index 95d12b722..fdd048b36 100644 --- a/src/views/settings/bakery/network/tor-inbound.tsx +++ b/src/views/settings/bakery/network/tor-inbound.tsx @@ -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 ; else if (!status.tor.inbound.available) { diff --git a/src/views/settings/bakery/network/tor-outbound.tsx b/src/views/settings/bakery/network/tor-outbound.tsx index 08909c558..4e9161ecc 100644 --- a/src/views/settings/bakery/network/tor-outbound.tsx +++ b/src/views/settings/bakery/network/tor-outbound.tsx @@ -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() { controlApi?.setConfigField("enableTorConnections", e.currentTarget.checked)} + onChange={(e) => control?.setConfigField("enableTorConnections", e.currentTarget.checked)} > Connect to tor relays @@ -52,7 +51,7 @@ export default function TorOutboundStatus() { controlApi?.setConfigField("routeAllTrafficThroughTor", e.currentTarget.checked)} + onChange={(e) => control?.setConfigField("routeAllTrafficThroughTor", e.currentTarget.checked)} > Route all traffic through tor proxy diff --git a/src/views/settings/bakery/network/tor.tsx b/src/views/settings/bakery/network/tor.tsx index 354d75eee..5b3260480 100644 --- a/src/views/settings/bakery/network/tor.tsx +++ b/src/views/settings/bakery/network/tor.tsx @@ -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; diff --git a/src/views/settings/bakery/notifications/index.tsx b/src/views/settings/bakery/notifications/index.tsx index e411022d2..5a2599349 100644 --- a/src/views/settings/bakery/notifications/index.tsx +++ b/src/views/settings/bakery/notifications/index.tsx @@ -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 ( diff --git a/src/views/settings/bakery/notifications/ntfy.tsx b/src/views/settings/bakery/notifications/ntfy.tsx index e82965d3d..82c87ad62 100644 --- a/src/views/settings/bakery/notifications/ntfy.tsx +++ b/src/views/settings/bakery/notifications/ntfy.tsx @@ -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 = {}; 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((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((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); }; diff --git a/src/views/settings/bakery/notifications/other.tsx b/src/views/settings/bakery/notifications/other.tsx index 921561e0c..c57f0343a 100644 --- a/src/views/settings/bakery/notifications/other.tsx +++ b/src/views/settings/bakery/notifications/other.tsx @@ -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 @@ -41,7 +40,7 @@ function Channel({ channel }: { channel: NotificationChannel }) { } export default function OtherSubscriptions() { - const { channels, report } = useNotificationChannelsReport(); + const channels: Record = {}; if (!channels || Object.keys(channels).length === 0) return null; diff --git a/src/views/settings/bakery/notifications/web-push.tsx b/src/views/settings/bakery/notifications/web-push.tsx index 63b046c74..b43b8c65c 100644 --- a/src/views/settings/bakery/notifications/web-push.tsx +++ b/src/views/settings/bakery/notifications/web-push.tsx @@ -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 ( diff --git a/src/views/settings/bakery/service-logs/index.tsx b/src/views/settings/bakery/service-logs/index.tsx index 31780046c..1d27a7978 100644 --- a/src/views/settings/bakery/service-logs/index.tsx +++ b/src/views/settings/bakery/service-logs/index.tsx @@ -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(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() { - Show Raw diff --git a/src/views/settings/bakery/service-logs/service-tree.tsx b/src/views/settings/bakery/service-logs/service-tree.tsx index 09d6eedbf..3414f6546 100644 --- a/src/views/settings/bakery/service-logs/service-tree.tsx +++ b/src/views/settings/bakery/service-logs/service-tree.tsx @@ -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 & { select: (service: string) => void; selected?: string }) { - const services = useServicesReport() ?? []; + const control = useBakeryControl(); + const services = useObservable(control?.services) ?? []; const servicesById = new Map(); for (const service of services) { - getOrCreate(servicesById, service.id.split(":")); + getOrCreate(servicesById, service.split(":")); } const rootServices = Array.from(servicesById.values()).filter((service) => !service.parent); diff --git a/src/views/settings/bakery/setup/index.tsx b/src/views/settings/bakery/setup/index.tsx index b04314715..eb12fd6b3 100644 --- a/src/views/settings/bakery/setup/index.tsx +++ b/src/views/settings/bakery/setup/index.tsx @@ -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 }); }