Merge pull request #164 from v0l/nip21

protocol handler
This commit is contained in:
Kieran 2023-02-14 10:32:11 +00:00 committed by GitHub
commit 36dc9f6543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 10 deletions

56
src/Nostr/Links.ts Normal file
View File

@ -0,0 +1,56 @@
import * as secp from "@noble/secp256k1";
import { bech32 } from "bech32";
export enum NostrPrefix {
PublicKey = "npub",
PrivateKey = "nsec",
Note = "note",
// TLV prefixes
Profile = "nprofile",
Event = "nevent",
Relay = "nrelay"
}
export interface TLVEntry {
type: number,
length: number,
value: string // hex encoded data
}
export function encodeTLV(hex: string, prefix: NostrPrefix, relays?: string[]) {
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) {
return "";
}
let enc = new TextEncoder();
let buf = secp.utils.hexToBytes(hex);
let tl0 = [0, buf.length, ...buf];
let tl1 = relays?.map(a => {
let data = enc.encode(a);
return [1, data.length, ...data];
}).flat() ?? [];
return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1]));
}
export function decodeTLV(str: string) {
let decoded = bech32.decode(str);
let data = bech32.fromWords(decoded.words);
let entries: TLVEntry[] = [];
let x = 0;
while (x < data.length) {
let t = data[x];
let l = data[x + 1];
let v = data.slice(x + 2, x + 2 + l);
entries.push({
type: t,
length: l,
value: secp.utils.bytesToHex(new Uint8Array(v))
});
x += 2 + l;
}
return entries;
}

View File

@ -65,9 +65,9 @@ export default function Layout() {
() => () =>
publicKey publicKey
? totalUnread( ? totalUnread(
dms.filter(a => !isMuted(a.pubkey)), dms.filter(a => !isMuted(a.pubkey)),
publicKey publicKey
) )
: 0, : 0,
[dms, publicKey] [dms, publicKey]
); );
@ -138,6 +138,15 @@ export default function Layout() {
console.debug(`Using db: ${dbType}`); console.debug(`Using db: ${dbType}`);
dispatch(init(dbType)); dispatch(init(dbType));
try {
if ("registerProtocolHandler" in window.navigator) {
window.navigator.registerProtocolHandler("web+nostr", `${window.location.protocol}//${window.location.host}/handler/%s`);
console.info("Registered protocol handler for \"nostr\"");
}
} catch (e) {
console.error("Failed to register protocol handler", e);
}
}); });
}, []); }, []);

View File

@ -0,0 +1,26 @@
import { NostrPrefix } from "Nostr/Links";
import { useEffect } from "react";
import { redirect, useNavigate, useParams } from "react-router-dom";
export default function NostrLinkHandler() {
const params = useParams();
const navigate = useNavigate();
const link = decodeURIComponent(params["*"] ?? "");
useEffect(() => {
if (link.length > 0) {
let ls = link.split(":");
let entity = ls[1];
if (entity.startsWith(NostrPrefix.PublicKey) || entity.startsWith(NostrPrefix.Profile)) {
navigate(`/p/${entity}`);
}
else if (entity.startsWith(NostrPrefix.Event) || entity.startsWith(NostrPrefix.Note)) {
navigate(`/e/${entity}`)
}
}
}, [link]);
return (
<>Could not handle {link}</>
)
}

View File

@ -42,7 +42,7 @@ import QrCode from "Element/QrCode";
import Modal from "Element/Modal"; import Modal from "Element/Modal";
import { ProxyImg } from "Element/ProxyImg"; import { ProxyImg } from "Element/ProxyImg";
import useHorizontalScroll from "Hooks/useHorizontalScroll"; import useHorizontalScroll from "Hooks/useHorizontalScroll";
import { NostrPrefix } from "Nostr/Links";
import messages from "./messages"; import messages from "./messages";
const NOTES = 0; const NOTES = 0;
@ -262,7 +262,7 @@ export default function ProfilePage() {
{showProfileQr && ( {showProfileQr && (
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}> <Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
<ProfileImage pubkey={id} /> <ProfileImage pubkey={id} />
<QrCode data={`nostr:${hexToBech32("npub", id)}`} link={undefined} className="m10" /> <QrCode data={`nostr:${hexToBech32(NostrPrefix.PublicKey, id)}`} link={undefined} className="m10" />
</Modal> </Modal>
)} )}
{isMe ? ( {isMe ? (
@ -280,7 +280,7 @@ export default function ProfilePage() {
)} )}
{!loggedOut && ( {!loggedOut && (
<> <>
<IconButton onClick={() => navigate(`/messages/${hexToBech32("npub", id)}`)}> <IconButton onClick={() => navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`)}>
<Envelope width={16} height={13} /> <Envelope width={16} height={13} />
</IconButton> </IconButton>
</> </>

View File

@ -3,6 +3,7 @@ import { sha256 as hash } from "@noble/hashes/sha256";
import { bech32 } from "bech32"; import { bech32 } from "bech32";
import { HexKey, TaggedRawEvent, u256 } from "Nostr"; import { HexKey, TaggedRawEvent, u256 } from "Nostr";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { encodeTLV, NostrPrefix } from "Nostr/Links";
export const sha256 = (str: string) => { export const sha256 = (str: string) => {
return secp.utils.bytesToHex(hash(str)); return secp.utils.bytesToHex(hash(str));
@ -64,7 +65,7 @@ export function bech32ToText(str: string) {
* @returns * @returns
*/ */
export function eventLink(hex: u256) { export function eventLink(hex: u256) {
return `/e/${hexToBech32("note", hex)}`; return `/e/${hexToBech32(NostrPrefix.Note, hex)}`;
} }
/** /**
@ -77,8 +78,12 @@ export function hexToBech32(hrp: string, hex?: string) {
} }
try { try {
const buf = secp.utils.hexToBytes(hex); if (hrp === NostrPrefix.Note || hrp === NostrPrefix.PrivateKey || hrp === NostrPrefix.PublicKey) {
return bech32.encode(hrp, bech32.toWords(buf)); let buf = secp.utils.hexToBytes(hex);
return bech32.encode(hrp, bech32.toWords(buf));
} else {
return encodeTLV(hex, hrp as NostrPrefix);
}
} catch (e) { } catch (e) {
console.warn("Invalid hex", hex, e); console.warn("Invalid hex", hex, e);
return ""; return "";
@ -91,7 +96,7 @@ export function hexToBech32(hrp: string, hex?: string) {
* @returns * @returns
*/ */
export function profileLink(hex: HexKey) { export function profileLink(hex: HexKey) {
return `/p/${hexToBech32("npub", hex)}`; return `/p/${hexToBech32(NostrPrefix.PublicKey, hex)}`;
} }
/** /**

View File

@ -25,6 +25,7 @@ import HashTagsPage from "Pages/HashTagsPage";
import SearchPage from "Pages/SearchPage"; import SearchPage from "Pages/SearchPage";
import HelpPage from "Pages/HelpPage"; import HelpPage from "Pages/HelpPage";
import { NewUserRoutes } from "Pages/new"; import { NewUserRoutes } from "Pages/new";
import NostrLinkHandler from 'Pages/NostrLinkHandler';
import { IntlProvider } from "./IntlProvider"; import { IntlProvider } from "./IntlProvider";
import { unwrap } from "Util"; import { unwrap } from "Util";
@ -93,6 +94,10 @@ export const router = createBrowserRouter([
path: "/search/:keyword?", path: "/search/:keyword?",
element: <SearchPage />, element: <SearchPage />,
}, },
{
path: "/handler/*",
element: <NostrLinkHandler />
},
...NewUserRoutes, ...NewUserRoutes,
], ],
}, },