clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

245 lines
7.7 KiB

import NDK, { NDKNip07Signer, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, NDKUser } from '@nostr-dev-kit/ndk';
import { get, writable, type Writable } from 'svelte/store';
import { fallbackRelays, FeedType, loginStorageKey, standardRelays } from './consts';
import { feedType } from './stores';
export const ndkInstance: Writable<NDK> = writable();
export const ndkSignedIn: Writable<boolean> = writable(false);
export const activePubkey: Writable<string | null> = writable(null);
export const inboxRelays: Writable<string[]> = writable([]);
export const outboxRelays: Writable<string[]> = writable([]);
/**
* Gets the user's pubkey from local storage, if it exists.
* @returns The user's pubkey, or null if there is no logged-in user.
* @remarks Local storage is used in place of cookies to persist the user's login across browser
* sessions.
*/
export function getPersistedLogin(): string | null {
const pubkey = localStorage.getItem(loginStorageKey);
return pubkey;
}
/**
* Writes the user's pubkey to local storage.
* @param user The user to persist.
* @remarks Use this function when the user logs in. Currently, only one pubkey is stored at a
* time.
*/
export function persistLogin(user: NDKUser): void {
localStorage.setItem(loginStorageKey, user.pubkey);
}
/**
* Clears the user's pubkey from local storage.
* @remarks Use this function when the user logs out.
*/
export function clearLogin(): void {
localStorage.removeItem(loginStorageKey);
}
/**
* Constructs a key use to designate a user's relay lists in local storage.
* @param user The user for whom to construct the key.
* @param type The type of relay list to designate.
* @returns The constructed key.
*/
function getRelayStorageKey(user: NDKUser, type: 'inbox' | 'outbox'): string {
return `${loginStorageKey}/${user.pubkey}/${type}`;
}
/**
* Stores the user's relay lists in local storage.
* @param user The user for whom to store the relay lists.
* @param inboxes The user's inbox relays.
* @param outboxes The user's outbox relays.
*/
function persistRelays(user: NDKUser, inboxes: Set<NDKRelay>, outboxes: Set<NDKRelay>): void {
localStorage.setItem(
getRelayStorageKey(user, 'inbox'),
JSON.stringify(Array.from(inboxes).map(relay => relay.url))
);
localStorage.setItem(
getRelayStorageKey(user, 'outbox'),
JSON.stringify(Array.from(outboxes).map(relay => relay.url))
);
}
/**
* Retrieves the user's relay lists from local storage.
* @param user The user for whom to retrieve the relay lists.
* @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`. Either set may be
* empty if no relay lists were stored for the user.
*/
function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] {
const inboxes = new Set<string>(
JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'inbox')) ?? '[]')
);
const outboxes = new Set<string>(
JSON.parse(localStorage.getItem(getRelayStorageKey(user, 'outbox')) ?? '[]')
);
return [inboxes, outboxes];
}
export function clearPersistedRelays(user: NDKUser): void {
localStorage.removeItem(getRelayStorageKey(user, 'inbox'));
localStorage.removeItem(getRelayStorageKey(user, 'outbox'));
}
export function getActiveRelays(ndk: NDK): NDKRelaySet {
return get(feedType) === FeedType.UserRelays
? new NDKRelaySet(
new Set(get(inboxRelays).map(relay => new NDKRelay(
relay,
NDKRelayAuthPolicies.signIn({ ndk }),
ndk,
))),
ndk
)
: new NDKRelaySet(
new Set(standardRelays.map(relay => new NDKRelay(
relay,
NDKRelayAuthPolicies.signIn({ ndk }),
ndk,
))),
ndk
);
}
/**
* Initializes an instance of NDK, and connects it to the logged-in user's preferred relay set
* (if available), or to Alexandria's standard relay set.
* @returns The initialized NDK instance.
*/
export function initNdk(): NDK {
const startingPubkey = getPersistedLogin();
const [startingInboxes, _] = startingPubkey != null
? getPersistedRelays(new NDKUser({ pubkey: startingPubkey }))
: [null, null];
const ndk = new NDK({
autoConnectUserRelays: true,
enableOutboxModel: true,
explicitRelayUrls: startingInboxes != null
? Array.from(startingInboxes.values())
: standardRelays,
});
// TODO: Should we prompt the user to confirm authentication?
ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk });
ndk.connect().then(() => console.debug("ndk connected"));
return ndk;
}
/**
* Signs in with a NIP-07 browser extension, and determines the user's preferred inbox and outbox
* relays.
* @returns The user's profile, if it is available.
* @throws If sign-in fails. This may because there is no accessible NIP-07 extension, or because
* NDK is unable to fetch the user's profile or relay lists.
*/
export async function loginWithExtension(pubkey?: string): Promise<NDKUser | null> {
try {
const ndk = get(ndkInstance);
const signer = new NDKNip07Signer();
const signerUser = await signer.user();
// TODO: Handle changing pubkeys.
if (pubkey && signerUser.pubkey !== pubkey) {
console.debug('Switching pubkeys from last login.');
}
activePubkey.set(signerUser.pubkey);
const [persistedInboxes, persistedOutboxes] = getPersistedRelays(signerUser);
for (const relay of persistedInboxes) {
ndk.addExplicitRelay(relay);
}
const user = ndk.getUser({ pubkey: signerUser.pubkey });
const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user);
inboxRelays.set(Array.from(inboxes ?? persistedInboxes).map(relay => relay.url));
outboxRelays.set(Array.from(outboxes ?? persistedOutboxes).map(relay => relay.url));
persistRelays(signerUser, inboxes, outboxes);
ndk.signer = signer;
ndk.activeUser = user;
ndkInstance.set(ndk);
ndkSignedIn.set(true);
return user;
} catch (e) {
throw new Error(`Failed to sign in with NIP-07 extension: ${e}`);
}
}
/**
* Handles logging out a user.
* @param user The user to log out.
*/
export function logout(user: NDKUser): void {
clearLogin();
clearPersistedRelays(user);
activePubkey.set(null);
ndkSignedIn.set(false);
}
/**
* Fetches the user's NIP-65 relay list, if one can be found, and returns the inbox and outbox
* relay sets.
* @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`.
*/
async function getUserPreferredRelays(
ndk: NDK,
user: NDKUser,
fallbacks: readonly string[] = fallbackRelays
): Promise<[Set<NDKRelay>, Set<NDKRelay>]> {
const relayList = await ndk.fetchEvent(
{
kinds: [10002],
authors: [user.pubkey],
},
{
groupable: false,
skipVerification: false,
skipValidation: false,
},
NDKRelaySet.fromRelayUrls(fallbacks, ndk),
);
const inboxRelays = new Set<NDKRelay>();
const outboxRelays = new Set<NDKRelay>();
if (relayList == null) {
const relayMap = await window.nostr?.getRelays?.();
Object.entries(relayMap ?? {}).forEach(([url, relayType]) => {
const relay = new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk);
if (relayType.read) inboxRelays.add(relay);
if (relayType.write) outboxRelays.add(relay);
});
} else {
relayList.tags.forEach(tag => {
switch (tag[0]) {
case 'r':
inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
break;
case 'w':
outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
break;
default:
inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk));
break;
}
});
}
return [inboxRelays, outboxRelays];
}