protocol handler
This commit is contained in:
parent
37ef874dc6
commit
bfc4c47189
56
src/Nostr/Links.ts
Normal file
56
src/Nostr/Links.ts
Normal 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;
|
||||
}
|
@ -65,9 +65,9 @@ export default function Layout() {
|
||||
() =>
|
||||
publicKey
|
||||
? totalUnread(
|
||||
dms.filter(a => !isMuted(a.pubkey)),
|
||||
publicKey
|
||||
)
|
||||
dms.filter(a => !isMuted(a.pubkey)),
|
||||
publicKey
|
||||
)
|
||||
: 0,
|
||||
[dms, publicKey]
|
||||
);
|
||||
@ -138,6 +138,15 @@ export default function Layout() {
|
||||
|
||||
console.debug(`Using db: ${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);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
26
src/Pages/NostrLinkHandler.tsx
Normal file
26
src/Pages/NostrLinkHandler.tsx
Normal 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}</>
|
||||
)
|
||||
}
|
@ -42,7 +42,7 @@ import QrCode from "Element/QrCode";
|
||||
import Modal from "Element/Modal";
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||
|
||||
import { NostrPrefix } from "Nostr/Links";
|
||||
import messages from "./messages";
|
||||
|
||||
const NOTES = 0;
|
||||
@ -262,7 +262,7 @@ export default function ProfilePage() {
|
||||
{showProfileQr && (
|
||||
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||
<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>
|
||||
)}
|
||||
{isMe ? (
|
||||
@ -280,7 +280,7 @@ export default function ProfilePage() {
|
||||
)}
|
||||
{!loggedOut && (
|
||||
<>
|
||||
<IconButton onClick={() => navigate(`/messages/${hexToBech32("npub", id)}`)}>
|
||||
<IconButton onClick={() => navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`)}>
|
||||
<Envelope width={16} height={13} />
|
||||
</IconButton>
|
||||
</>
|
||||
|
13
src/Util.ts
13
src/Util.ts
@ -3,6 +3,7 @@ import { sha256 as hash } from "@noble/hashes/sha256";
|
||||
import { bech32 } from "bech32";
|
||||
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { encodeTLV, NostrPrefix } from "Nostr/Links";
|
||||
|
||||
export const sha256 = (str: string) => {
|
||||
return secp.utils.bytesToHex(hash(str));
|
||||
@ -64,7 +65,7 @@ export function bech32ToText(str: string) {
|
||||
* @returns
|
||||
*/
|
||||
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 {
|
||||
const buf = secp.utils.hexToBytes(hex);
|
||||
return bech32.encode(hrp, bech32.toWords(buf));
|
||||
if (hrp === NostrPrefix.Note || hrp === NostrPrefix.PrivateKey || hrp === NostrPrefix.PublicKey) {
|
||||
let buf = secp.utils.hexToBytes(hex);
|
||||
return bech32.encode(hrp, bech32.toWords(buf));
|
||||
} else {
|
||||
return encodeTLV(hex, hrp as NostrPrefix);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Invalid hex", hex, e);
|
||||
return "";
|
||||
@ -91,7 +96,7 @@ export function hexToBech32(hrp: string, hex?: string) {
|
||||
* @returns
|
||||
*/
|
||||
export function profileLink(hex: HexKey) {
|
||||
return `/p/${hexToBech32("npub", hex)}`;
|
||||
return `/p/${hexToBech32(NostrPrefix.PublicKey, hex)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,6 +25,7 @@ import HashTagsPage from "Pages/HashTagsPage";
|
||||
import SearchPage from "Pages/SearchPage";
|
||||
import HelpPage from "Pages/HelpPage";
|
||||
import { NewUserRoutes } from "Pages/new";
|
||||
import NostrLinkHandler from 'Pages/NostrLinkHandler';
|
||||
import { IntlProvider } from "./IntlProvider";
|
||||
import { unwrap } from "Util";
|
||||
|
||||
@ -93,6 +94,10 @@ export const router = createBrowserRouter([
|
||||
path: "/search/:keyword?",
|
||||
element: <SearchPage />,
|
||||
},
|
||||
{
|
||||
path: "/handler/*",
|
||||
element: <NostrLinkHandler />
|
||||
},
|
||||
...NewUserRoutes,
|
||||
],
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user