finish basic webrtc classes

This commit is contained in:
hzrd149
2024-07-22 21:52:42 -05:00
parent 8e52346e6e
commit 7b45e6f296
10 changed files with 720 additions and 115 deletions

View File

@@ -0,0 +1,151 @@
import { SubCloser } from "nostr-tools/abstract-pool";
import EventEmitter from "eventemitter3";
import { generateSecretKey, nip19, NostrEvent } from "nostr-tools";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import dayjs from "dayjs";
import NostrWebRTCPeer, { Pool, RTCDescriptionEventKind, Signer } from "./nostr-webrtc-peer";
import { isHex } from "../helpers/nip19";
import { logger } from "../helpers/debug";
import SimpleSigner from "./simple-signer";
type EventMap = {
call: [NostrEvent];
};
export default class NostrWebRtcBroker extends EventEmitter<EventMap> {
log = logger.extend("NostrWebRtcBroker");
signer: Signer;
pool: Pool;
defaultRelays: string[];
peers = new Map<string, NostrWebRTCPeer>();
signers = new Map<string, Signer>();
relays = new Map<string, string[]>();
constructor(signer: Signer, pool: Pool, relays: string[]) {
super();
this.signer = signer;
this.pool = pool;
this.defaultRelays = relays;
}
getConnection(pubkey: string) {
return this.peers.get(pubkey);
}
async requestConnection(uri: string) {
const { pubkey, relays, key } = NostrWebRtcBroker.parseNostrWebRtcURI(uri);
const cached = this.peers.get(pubkey);
if (cached) return cached;
this.log(`Creating new connection for ${pubkey}`);
// set signer
let signer = this.signer;
if (key) {
signer = new SimpleSigner(key);
this.signers.set(pubkey, signer);
}
// set relays
if (relays.length > 0) this.relays.set(pubkey, relays);
else this.relays.set(pubkey, this.defaultRelays);
const peer = new NostrWebRTCPeer(signer, this.pool, relays.length > 0 ? relays : this.defaultRelays);
this.peers.set(pubkey, peer);
await peer.makeCall(pubkey);
return peer;
}
setPeerSigner(pubkey: string, signer: Signer) {
this.signers.set(pubkey, signer);
}
async answerCall(event: NostrEvent): Promise<NostrWebRTCPeer> {
if (this.peers.has(event.pubkey)) throw new Error("Already have a peer connection for this pubkey");
// set signer
let signer = this.signers.get(event.pubkey);
if (!signer) {
signer = this.signer;
this.signers.set(event.pubkey, signer);
}
const peer = new NostrWebRTCPeer(signer, this.pool, this.defaultRelays);
this.peers.set(event.pubkey, peer);
await peer.answerCall(event);
return peer;
}
closeConnection(pubkey: string) {
const peer = this.peers.get(pubkey);
if (peer) {
this.log(`Hanging up connection to ${pubkey}`);
peer.hangup();
this.peers.delete(pubkey);
}
}
listening = false;
subscription?: SubCloser;
async listenForCalls() {
if (this.listening) throw new Error("Already listening");
this.log("Listening for calls");
this.listening = true;
this.subscription = this.pool.subscribeMany(
this.defaultRelays,
[{ kinds: [RTCDescriptionEventKind], "#p": [await this.signer.getPublicKey()], since: dayjs().unix() }],
{
onevent: (event) => {
this.emit("call", event);
},
onclose: () => {
this.listening = false;
},
},
);
}
stopListening() {
if (!this.listening) return;
this.log("Stop listening for calls");
if (this.subscription) this.subscription.close();
this.subscription = undefined;
this.listening = false;
}
static parseNostrWebRtcURI(uri: string | URL) {
const url = typeof uri === "string" ? new URL(uri) : uri;
if (url.protocol !== "webrtc+nostr:") throw new Error("Incorrect protocol");
const parsedPath = nip19.decode(url.pathname);
const keyParam = url.searchParams.get("key");
const relays = url.searchParams.getAll("relay");
if (parsedPath.type !== "npub") throw new Error("Incorrect npub");
const pubkey = parsedPath.data;
if (keyParam && !isHex(keyParam)) throw new Error("Key must be in hex format");
const key = keyParam ? hexToBytes(keyParam) : null;
return { pubkey, key, relays };
}
static createNostrWebRtcURI(pubkey: string, relays: string[], key?: Uint8Array | boolean) {
const uri = new URL(`webrtc+nostr:${nip19.npubEncode(pubkey)}`);
for (const relay of relays) uri.searchParams.append("relay", relay);
if (key === true) uri.searchParams.append("key", bytesToHex(generateSecretKey()));
else if (key instanceof Uint8Array) uri.searchParams.append("key", bytesToHex(key));
return uri.toString();
}
}
if (import.meta.env.DEV) {
// @ts-expect-error
window.NostrWebRtcBroker = NostrWebRtcBroker;
}

View File

@@ -1,23 +1,15 @@
import { Debugger } from "debug";
import EventEmitter from "eventemitter3";
import dayjs from "dayjs";
import {
EventTemplate,
Filter,
NostrEvent,
SimplePool,
finalizeEvent,
generateSecretKey,
getPublicKey,
nip44,
} from "nostr-tools";
import { EventTemplate, Filter, NostrEvent } from "nostr-tools";
import { SubCloser, SubscribeManyParams } from "nostr-tools/abstract-pool";
import { logger } from "../../../helpers/debug";
import { logger } from "../helpers/debug";
const RTCDescriptionEventKind = 25050;
const RTCICEEventKind = 25051;
type Signer = {
export const RTCDescriptionEventKind = 25050;
export const RTCICEEventKind = 25051;
export type Signer = {
getPublicKey: () => Promise<string> | string;
signEvent: (event: EventTemplate) => Promise<NostrEvent> | NostrEvent;
nip44: {
@@ -26,42 +18,18 @@ type Signer = {
};
};
type Pool = {
export type Pool = {
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser;
publish(relays: string[], event: NostrEvent): Promise<string>[];
};
type EventMap = {
connect: [];
disconnect: [];
incomingCall: [NostrEvent];
connected: [];
disconnected: [];
message: [string];
};
class SimpleSigner {
key: Uint8Array;
constructor() {
this.key = generateSecretKey();
}
async getPublicKey() {
return getPublicKey(this.key);
}
async signEvent(event: EventTemplate) {
return finalizeEvent(event, this.key);
}
nip44 = {
encrypt: async (pubkey: string, plaintext: string) =>
nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
decrypt: async (pubkey: string, ciphertext: string) =>
nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
};
}
const defaultPool = new SimplePool();
class WebRTCPeer extends EventEmitter<EventMap> {
export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
log: Debugger;
signer: Signer;
pool: Pool;
@@ -72,7 +40,6 @@ class WebRTCPeer extends EventEmitter<EventMap> {
connection?: RTCPeerConnection;
channel?: RTCDataChannel;
listening = false;
subscription?: SubCloser;
async isCaller() {
@@ -90,9 +57,9 @@ class WebRTCPeer extends EventEmitter<EventMap> {
private candidateQueue: RTCIceCandidateInit[] = [];
constructor(signer: Signer, pool: Pool = defaultPool, relays?: string[], iceServers?: RTCIceServer[]) {
constructor(signer: Signer, pool: Pool, relays?: string[], iceServers?: RTCIceServer[]) {
super();
this.log = logger.extend(`webrtc`);
this.log = logger.extend(`NostrWebRTCPeer`);
this.signer = signer;
this.pool = pool;
@@ -115,7 +82,7 @@ class WebRTCPeer extends EventEmitter<EventMap> {
this.connection.onicegatheringstatechange = this.flushCandidateQueue.bind(this);
this.connection.ondatachannel = ({ channel }) => {
this.log("Got data channel", channel);
this.log("Got data channel", channel.id, channel.label);
if (channel.label !== "nostr") return;
@@ -125,6 +92,17 @@ class WebRTCPeer extends EventEmitter<EventMap> {
this.channel.onmessage = this.handleChannelMessage.bind(this);
};
this.connection.onconnectionstatechange = (event) => {
switch (this.connection?.connectionState) {
case "connected":
this.emit("connected");
break;
case "disconnected":
this.emit("disconnected");
break;
}
};
return this.connection;
}
@@ -140,7 +118,7 @@ class WebRTCPeer extends EventEmitter<EventMap> {
created_at: dayjs().unix(),
});
this.log(`Publishing ICE candidates`, this.candidateQueue);
this.log(`Publishing ${this.candidateQueue.length} ICE candidates`);
await this.pool.publish(this.relays, iceEvent);
this.candidateQueue = [];
}
@@ -149,7 +127,6 @@ class WebRTCPeer extends EventEmitter<EventMap> {
async makeCall(peer: string) {
if (this.peer) throw new Error("Already calling peer");
this.stopListening();
const pc = this.createConnection();
this.channel = pc.createDataChannel("nostr", { ordered: true });
@@ -168,7 +145,7 @@ class WebRTCPeer extends EventEmitter<EventMap> {
created_at: dayjs().unix(),
});
this.log("Created offer", offer);
this.log("Created offer");
// listen for answers and ice events
this.subscription = this.pool.subscribeMany(
@@ -199,14 +176,14 @@ class WebRTCPeer extends EventEmitter<EventMap> {
}
},
onclose: () => {
this.log("Subscription Closed");
this.log("Signaling subscription closed");
},
},
);
this.peer = peer;
this.log("Publishing event", offerEvent);
this.log("Publishing event", offerEvent.id);
await this.pool.publish(this.relays, offerEvent);
await pc.setLocalDescription(offer);
@@ -222,7 +199,7 @@ class WebRTCPeer extends EventEmitter<EventMap> {
const answer = JSON.parse(plaintext) as RTCSessionDescriptionInit;
if (answer.type !== "answer") throw new Error("Unexpected rtc description type");
this.log("Got answer", answer);
this.log("Got answer");
await pc.setRemoteDescription(answer);
@@ -230,7 +207,6 @@ class WebRTCPeer extends EventEmitter<EventMap> {
}
async answerCall(event: NostrEvent) {
this.stopListening();
const pc = this.createConnection();
this.log(`Answering call ${event.id} from ${event.pubkey}`);
@@ -240,6 +216,8 @@ class WebRTCPeer extends EventEmitter<EventMap> {
if (offer.type !== "offer") throw new Error("Unexpected rtc description type");
this.relays = event.tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]);
this.log(`Switching to callers signaling relays`, this.relays);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
@@ -254,7 +232,7 @@ class WebRTCPeer extends EventEmitter<EventMap> {
created_at: dayjs().unix(),
});
this.log("Created answer", answer);
this.log("Created answer");
this.peer = event.pubkey;
this.offerEvent = event;
@@ -275,12 +253,12 @@ class WebRTCPeer extends EventEmitter<EventMap> {
}
},
onclose: () => {
this.log("Subscription Closed");
this.log("Signaling subscription closed");
},
},
);
this.log("Publishing event", answerEvent);
this.log("Publishing event", answerEvent.id);
await this.pool.publish(this.relays, answerEvent);
await pc.setLocalDescription(answer);
@@ -297,39 +275,13 @@ class WebRTCPeer extends EventEmitter<EventMap> {
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
const candidates = JSON.parse(plaintext) as RTCIceCandidateInit[];
this.log("Got candidates", candidates);
this.log(`Got ${candidates.length} candidates`);
for (let candidate of candidates) {
await pc.addIceCandidate(candidate);
}
}
async listenForCall() {
if (this.listening) throw new Error("Already listening");
this.listening = true;
this.subscription = this.pool.subscribeMany(
this.relays,
[{ kinds: [RTCDescriptionEventKind], "#p": [await this.signer.getPublicKey()], since: dayjs().unix() }],
{
onevent: (event) => {
this.emit("incomingCall", event);
},
onclose: () => {
this.listening = false;
},
},
);
}
stopListening() {
if (!this.listening) return;
if (this.subscription) this.subscription.close();
this.subscription = undefined;
this.listening = false;
}
private onChannelStateChange() {
const readyState = this.channel?.readyState;
console.log("Send channel state is: " + readyState);
@@ -343,14 +295,15 @@ class WebRTCPeer extends EventEmitter<EventMap> {
this.channel?.send(message);
}
disconnect() {
hangup() {
this.log("Closing data channel");
if (this.channel) this.channel.close();
this.log("Closing connection");
if (this.connection) this.connection.close();
}
}
// @ts-expect-error
window.SimpleSigner = SimpleSigner;
// @ts-expect-error
window.WebRTCPeer = WebRTCPeer;
if (import.meta.env.DEV) {
// @ts-expect-error
window.WebRTCPeer = NostrWebRTCPeer;
}

View File

@@ -0,0 +1,31 @@
import { EventTemplate, finalizeEvent, generateSecretKey, getPublicKey, nip04, nip44 } from "nostr-tools";
export default class SimpleSigner {
key: Uint8Array;
constructor(key?: Uint8Array) {
this.key = key || generateSecretKey();
}
async getPublicKey() {
return getPublicKey(this.key);
}
async signEvent(event: EventTemplate) {
return finalizeEvent(event, this.key);
}
nip04 = {
encrypt: async (pubkey: string, plaintext: string) => nip04.encrypt(this.key, pubkey, plaintext),
decrypt: async (pubkey: string, ciphertext: string) => nip04.decrypt(this.key, pubkey, ciphertext),
};
nip44 = {
encrypt: async (pubkey: string, plaintext: string) =>
nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
decrypt: async (pubkey: string, ciphertext: string) =>
nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
};
}
if (import.meta.env.DEV) {
// @ts-expect-error
window.SimpleSigner = SimpleSigner;
}

View File

@@ -0,0 +1,118 @@
import NostrWebRTCPeer from "./nostr-webrtc-peer";
import { AbstractRelay, AbstractRelayConstructorOptions } from "nostr-tools/abstract-relay";
export class WebRtcWebSocket extends EventTarget implements WebSocket {
binaryType: BinaryType = "blob";
bufferedAmount: number = 0;
extensions: string = "";
protocol: string = "webrtc";
peer: NostrWebRTCPeer;
url: string;
onclose: ((this: WebSocket, ev: CloseEvent) => any) | null = null;
onerror: ((this: WebSocket, ev: Event) => any) | null = null;
onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null = null;
onopen: ((this: WebSocket, ev: Event) => any) | null = null;
constructor(peer: NostrWebRTCPeer) {
super();
this.peer = peer;
this.url = `webrtc+nostr:` + peer.answerEvent?.pubkey;
this.peer.on("message", this.handleMessage, this);
this.peer.on("connected", this.handleConnect, this);
this.peer.on("disconnected", this.handleDisconnect, this);
if (this.readyState === WebRtcWebSocket.OPEN) {
setTimeout(() => this.handleConnect(), 100);
}
}
get readyState() {
const state = this.peer.connection?.connectionState;
switch (state) {
case "closed":
case "disconnected":
return this.CLOSED;
case "failed":
return this.CLOSED;
case "connected":
return this.OPEN;
case "new":
case "connecting":
default:
return this.CONNECTING;
}
}
private handleMessage(data: string) {
const event = new MessageEvent("message", { data });
this.onmessage?.(event);
this.dispatchEvent(event);
}
private handleConnect() {
const event = new Event("open");
this.onopen?.(event);
this.dispatchEvent(event);
}
private handleDisconnect() {
const event = new CloseEvent("close", { reason: "none" });
this.onclose?.(event);
this.dispatchEvent(event);
this.peer.off("message", this.handleMessage, this);
this.peer.off("connected", this.handleConnect, this);
this.peer.off("disconnected", this.handleDisconnect, this);
}
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
send(data: unknown): void {
if (typeof data === "string") {
this.peer.send(data);
} else throw new Error("Unsupported data type");
}
close(code?: number, reason?: string): void;
close(code?: number, reason?: string): void;
close(code?: unknown, reason?: unknown): void {
this.peer.hangup();
this.peer.off("message", this.handleMessage, this);
this.peer.off("connected", this.handleConnect, this);
this.peer.off("disconnected", this.handleDisconnect, this);
}
readonly CONNECTING = WebSocket.CONNECTING;
readonly OPEN = WebSocket.OPEN;
readonly CLOSING = WebSocket.CLOSING;
readonly CLOSED = WebSocket.CLOSED;
static readonly CONNECTING = WebSocket.CONNECTING;
static readonly OPEN = WebSocket.OPEN;
static readonly CLOSING = WebSocket.CLOSING;
static readonly CLOSED = WebSocket.CLOSED;
}
export default class WebRtcRelayClient extends AbstractRelay {
constructor(peer: NostrWebRTCPeer, opts: AbstractRelayConstructorOptions) {
super("wss://example.com", opts);
// @ts-expect-error
this.url = `webrtc+nostr:` + peer.answerEvent?.pubkey;
this.connectionTimeout = 30_000;
// @ts-expect-error
this._WebSocket = function () {
return new WebRtcWebSocket(peer);
};
}
}
if (import.meta.env.DEV) {
// @ts-expect-error
window.WebRtcWebSocket = WebRtcWebSocket;
// @ts-expect-error
window.WebRtcRelayClient = WebRtcRelayClient;
}

View File

@@ -0,0 +1,108 @@
import EventEmitter from "eventemitter3";
import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay, Subscription } from "nostr-tools/abstract-relay";
import NostrWebRTCPeer from "./nostr-webrtc-peer";
import { logger } from "../helpers/debug";
type EventMap = {
call: [NostrEvent];
};
export default class WebRtcRelayServer extends EventEmitter<EventMap> {
log = logger.extend("WebRtcRelayServer");
peer: NostrWebRTCPeer;
upstream: AbstractRelay;
// A map of subscriptions
subscriptions = new Map<string, Subscription>();
constructor(peer: NostrWebRTCPeer, upstream: AbstractRelay) {
super();
this.peer = peer;
this.upstream = upstream;
this.peer.on("message", this.handleMessage, this);
this.peer.on("disconnected", this.handleDisconnect, this);
}
private send(data: any[]) {
this.peer.send(JSON.stringify(data));
}
async handleMessage(message: string) {
let data;
try {
data = JSON.parse(message);
if (!Array.isArray(data)) throw new Error("Message is not an array");
// Pass the data to appropriate handler
switch (data[0]) {
case "REQ":
case "COUNT":
await this.handleSubscriptionMessage(data);
break;
case "EVENT":
await this.handleEventMessage(data);
break;
case "CLOSE":
await this.handleCloseMessage(data);
break;
}
} catch (err) {
this.log("Failed to handle message", message, err);
}
return data;
}
handleSubscriptionMessage(data: any[]) {
const [_, id, ...filters] = data as [string, string, ...Filter[]];
let sub = this.subscriptions.get(id);
if (sub) {
sub.filters = filters;
sub.fire();
} else {
sub = this.upstream.subscribe(filters, {
onevent: (event) => this.send(["EVENT", id, event]),
onclose: (reason) => this.send(["CLOSED", id, reason]),
oneose: () => this.send(["EOSE", id]),
});
}
}
handleCloseMessage(data: any[]) {
const [_, id] = data as [string, string, ...Filter[]];
let sub = this.subscriptions.get(id);
if (sub) {
sub.close();
this.subscriptions.delete(id);
}
}
async handleEventMessage(data: any[]) {
const [_, event] = data as [string, NostrEvent];
try {
const result = await this.upstream.publish(event);
this.peer.send(JSON.stringify(["OK", event.id, true, result]));
} catch (error) {
if (error instanceof Error) this.peer.send(JSON.stringify(["OK", event.id, false, error.message]));
}
}
handleDisconnect() {
for (const [id, sub] of this.subscriptions) sub.close();
this.subscriptions.clear();
}
destroy() {
this.peer.off("message", this.handleMessage, this);
this.peer.off("disconnected", this.handleDisconnect, this);
}
}

View File

@@ -17,7 +17,7 @@ export default function useThreadTimelineLoader(
const refs = focusedEvent && getThreadReferences(focusedEvent);
const rootPointer = refs?.root?.e || (focusedEvent && { id: focusedEvent?.id });
const readRelays = unique([...relays, ...(rootPointer?.relays ?? [])]);
const readRelays = useMemo(() => unique([...relays, ...(rootPointer?.relays ?? [])]), [relays, rootPointer?.relays]);
const timelineId = `${rootPointer?.id}-thread`;
const timeline = useTimelineLoader(

View File

@@ -1,3 +1,6 @@
import { generateSecretKey } from "nostr-tools";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { PersistentSubject } from "../classes/subject";
class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
@@ -49,25 +52,40 @@ class LocalStorageEntry<T = string> extends PersistentSubject<T> {
decode?: (raw: string) => T;
encode?: (value: T) => string | null;
constructor(key: string, fallback: T, decode?: (raw: string) => T, encode?: (value: T) => string | null) {
setDefault = false;
constructor(
key: string,
fallback: T,
decode?: (raw: string) => T,
encode?: (value: T) => string | null,
setDefault = false,
) {
let value = fallback;
if (localStorage.hasOwnProperty(key)) {
const raw = localStorage.getItem(key);
if (decode && raw) value = decode(raw);
else if (raw) value = raw as T;
} else if (setDefault) {
const encoded = encode ? encode(fallback) : String(fallback);
if (!encoded) throw new Error("encode can not return null when setDefault is set");
localStorage.setItem(key, encoded);
}
super(value);
this.key = key;
this.decode = decode;
this.encode = encode;
this.fallback = fallback;
this.setDefault = setDefault;
}
next(value: T) {
const encoded = this.encode ? this.encode(value) : String(value);
if (encoded !== null) localStorage.setItem(this.key, encoded);
else if (this.setDefault && encoded) localStorage.setItem(this.key, encoded);
else localStorage.removeItem(this.key);
super.next(value);
@@ -112,10 +130,27 @@ const enableNoteThreadDrawer = new LocalStorageEntry(
(v) => String(v),
);
// webrtc relay
const webRtcUseLocalIdentity = new LocalStorageEntry(
"nostr-webrtc-use-identity",
true,
(raw) => raw === "true",
(v) => String(v),
);
const webRtcLocalIdentity = new LocalStorageEntry(
"nostr-webrtc-identity",
generateSecretKey(),
(raw) => hexToBytes(raw),
(key) => bytesToHex(key),
true,
);
const localSettings = {
idbMaxEvents,
wasmPersistForDays,
enableNoteThreadDrawer,
webRtcUseLocalIdentity,
webRtcLocalIdentity,
};
if (import.meta.env.DEV) {

View File

@@ -0,0 +1,113 @@
import { NostrEvent, SimplePool } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { logger } from "../helpers/debug";
import NostrWebRtcBroker from "../classes/nostr-webrtc-broker";
import WebRtcRelayClient from "../classes/webrtc-relay-client";
import WebRtcRelayServer from "../classes/webrtc-relay-server";
import verifyEventMethod from "./verify-event";
import SimpleSigner from "../classes/simple-signer";
import { localRelay } from "./local-relay";
import localSettings from "./local-settings";
class WebRtcRelaysService {
log = logger.extend("NostrWebRtcBroker");
broker: NostrWebRtcBroker;
upstream: AbstractRelay | null;
approved: string[] = [];
calls: NostrEvent[] = [];
get answered() {
return this.calls.filter((event) => this.broker.peers.has(event.pubkey));
}
get unanswered() {
return this.calls.filter((event) => this.broker.peers.has(event.pubkey) === false);
}
clients = new Map<string, WebRtcRelayClient>();
servers = new Map<string, WebRtcRelayServer>();
get relays() {
return Array.from(this.clients.values());
}
constructor(broker: NostrWebRtcBroker, upstream: AbstractRelay | null) {
this.upstream = upstream;
this.broker = broker;
}
async handleCall(event: NostrEvent) {
if (!this.calls.includes(event)) {
this.log(`Received request from ${event.pubkey}`);
this.calls.push(event);
}
if (this.approved.includes(event.pubkey)) {
this.log(`Answering call from ${event.pubkey}`);
const peer = await this.broker.answerCall(event);
if (!peer.peer) return;
if (this.upstream) {
const server = new WebRtcRelayServer(peer, this.upstream);
this.servers.set(peer.peer, server);
}
const client = new WebRtcRelayClient(peer, {
websocketImplementation: WebSocket,
verifyEvent: verifyEventMethod,
});
this.clients.set(peer.peer, client);
}
}
async acceptCall(event: NostrEvent) {
this.log(`Accepting connection from ${event.pubkey}`);
this.approved.push(event.pubkey);
await this.handleCall(event);
}
async connect(uri: string) {
this.log(`Connecting to ${uri}`);
const peer = await this.broker.requestConnection(uri);
if (!peer.peer) return;
if (this.upstream) {
const server = new WebRtcRelayServer(peer, this.upstream);
this.servers.set(peer.peer, server);
}
const client = new WebRtcRelayClient(peer, {
websocketImplementation: WebSocket,
verifyEvent: verifyEventMethod,
});
this.clients.set(peer.peer, client);
await client.connect();
}
start() {
this.broker.listenForCalls();
this.broker.on("call", this.handleCall, this);
}
stop() {
this.broker.stopListening();
this.broker.off("call", this.handleCall, this);
}
}
const signer = new SimpleSigner(localSettings.webRtcLocalIdentity.value);
const webRtcRelaysService = new WebRtcRelaysService(
new NostrWebRtcBroker(signer, new SimplePool(), ["wss://nos.lol"]),
localRelay as AbstractRelay | null,
);
webRtcRelaysService.start();
// if (import.meta.env.DEV) {
// @ts-expect-error
window.webRtcRelaysService = webRtcRelaysService;
// }
export default webRtcRelaysService;

View File

@@ -91,15 +91,19 @@ export default function RelaysView() {
NIP-05 Relays
</Button>
)}
<Button
variant="outline"
as={RouterLink}
to="/relays/contacts"
leftIcon={<UserSquare boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/contacts") ? "primary" : undefined}
>
Contact List Relays
</Button>
{account && (
<>
<Button
variant="outline"
as={RouterLink}
to="/relays/contacts"
leftIcon={<UserSquare boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/contacts") ? "primary" : undefined}
>
Contact List Relays
</Button>
</>
)}
{/* {account && (
<>
<Heading size="sm" mt="2">

View File

@@ -1,30 +1,122 @@
import { Button, ButtonGroup, Code, Flex, Heading, Link, Text } from "@chakra-ui/react";
import { useEffect, useMemo, useState } from "react";
import {
Button,
ButtonGroup,
Flex,
FormControl,
FormLabel,
Heading,
Input,
useForceUpdate,
useInterval,
} from "@chakra-ui/react";
import { getPublicKey, nip19 } from "nostr-tools";
import BackButton from "../../../components/router/back-button";
import useCurrentAccount from "../../../hooks/use-current-account";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
import { Link as RouterLink } from "react-router-dom";
import { RelayFavicon } from "../../../components/relay-favicon";
import { QrCodeIcon } from "../../../components/icons";
import webRtcRelaysService from "../../../services/webrtc-relays";
import useSubject from "../../../hooks/use-subject";
import localSettings from "../../../services/local-settings";
import { CopyIconButton } from "../../../components/copy-icon-button";
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import "./connect";
function WebRtcRelaysPage() {
const update = useForceUpdate();
const identity = useSubject(localSettings.webRtcLocalIdentity);
const pubkey = useMemo(() => getPublicKey(identity), [identity]);
const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]);
const uri = "webrtc+nostr:" + npub;
const [connectURI, setConnectURI] = useState("");
useEffect(() => {
webRtcRelaysService.broker.on("call", update);
return () => {
webRtcRelaysService.broker.off("call", update);
};
}, [update]);
useInterval(update, 1000);
export default function WebRtcRelaysView() {
return (
<Flex gap="2" direction="column" overflow="auto hidden" flex={1} px={{ base: "2", lg: 0 }}>
<Flex gap="2" alignItems="center" wrap="wrap">
<BackButton hideFrom="lg" size="sm" />
<Heading size="lg">WebRTC Relays</Heading>
<ButtonGroup size="sm" ml="auto">
<Button leftIcon={<QrCodeIcon />}>Pair</Button>
<Button colorScheme="primary">Connect</Button>
</ButtonGroup>
</Flex>
{/* <Text fontStyle="italic" mt="-2">
These relays cant be modified by noStrudel, they must be set manually on your
</Text> */}
<FormControl>
<FormLabel>WebRTC Connection URI</FormLabel>
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={pubkey} size="sm" />
<Input readOnly userSelect="all" value={uri} />
<CopyIconButton value={uri} aria-label="Copy Npub" />
</Flex>
</FormControl>
<Heading size="md">Connect:</Heading>
<Flex gap="2">
<Input placeholder="webrtc+nostr:npub1..." value={connectURI} onChange={(e) => setConnectURI(e.target.value)} />
<QRCodeScannerButton onData={(data) => setConnectURI(data)} />
<Button
colorScheme="primary"
onClick={() => {
webRtcRelaysService.connect(connectURI);
setConnectURI("");
}}
>
Connect
</Button>
</Flex>
{webRtcRelaysService.answered.length > 0 && (
<>
<Heading size="md">Connections:</Heading>
{webRtcRelaysService.answered.map((event) => (
<Flex key={event.id} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserName pubkey={event.pubkey} />
<Button size="sm" ml="auto" colorScheme="red">
Close
</Button>
</Flex>
))}
</>
)}
{webRtcRelaysService.unanswered.length > 0 && (
<>
<Heading size="md">Connection Requests:</Heading>
{webRtcRelaysService.unanswered.map((event) => (
<Flex key={event.id} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserName pubkey={event.pubkey} />
<Button
size="sm"
ml="auto"
colorScheme="green"
onClick={() => {
webRtcRelaysService.acceptCall(event);
update();
}}
>
Accept
</Button>
</Flex>
))}
</>
)}
</Flex>
);
}
export default function WebRtcRelaysView() {
if (webRtcRelaysService) {
return <WebRtcRelaysPage />;
}
return <Heading>WebRTC Relays don't work without</Heading>;
}