diff --git a/src/Feed/EventPublisher.ts b/src/Feed/EventPublisher.ts index 3c8cbd8c..29bb6deb 100644 --- a/src/Feed/EventPublisher.ts +++ b/src/Feed/EventPublisher.ts @@ -77,6 +77,16 @@ export default function useEventPublisher() { } return { + nip42Auth: async (challenge: string, relay:string) => { + if(pubKey) { + const ev = NEvent.ForPubKey(pubKey); + ev.Kind = EventKind.Auth; + ev.Content = ""; + ev.Tags.push(new Tag(["relay", relay], 0)); + ev.Tags.push(new Tag(["challenge", challenge], 1)); + return await signEvent(ev); + } + }, broadcast: (ev: NEvent | undefined) => { if (ev) { console.debug("Sending event: ", ev); diff --git a/src/Nostr/Connection.ts b/src/Nostr/Connection.ts index bd92816a..060fc934 100644 --- a/src/Nostr/Connection.ts +++ b/src/Nostr/Connection.ts @@ -8,6 +8,7 @@ import { ConnectionStats } from "Nostr/ConnectionStats"; import { RawEvent, TaggedRawEvent, u256 } from "Nostr"; import { RelayInfo } from "./RelayInfo"; import Nips from "./Nips"; +import { System } from "./System"; export type CustomHook = (state: Readonly) => void; @@ -50,7 +51,9 @@ export default class Connection { LastState: Readonly; IsClosed: boolean; ReconnectTimer: ReturnType | null; - EventsCallback: Map void>; + EventsCallback: Map void>; + AwaitingAuth: Map; + Authed: boolean; constructor(addr: string, options: RelaySettings) { this.Id = uuid(); @@ -76,6 +79,8 @@ export default class Connection { this.IsClosed = false; this.ReconnectTimer = null; this.EventsCallback = new Map(); + this.AwaitingAuth = new Map(); + this.Authed = false; this.Connect(); } @@ -122,18 +127,8 @@ export default class Connection { OnOpen(e: Event) { this.ConnectTimeout = DefaultConnectTimeout; + this._InitSubscriptions(); console.log(`[${this.Address}] Open!`); - - // send pending - for (let p of this.Pending) { - this._SendJson(p); - } - this.Pending = []; - - for (let [_, s] of this.Subscriptions) { - this._SendSubscription(s); - } - this._UpdateState(); } OnClose(e: CloseEvent) { @@ -156,6 +151,12 @@ export default class Connection { let msg = JSON.parse(e.data); let tag = msg[0]; switch (tag) { + case "AUTH": { + this._OnAuthAsync(msg[1]) + this.Stats.EventsReceived++; + this._UpdateState(); + break; + } case "EVENT": { this._OnEvent(msg[1], msg[2]); this.Stats.EventsReceived++; @@ -173,7 +174,7 @@ export default class Connection { if (this.EventsCallback.has(id)) { let cb = this.EventsCallback.get(id)!; this.EventsCallback.delete(id); - cb(); + cb(msg); } break; } @@ -314,7 +315,25 @@ export default class Connection { } } + _InitSubscriptions() { + // send pending + for (let p of this.Pending) { + this._SendJson(p); + } + this.Pending = []; + + for (let [_, s] of this.Subscriptions) { + this._SendSubscription(s); + } + this._UpdateState(); + } + _SendSubscription(sub: Subscriptions) { + if(!this.Authed && this.AwaitingAuth.size > 0) { + this.Pending.push(sub); + return; + } + let req = ["REQ", sub.Id, sub.ToObject()]; if (sub.OrSubs.length > 0) { req = [ @@ -349,6 +368,40 @@ export default class Connection { } } + async _OnAuthAsync(challenge: string): Promise { + const authCleanup = () => { + this.AwaitingAuth.delete(challenge) + } + this.AwaitingAuth.set(challenge, true) + const authEvent = await System.nip42Auth(challenge, this.Address) + return new Promise((resolve,_) => { + if(!authEvent) { + authCleanup(); + return Promise.reject('no event'); + } + + let t = setTimeout(() => { + authCleanup(); + resolve(); + }, 10_000); + + this.EventsCallback.set(authEvent.Id, (msg:any[]) => { + clearTimeout(t); + authCleanup(); + if(msg.length > 3 && msg[2] === true) { + this.Authed = true; + this._InitSubscriptions(); + } + resolve(); + }); + + let req = ["AUTH", authEvent.ToObject()]; + this._SendJson(req); + this.Stats.EventsSent++; + this._UpdateState(); + }) + } + _OnEnd(subId: string) { let sub = this.Subscriptions.get(subId); if (sub) { @@ -392,4 +445,4 @@ export default class Connection { } return ev; } -} \ No newline at end of file +} diff --git a/src/Nostr/EventKind.ts b/src/Nostr/EventKind.ts index d12012b9..5b4cfbdb 100644 --- a/src/Nostr/EventKind.ts +++ b/src/Nostr/EventKind.ts @@ -7,7 +7,8 @@ const enum EventKind { DirectMessage = 4, // NIP-04 Deletion = 5, // NIP-09 Repost = 6, // NIP-18 - Reaction = 7 // NIP-25 + Reaction = 7, // NIP-25 + Auth = 22242 // NIP-42 }; export default EventKind; \ No newline at end of file diff --git a/src/Nostr/System.ts b/src/Nostr/System.ts index a51d2528..7c1fcb4c 100644 --- a/src/Nostr/System.ts +++ b/src/Nostr/System.ts @@ -212,6 +212,10 @@ export class NostrSystem { setTimeout(() => this._FetchMetadata(), 500); } + + async nip42Auth(challenge: string, relay:string): Promise { + return + } } export const System = new NostrSystem(); diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index b173c94a..a7e2cfaa 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -1,12 +1,12 @@ import "./Layout.css"; -import { useEffect, useState } from "react" +import { useEffect, useMemo } from "react" import { useDispatch, useSelector } from "react-redux"; import { Outlet, useNavigate } from "react-router-dom"; import { faBell, faMessage, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { RootState } from "State/Store"; -import { init, setPreferences, UserPreferences } from "State/Login"; +import { init, UserPreferences } from "State/Login"; import { HexKey, RawEvent, TaggedRawEvent } from "Nostr"; import { RelaySettings } from "Nostr/Connection"; import { System } from "Nostr/System" @@ -14,6 +14,7 @@ import ProfileImage from "Element/ProfileImage"; import useLoginFeed from "Feed/LoginFeed"; import { totalUnread } from "Pages/MessagesPage"; import { SearchRelays } from 'Const'; +import useEventPublisher from "Feed/EventPublisher"; export default function Layout() { const dispatch = useDispatch(); @@ -25,11 +26,13 @@ export default function Layout() { const readNotifications = useSelector(s => s.login.readNotifications); const dms = useSelector(s => s.login.dms); const prefs = useSelector(s => s.login.preferences); - - const [keyword, setKeyword] = useState(''); - + const pub = useEventPublisher(); useLoginFeed(); + useEffect(() => { + System.nip42Auth = pub.nip42Auth + },[pub]) + useEffect(() => { if (relays) { for (let [k, v] of Object.entries(relays)) {