add webrtc test

This commit is contained in:
hzrd149 2024-07-20 16:57:58 -05:00
parent 82c9f93a4f
commit cfef0cc8b5
57 changed files with 496 additions and 86 deletions

View File

@ -68,7 +68,7 @@
"nanoid": "^5.0.4",
"ngeohash": "^0.6.3",
"nostr-idb": "^2.1.4",
"nostr-tools": "2.5.2",
"nostr-tools": "^2.7.1",
"nostr-wasm": "^0.1.0",
"prettier": "^3.2.5",
"react": "^18.2.0",
@ -117,7 +117,8 @@
"@types/zen-observable": "^0.8.7",
"@vitejs/plugin-react": "^4.2.1",
"camelcase": "^8.0.0",
"typescript": "^5.3.3",
"eventemitter3": "^5.0.1",
"typescript": "^5.5.3",
"vite": "^5.2.10",
"vite-plugin-pwa": "^0.19.8",
"workbox-build": "^7.0.0",

View File

@ -76,6 +76,7 @@ import MailboxesView from "./views/relays/mailboxes";
import MediaServersView from "./views/relays/media-servers";
import NIP05RelaysView from "./views/relays/nip05";
import ContactListRelaysView from "./views/relays/contact-list";
import WebRtcRelaysView from "./views/relays/webrtc";
import UserDMsTab from "./views/user/dms";
import LoginNostrConnectView from "./views/signin/nostr-connect";
import ThreadsNotificationsView from "./views/notifications/threads";
@ -291,6 +292,7 @@ const router = createHashRouter([
{ path: "media-servers", element: <MediaServersView /> },
{ path: "nip05", element: <NIP05RelaysView /> },
{ path: "contacts", element: <ContactListRelaysView /> },
{ path: "webrtc", element: <WebRtcRelaysView /> },
{ path: "sets", element: <BrowseRelaySetsView /> },
{ path: ":id", element: <RelaySetView /> },
],

View File

@ -1,4 +1,5 @@
import { NostrEvent, AbstractRelay } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";

View File

@ -1,4 +1,5 @@
import { NostrEvent, AbstractRelay } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import { getEventUID } from "nostr-idb";

View File

@ -1,4 +1,5 @@
import { Filter, NostrEvent, AbstractRelay } from "nostr-tools";
import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";

View File

@ -1,4 +1,5 @@
import { NostrEvent, AbstractRelay } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";

View File

@ -1,5 +1,6 @@
import { Debugger } from "debug";
import { AbstractRelay, Filter, NostrEvent, matchFilters } from "nostr-tools";
import { Filter, NostrEvent, matchFilters } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { SimpleRelay } from "nostr-idb";
import _throttle from "lodash.throttle";
import { nanoid } from "nanoid";

View File

@ -1,10 +1,11 @@
import { nanoid } from "nanoid";
import { Filter } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { NostrEvent } from "../types/nostr-event";
import relayPoolService from "../services/relay-pool";
import { isFilterEqual } from "../helpers/nostr/filter";
import ControlledObservable from "./controlled-observable";
import { AbstractRelay, Filter } from "nostr-tools";
import { offlineMode } from "../services/offline-mode";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";

View File

@ -1,5 +1,6 @@
import { nanoid } from "nanoid";
import { NostrEvent, AbstractRelay } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import relayPoolService from "../services/relay-pool";
import createDefer from "./deferred";

View File

@ -1,5 +1,6 @@
import { nanoid } from "nanoid";
import { Filter, NostrEvent, Relay, Subscription } from "nostr-tools";
import { Filter, NostrEvent, Relay } from "nostr-tools";
import { Subscription } from "nostr-tools/abstract-relay";
import relayPoolService from "../services/relay-pool";
import ControlledObservable from "./controlled-observable";

View File

@ -1,5 +1,6 @@
import { nanoid } from "nanoid";
import { AbstractRelay, Filter, Relay, Subscription, SubscriptionParams } from "nostr-tools";
import { Filter, Relay } from "nostr-tools";
import { AbstractRelay, Subscription, SubscriptionParams } from "nostr-tools/abstract-relay";
import relayPoolService from "../services/relay-pool";
import Process from "./process";

View File

@ -1,6 +1,6 @@
import { ComponentWithAs, IconProps } from "@chakra-ui/react";
import { SimpleRelay } from "nostr-idb";
import { AbstractRelay } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
let lastId = 0;
export default class Process {

View File

@ -1,4 +1,4 @@
import { AbstractRelay } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import dayjs from "dayjs";
import { logger } from "../helpers/debug";

View File

@ -1,6 +1,7 @@
import dayjs from "dayjs";
import { Debugger } from "debug";
import { AbstractRelay, Filter, NostrEvent } from "nostr-tools";
import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import MultiSubscription from "./multi-subscription";

View File

@ -1,5 +1,5 @@
import { Suspense, lazy } from "react";
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
import type { DecodeResult } from "nostr-tools/nip19";
import { CardProps, Spinner } from "@chakra-ui/react";
import { kinds } from "nostr-tools";

View File

@ -14,7 +14,7 @@ import {
} from "@chakra-ui/react";
import { EventTemplate, NostrEvent, kinds } from "nostr-tools";
import dayjs from "dayjs";
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
import type { AddressPointer } from "nostr-tools/nip19";
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../../icons";
import relayHintService from "../../../../services/event-relay-hint";

View File

@ -4,7 +4,7 @@ import { Select, SelectProps } from "@chakra-ui/react";
import useUserCommunitiesList from "../../hooks/use-user-communities-list";
import useCurrentAccount from "../../hooks/use-current-account";
import { getCommunityName } from "../../helpers/nostr/communities";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { getEventCoordinate } from "../../helpers/nostr/event";

View File

@ -1,14 +1,6 @@
import { useCallback, useState } from "react";
import {
Button,
ButtonProps,
IconButton,
IconButtonProps,
useForceUpdate,
useInterval,
useToast,
} from "@chakra-ui/react";
import { AbstractRelay } from "nostr-tools";
import { IconButton, IconButtonProps, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
import { type AbstractRelay } from "nostr-tools/abstract-relay";
import relayPoolService from "../../services/relay-pool";
import { useSigningContext } from "../../providers/global/signing-provider";

View File

@ -1,6 +1,6 @@
import { ChangeEventHandler } from "react";
import { Switch, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
import { AbstractRelay } from "nostr-tools";
import { type AbstractRelay } from "nostr-tools/abstract-relay";
import relayPoolService from "../../services/relay-pool";
import useSubject from "../../hooks/use-subject";

View File

@ -1,8 +1,8 @@
import { Badge, useForceUpdate } from "@chakra-ui/react";
import { useInterval } from "react-use";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import relayPoolService from "../../services/relay-pool";
import { AbstractRelay } from "nostr-tools";
import useSubject from "../../hooks/use-subject";
const getStatusText = (relay: AbstractRelay, connecting = false) => {

View File

@ -5,7 +5,7 @@ import { nanoid } from "nanoid";
import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, isPTag, NostrEvent, Tag } from "../../types/nostr-event";
import { getMatchNostrLink } from "../regexp";
import { AddressPointer, DecodeResult, EventPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer, DecodeResult, EventPointer } from "nostr-tools/nip19";
import { safeJson } from "../parse";
import { safeDecode } from "../nip19";
import { safeRelayUrl, safeRelayUrls } from "../relay";

View File

@ -1,6 +1,6 @@
import dayjs from "dayjs";
import { NostrEvent, isRTag } from "../../types/nostr-event";
import { DecodeResult } from "nostr-tools/lib/types/nip19";
import { DecodeResult } from "nostr-tools/nip19";
import { getPointerFromTag } from "../nip19";
export const GOAL_KIND = 9041;

View File

@ -1,4 +1,6 @@
import { AbstractRelay, Filter, SubCloser, SubscribeManyParams, Subscription } from "nostr-tools";
import { Filter } from "nostr-tools";
import { SubCloser, SubscribeManyParams } from "nostr-tools/abstract-pool";
import { AbstractRelay, Subscription } from "nostr-tools/abstract-relay";
// NOTE: only use this for equality checks and querying
export function getRelayVariations(relay: string) {

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
import type { AddressPointer } from "nostr-tools/nip19";
import useReplaceableEvent from "./use-replaceable-event";
import { parseDVMMetadata } from "../helpers/nostr/dvm";

View File

@ -1,6 +1,6 @@
import { useParams } from "react-router-dom";
import { nip19 } from "nostr-tools";
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
import type { AddressPointer } from "nostr-tools/nip19";
import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/event";

View File

@ -1,6 +1,6 @@
import { useParams } from "react-router-dom";
import { nip19 } from "nostr-tools";
import type { EventPointer } from "nostr-tools/lib/types/nip19";
import type { EventPointer } from "nostr-tools/nip19";
import { isHexKey } from "../helpers/nip19";

View File

@ -1,6 +1,6 @@
import { useParams } from "react-router-dom";
import { nip19 } from "nostr-tools";
import type { ProfilePointer } from "nostr-tools/lib/types/nip19";
import type { ProfilePointer } from "nostr-tools/nip19";
import { isHexKey } from "../helpers/nip19";
export default function useParamsProfilePointer(key: string = "pubkey"): ProfilePointer {

View File

@ -1,4 +1,5 @@
import { AbstractRelay, NostrEvent } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { WIKI_PAGE_KIND } from "../helpers/nostr/wiki";
import { logger } from "../helpers/debug";

View File

@ -1,5 +1,6 @@
import { AbstractRelay, kinds } from "nostr-tools";
import { kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";

View File

@ -1,5 +1,5 @@
import { nip19 } from "nostr-tools";
import type { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import type { AddressPointer, EventPointer } from "nostr-tools/nip19";
import { NostrEvent, isDTag } from "../types/nostr-event";
import relayScoreboardService from "./relay-scoreboard";

View File

@ -1,5 +1,6 @@
import { AbstractRelay, kinds } from "nostr-tools";
import { kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";

View File

@ -1,5 +1,6 @@
import { CacheRelay, openDB } from "nostr-idb";
import { AbstractRelay } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { logger } from "../helpers/debug";
import { safeRelayUrl } from "../helpers/relay";
import WasmRelay from "./wasm-relay";

View File

@ -1,4 +1,4 @@
import { AbstractRelay } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import Process from "../classes/process";
import relayPoolService from "./relay-pool";

View File

@ -1,4 +1,5 @@
import { AbstractRelay, NostrEvent } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import SuperMap from "../classes/super-map";

View File

@ -1,7 +1,7 @@
import { nip04, getPublicKey, finalizeEvent } from "nostr-tools";
import { nip04, getPublicKey, finalizeEvent, EventTemplate } from "nostr-tools";
import { hexToBytes } from "@noble/hashes/utils";
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
import { NostrEvent } from "../types/nostr-event";
import { Account } from "./account";
import db from "./db";
import serialPortService from "./serial-port";
@ -96,7 +96,7 @@ class SigningService {
return await p;
}
async requestSignature(draft: DraftNostrEvent, account: Account) {
async requestSignature(draft: EventTemplate, account: Account) {
const checkSig = (signed: NostrEvent) => {
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey");
};

View File

@ -1,4 +1,5 @@
import _throttle from "lodash.throttle";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
@ -7,7 +8,6 @@ import { logger } from "../helpers/debug";
import Subject from "../classes/subject";
import relayPoolService from "./relay-pool";
import Process from "../classes/process";
import { AbstractRelay } from "nostr-tools";
import processManager from "./process-manager";
import Code02 from "../components/icons/code-02";
import BatchEventLoader from "../classes/batch-event-loader";

View File

@ -1,15 +1,19 @@
import { DraftNostrEvent, NostrEvent } from "./nostr-event";
import { EventTemplate, NostrEvent, UnsignedEvent } from "nostr-tools";
declare global {
interface Window {
nostr?: {
getPublicKey: () => Promise<string> | string;
signEvent: (event: DraftNostrEvent) => Promise<NostrEvent> | NostrEvent;
signEvent: (event: EventTemplate) => Promise<NostrEvent> | NostrEvent;
getRelays?: () => Record<string, { read: boolean; write: boolean }> | string[];
nip04?: {
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
};
nip44?: {
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
};
};
}
}

View File

@ -1,3 +0,0 @@
declare module "nostr-tools/wasm" {
export * from "nostr-tools/lib/types/wasm.d.ts";
}

View File

@ -1,4 +1,4 @@
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
import { Button, ButtonGroup, Flex, Heading, SkeletonText, Spinner } from "@chakra-ui/react";
import { useParams } from "react-router-dom";

View File

@ -1,6 +1,6 @@
import { Link as RouterLink } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { EventPointer } from "nostr-tools/lib/types/nip19";
import { EventPointer } from "nostr-tools/nip19";
import {
Box,
Card,

View File

@ -23,7 +23,7 @@ import useCountCommunityMembers from "../../../hooks/use-count-community-members
import { readablizeSats } from "../../../helpers/bolt11";
import User01 from "../../../components/icons/user-01";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) {

View File

@ -15,7 +15,7 @@ import { useNavigate } from "react-router-dom";
import { ChevronLeftIcon } from "../../components/icons";
import { parseCoordinate } from "../../helpers/nostr/event";
import UserAvatarLink from "../../components/user/user-avatar-link";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays?: Iterable<string>) {
const readRelays = useReadRelays(additionalRelays);

View File

@ -5,7 +5,7 @@ import { Box, BoxProps } from "@chakra-ui/react";
import useUserMetadata from "../../../hooks/use-user-metadata";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
type DVMAvatarProps = {

View File

@ -1,7 +1,7 @@
import { Card, CardProps, Heading, LinkBox, LinkOverlayProps, Text } from "@chakra-ui/react";
import { Link as RouterLink, To } from "react-router-dom";
import { useMemo } from "react";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import HoverLinkOverlay from "../../../components/hover-link-overlay";

View File

@ -4,7 +4,7 @@ import { nip19 } from "nostr-tools";
import useUserMetadata from "../../../hooks/use-user-metadata";
import { getDisplayName } from "../../../helpers/nostr/user-metadata";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
export function DVMName({

View File

@ -1,4 +1,4 @@
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
import { Select } from "@chakra-ui/react";

View File

@ -26,7 +26,7 @@ import { DraftNostrEvent } from "../../../types/nostr-event";
import { useReadRelays } from "../../../hooks/use-client-relays";
import { DVMAvatarLink } from "./dvm-avatar";
import DVMLink from "./dvm-name";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
import { usePublishEvent } from "../../../providers/global/publish-provider";

View File

@ -1,6 +1,6 @@
import { ChainedDVMJob, getEventIdsFromJobs } from "../../../helpers/nostr/dvm";
import FeedStatus from "./feed-status";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useSingleEvents from "../../../hooks/use-single-events";
import TimelineItem from "../../../components/timeline-page/generic-note-timeline/timeline-item";

View File

@ -33,7 +33,7 @@ import RequireCurrentAccount from "../../providers/route/require-current-account
import { CodeIcon } from "../../components/icons";
import DebugChains from "./components/debug-chains";
import Feed from "./components/feed";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { AddressPointer } from "nostr-tools/nip19";
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
import DVMParams from "./components/dvm-params";
import useUserMailboxes from "../../hooks/use-user-mailboxes";

View File

@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom";
import { kinds, nip19 } from "nostr-tools";
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
import type { DecodeResult } from "nostr-tools/nip19";
import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react";
import UserLink from "../../../components/user/user-link";

View File

@ -12,6 +12,7 @@ import { useUserDNSIdentity } from "../../hooks/use-user-dns-identity";
import useUserContactRelays from "../../hooks/use-user-contact-relays";
import UserSquare from "../../components/icons/user-square";
import Image01 from "../../components/icons/image-01";
import Server05 from "../../components/icons/server-05";
export default function RelaysView() {
const account = useCurrentAccount();
@ -69,6 +70,15 @@ export default function RelaysView() {
</Button>
</>
)}
{/* <Button
variant="outline"
as={RouterLink}
to="/relays/webrtc"
leftIcon={<Server05 boxSize={6} />}
colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
>
WebRTC Relays
</Button> */}
{nip05?.exists && (
<Button
variant="outline"

View File

@ -0,0 +1,356 @@
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 { SubCloser, SubscribeManyParams } from "nostr-tools/abstract-pool";
import { logger } from "../../../helpers/debug";
const RTCDescriptionEventKind = 25050;
const RTCICEEventKind = 25051;
type Signer = {
getPublicKey: () => Promise<string> | string;
signEvent: (event: EventTemplate) => Promise<NostrEvent> | NostrEvent;
nip44: {
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
};
};
type Pool = {
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser;
publish(relays: string[], event: NostrEvent): Promise<string>[];
};
type EventMap = {
connect: [];
disconnect: [];
incomingCall: [NostrEvent];
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> {
log: Debugger;
signer: Signer;
pool: Pool;
peer?: string;
relays: string[] = [];
iceServers: RTCIceServer[] = [];
connection?: RTCPeerConnection;
channel?: RTCDataChannel;
listening = false;
subscription?: SubCloser;
async isCaller() {
if (!this.offerEvent) return null;
return (await this.signer.getPublicKey()) === this.offerEvent?.pubkey;
}
get offer() {
return this.connection?.localDescription;
}
offerEvent?: NostrEvent;
get answer() {
return this.connection?.remoteDescription;
}
answerEvent?: NostrEvent;
private candidateQueue: RTCIceCandidateInit[] = [];
constructor(signer: Signer, pool: Pool = defaultPool, relays?: string[], iceServers?: RTCIceServer[]) {
super();
this.log = logger.extend(`webrtc`);
this.signer = signer;
this.pool = pool;
if (iceServers) this.iceServers = iceServers;
if (relays) this.relays = relays;
}
private createConnection() {
if (this.connection) return this.connection;
this.connection = new RTCPeerConnection({ iceServers: this.iceServers });
this.log("Created local connection");
this.connection.onicecandidate = async ({ candidate }) => {
if (candidate) {
this.candidateQueue.push(candidate.toJSON());
} else this.flushCandidateQueue();
};
this.connection.onicegatheringstatechange = this.flushCandidateQueue.bind(this);
this.connection.ondatachannel = ({ channel }) => {
this.log("Got data channel", channel);
if (channel.label !== "nostr") return;
this.channel = channel;
this.channel.onclose = this.onChannelStateChange.bind(this);
this.channel.onopen = this.onChannelStateChange.bind(this);
this.channel.onmessage = this.handleChannelMessage.bind(this);
};
return this.connection;
}
private async flushCandidateQueue() {
if (this.connection?.iceGatheringState !== "complete") return;
if (this.offerEvent && this.answerEvent && this.peer && this.candidateQueue.length > 0) {
const cipherText = await this.signer.nip44.encrypt(this.peer, JSON.stringify(this.candidateQueue));
const iceEvent = await this.signer.signEvent({
kind: RTCICEEventKind,
content: cipherText,
tags: [["e", this.offerEvent.id]],
created_at: dayjs().unix(),
});
this.log(`Publishing ICE candidates`, this.candidateQueue);
await this.pool.publish(this.relays, iceEvent);
this.candidateQueue = [];
}
}
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 });
this.channel.onopen = this.onChannelStateChange.bind(this);
this.channel.onclose = this.onChannelStateChange.bind(this);
this.channel.onmessage = this.handleChannelMessage.bind(this);
this.log(`Making call to ${peer} `);
const offer = await pc.createOffer();
const cipherText = await this.signer.nip44.encrypt(peer, JSON.stringify(offer));
const offerEvent = await this.signer.signEvent({
kind: RTCDescriptionEventKind,
content: cipherText,
tags: [["p", peer], ...this.relays.map((r) => ["relay", r])],
created_at: dayjs().unix(),
});
this.log("Created offer", offer);
// listen for answers and ice events
this.subscription = this.pool.subscribeMany(
this.relays,
[
{
kinds: [RTCDescriptionEventKind, RTCICEEventKind],
"#e": [offerEvent.id],
authors: [peer],
},
],
{
onevent: async (event: NostrEvent) => {
if (!this.offerEvent) return;
if (!event.tags.some((t) => t[0] === "e" && t[1] === this.offerEvent?.id)) return;
console.log(event);
switch (event.kind) {
case RTCDescriptionEventKind:
await this.handleAnswer(event);
// got answer, send ICE candidates
await this.flushCandidateQueue();
break;
case RTCICEEventKind:
await this.handleICEEvent(event);
break;
}
},
onclose: () => {
this.log("Subscription Closed");
},
},
);
this.peer = peer;
this.log("Publishing event", offerEvent);
await this.pool.publish(this.relays, offerEvent);
await pc.setLocalDescription(offer);
this.offerEvent = offerEvent;
}
async handleAnswer(event: NostrEvent) {
const pc = this.createConnection();
if (!pc.localDescription) throw new Error("Got answer without offering");
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
const answer = JSON.parse(plaintext) as RTCSessionDescriptionInit;
if (answer.type !== "answer") throw new Error("Unexpected rtc description type");
this.log("Got answer", answer);
await pc.setRemoteDescription(answer);
this.answerEvent = event;
}
async answerCall(event: NostrEvent) {
this.stopListening();
const pc = this.createConnection();
this.log(`Answering call ${event.id} from ${event.pubkey}`);
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
const offer = JSON.parse(plaintext) as RTCSessionDescriptionInit;
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]);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
const cipherText = await this.signer.nip44.encrypt(event.pubkey, JSON.stringify(answer));
const answerEvent = await this.signer.signEvent({
kind: RTCDescriptionEventKind,
content: cipherText,
tags: [
["p", event.pubkey],
["e", event.id],
],
created_at: dayjs().unix(),
});
this.log("Created answer", answer);
this.peer = event.pubkey;
this.offerEvent = event;
// listen for ice events
this.subscription = this.pool.subscribeMany(
this.relays,
[{ kinds: [RTCICEEventKind], "#e": [event.id], authors: [event.pubkey] }],
{
onevent: async (event) => {
if (!this.offerEvent) return;
if (!event.tags.some((t) => t[0] === "e" && t[1] === this.offerEvent?.id)) return;
switch (event.kind) {
case RTCICEEventKind:
await this.handleICEEvent(event);
break;
}
},
onclose: () => {
this.log("Subscription Closed");
},
},
);
this.log("Publishing event", answerEvent);
await this.pool.publish(this.relays, answerEvent);
await pc.setLocalDescription(answer);
this.answerEvent = answerEvent;
// answered call, send ICE candidates
await this.flushCandidateQueue();
}
private async handleICEEvent(event: NostrEvent) {
if (!this.connection) throw new Error("Got ICE event without connection");
const pc = this.createConnection();
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
const candidates = JSON.parse(plaintext) as RTCIceCandidateInit[];
this.log("Got candidates", 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);
}
private handleChannelMessage(event: MessageEvent<any>) {
if (typeof event.data === "string") this.emit("message", event.data);
}
send(message: string) {
this.channel?.send(message);
}
disconnect() {
this.log("Closing data channel");
if (this.channel) this.channel.close();
if (this.connection) this.connection.close();
}
}
// @ts-expect-error
window.SimpleSigner = SimpleSigner;
// @ts-expect-error
window.WebRTCPeer = WebRTCPeer;

View File

@ -0,0 +1,30 @@
import { Button, ButtonGroup, Code, Flex, Heading, Link, Text } from "@chakra-ui/react";
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 "./connect";
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> */}
</Flex>
);
}

View File

@ -18,8 +18,8 @@ import {
useInterval,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { AbstractRelay } from "nostr-tools";
import { useLocalStorage } from "react-use";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import relayPoolService from "../../../services/relay-pool";
import { RelayFavicon } from "../../../components/relay-favicon";

View File

@ -15,7 +15,8 @@ import {
Text,
useDisclosure,
} from "@chakra-ui/react";
import { AbstractRelay, NostrEvent, Subscription } from "nostr-tools";
import { NostrEvent } from "nostr-tools";
import { AbstractRelay, Subscription } from "nostr-tools/abstract-relay";
import { useLocalStorage } from "react-use";
import { Subscription as IDBSubscription } from "nostr-idb";
import _throttle from "lodash.throttle";
@ -93,7 +94,7 @@ export default function EventConsoleView() {
if (!relay || relay.url !== url.toString()) {
r = await relayPoolService.requestRelay(url);
await relayPoolService.requestConnect(r);
setRelay(r);
setRelay(r as AbstractRelay);
} else r = relay;
} else {
if (relay) setRelay(null);

View File

@ -10,7 +10,7 @@
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "node",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

View File

@ -6218,20 +6218,6 @@ nostr-idb@^2.1.4:
idb "^8.0.0"
nostr-tools "^2.1.3"
nostr-tools@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.2.tgz#54b445380ac2a7740ad90ed3b044bca93ecf23bd"
integrity sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-tools@^1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.17.0.tgz#b6f62e32fedfd9e68ec0a7ce57f74c44fc768e8c"
@ -6272,6 +6258,20 @@ nostr-tools@^2.3.2:
optionalDependencies:
nostr-wasm v0.1.0
nostr-tools@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.7.1.tgz#cfedfe6c7ebf7f127f3ac32a5b57c7e570c35f67"
integrity sha512-4qAvlHSqBAA8lQMwRWE6dalSNdQT77Xut9lPiJZgEcb9RAlR69wR2+KVBAgnZVaabVYH7FJ7gOQXLw/jQBAYBg==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-wasm@^0.1.0, nostr-wasm@v0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
@ -7773,10 +7773,10 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typescript@^5.3.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
typescript@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
typo-js@*:
version "1.2.4"