feat: parse magnet links
This commit is contained in:
parent
4325c49435
commit
8e69342a0c
@ -21,6 +21,7 @@
|
|||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
|
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
|
||||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||||
|
"base32-decode": "^1.0.0",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"dexie": "^3.2.2",
|
"dexie": "^3.2.2",
|
||||||
"dexie-react-hooks": "^1.1.1",
|
"dexie-react-hooks": "^1.1.1",
|
||||||
|
@ -44,6 +44,7 @@ export const DefaultRelays = new Map<string, RelaySettings>([
|
|||||||
["wss://eden.nostr.land", { read: true, write: false }],
|
["wss://eden.nostr.land", { read: true, write: false }],
|
||||||
["wss://atlas.nostr.land", { read: true, write: false }],
|
["wss://atlas.nostr.land", { read: true, write: false }],
|
||||||
["wss://relay.orangepill.dev", { read: true, write: false }],
|
["wss://relay.orangepill.dev", { read: true, write: false }],
|
||||||
|
["wss://nos.lol", { read: true, write: false }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,3 +153,8 @@ export const AppleMusicRegex =
|
|||||||
* Nostr Nests embed regex
|
* Nostr Nests embed regex
|
||||||
*/
|
*/
|
||||||
export const NostrNestsRegex = /nostrnests\.com\/[a-zA-Z0-9]+/i;
|
export const NostrNestsRegex = /nostrnests\.com\/[a-zA-Z0-9]+/i;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Magnet link parser
|
||||||
|
*/
|
||||||
|
export const MagnetRegex = /(magnet:[\S]+)/i;
|
||||||
|
18
packages/app/src/Element/Magnet.tsx
Normal file
18
packages/app/src/Element/Magnet.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Magnet } from "Util";
|
||||||
|
|
||||||
|
interface MagnetLinkProps {
|
||||||
|
magnet: Magnet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MagnetLink = ({ magnet }: MagnetLinkProps) => {
|
||||||
|
return (
|
||||||
|
<div className="note-invoice">
|
||||||
|
<h4>Magnet Link</h4>
|
||||||
|
<a href={magnet.raw} rel="noreferrer">
|
||||||
|
{magnet.dn ?? magnet.infoHash}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MagnetLink;
|
@ -3,17 +3,16 @@ import { useMemo, useCallback } from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { visit, SKIP } from "unist-util-visit";
|
import { visit, SKIP } from "unist-util-visit";
|
||||||
|
import * as unist from "unist";
|
||||||
|
import { HexKey, Tag } from "@snort/nostr";
|
||||||
|
|
||||||
import { MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
|
import { MentionRegex, InvoiceRegex, HashtagRegex, MagnetRegex } from "Const";
|
||||||
import { eventLink, hexToBech32, splitByUrl, unwrap } from "Util";
|
import { eventLink, hexToBech32, magnetURIDecode, splitByUrl, unwrap } from "Util";
|
||||||
import Invoice from "Element/Invoice";
|
import Invoice from "Element/Invoice";
|
||||||
import Hashtag from "Element/Hashtag";
|
import Hashtag from "Element/Hashtag";
|
||||||
|
|
||||||
import { Tag } from "@snort/nostr";
|
|
||||||
import Mention from "Element/Mention";
|
import Mention from "Element/Mention";
|
||||||
import HyperText from "Element/HyperText";
|
import HyperText from "Element/HyperText";
|
||||||
import { HexKey } from "@snort/nostr";
|
import MagnetLink from "Element/Magnet";
|
||||||
import * as unist from "unist";
|
|
||||||
|
|
||||||
export type Fragment = string | React.ReactNode;
|
export type Fragment = string | React.ReactNode;
|
||||||
|
|
||||||
@ -45,6 +44,25 @@ export default function Text({ content, tags, creator }: TextProps) {
|
|||||||
.flat();
|
.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMagnetLinks(fragments: Fragment[]) {
|
||||||
|
return fragments
|
||||||
|
.map(f => {
|
||||||
|
if (typeof f === "string") {
|
||||||
|
return f.split(MagnetRegex).map(a => {
|
||||||
|
if (a.startsWith("magnet:")) {
|
||||||
|
const parsed = magnetURIDecode(a);
|
||||||
|
if (parsed) {
|
||||||
|
return <MagnetLink magnet={parsed} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
|
||||||
function extractMentions(frag: TextFragment) {
|
function extractMentions(frag: TextFragment) {
|
||||||
return frag.body
|
return frag.body
|
||||||
.map(f => {
|
.map(f => {
|
||||||
@ -135,6 +153,7 @@ export default function Text({ content, tags, creator }: TextProps) {
|
|||||||
fragments = extractLinks(fragments);
|
fragments = extractLinks(fragments);
|
||||||
fragments = extractInvoices(fragments);
|
fragments = extractInvoices(fragments);
|
||||||
fragments = extractHashtags(fragments);
|
fragments = extractHashtags(fragments);
|
||||||
|
fragments = extractMagnetLinks(fragments);
|
||||||
return fragments;
|
return fragments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { splitByUrl } from "./Util";
|
import { splitByUrl, magnetURIDecode } from "./Util";
|
||||||
|
|
||||||
describe("splitByUrl", () => {
|
describe("splitByUrl", () => {
|
||||||
it("should split a string by URLs", () => {
|
it("should split a string by URLs", () => {
|
||||||
@ -38,3 +38,21 @@ describe("splitByUrl", () => {
|
|||||||
expect(splitByUrl(inputStr)).toEqual(expectedOutput);
|
expect(splitByUrl(inputStr)).toEqual(expectedOutput);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("magnet", () => {
|
||||||
|
it("should parse magnet link", () => {
|
||||||
|
const book =
|
||||||
|
"magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36&xt=urn:btmh:1220d2474e86c95b19b8bcfdb92bc12c9d44667cfa36d2474e86c95b19b8bcfdb92b&dn=Leaves+of+Grass+by+Walt+Whitman.epub&tr=udp%3A%2F%2Ftracker.example4.com%3A80&tr=udp%3A%2F%2Ftracker.example5.com%3A80&tr=udp%3A%2F%2Ftracker.example3.com%3A6969&tr=udp%3A%2F%2Ftracker.example2.com%3A80&tr=udp%3A%2F%2Ftracker.example1.com%3A1337";
|
||||||
|
const output = magnetURIDecode(book);
|
||||||
|
expect(output).not.toBeUndefined();
|
||||||
|
expect(output!.dn).toEqual("Leaves of Grass by Walt Whitman.epub");
|
||||||
|
expect(output!.infoHash).toEqual("d2474e86c95b19b8bcfdb92bc12c9d44667cfa36");
|
||||||
|
expect(output!.tr).toEqual([
|
||||||
|
"udp://tracker.example4.com:80",
|
||||||
|
"udp://tracker.example5.com:80",
|
||||||
|
"udp://tracker.example3.com:6969",
|
||||||
|
"udp://tracker.example2.com:80",
|
||||||
|
"udp://tracker.example1.com:1337",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -3,6 +3,7 @@ import { sha256 as hash } from "@noble/hashes/sha256";
|
|||||||
import { bytesToHex } from "@noble/hashes/utils";
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
|
import base32Decode from "base32-decode";
|
||||||
import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr";
|
import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr";
|
||||||
|
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "State/Users";
|
||||||
@ -271,3 +272,110 @@ export function decodeInvoice(pr: string) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Magnet {
|
||||||
|
dn?: string | string[];
|
||||||
|
tr?: string | string[];
|
||||||
|
xs?: string | string[];
|
||||||
|
as?: string | string[];
|
||||||
|
ws?: string | string[];
|
||||||
|
kt?: string[];
|
||||||
|
ix?: number | number[];
|
||||||
|
xt?: string | string[];
|
||||||
|
infoHash?: string;
|
||||||
|
raw?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a magnet URI and return an object of keys/values
|
||||||
|
*/
|
||||||
|
export function magnetURIDecode(uri: string): Magnet | undefined {
|
||||||
|
try {
|
||||||
|
const result: Record<string, string | number | number[] | string[] | undefined> = {
|
||||||
|
raw: uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Support 'magnet:' and 'stream-magnet:' uris
|
||||||
|
const data = uri.trim().split("magnet:?")[1];
|
||||||
|
|
||||||
|
const params = data && data.length > 0 ? data.split("&") : [];
|
||||||
|
|
||||||
|
params.forEach(param => {
|
||||||
|
const split = param.split("=");
|
||||||
|
const key = split[0];
|
||||||
|
const val = decodeURIComponent(split[1]);
|
||||||
|
|
||||||
|
if (!result[key]) {
|
||||||
|
result[key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "dn": {
|
||||||
|
(result[key] as string[]).push(val.replace(/\+/g, " "));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "kt": {
|
||||||
|
val.split("+").forEach(e => {
|
||||||
|
(result[key] as string[]).push(e);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ix": {
|
||||||
|
(result[key] as number[]).push(Number(val));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "so": {
|
||||||
|
// todo: not implemented yet
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
(result[key] as string[]).push(val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convenience properties for parity with `parse-torrent-file` module
|
||||||
|
let m;
|
||||||
|
if (result.xt) {
|
||||||
|
const xts = Array.isArray(result.xt) ? result.xt : [result.xt];
|
||||||
|
xts.forEach(xt => {
|
||||||
|
if (typeof xt === "string") {
|
||||||
|
if ((m = xt.match(/^urn:btih:(.{40})/))) {
|
||||||
|
result.infoHash = [m[1].toLowerCase()];
|
||||||
|
} else if ((m = xt.match(/^urn:btih:(.{32})/))) {
|
||||||
|
const decodedStr = base32Decode(m[1], "RFC4648-HEX");
|
||||||
|
result.infoHash = [bytesToHex(new Uint8Array(decodedStr))];
|
||||||
|
} else if ((m = xt.match(/^urn:btmh:1220(.{64})/))) {
|
||||||
|
result.infoHashV2 = [m[1].toLowerCase()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.xs) {
|
||||||
|
const xss = Array.isArray(result.xs) ? result.xs : [result.xs];
|
||||||
|
xss.forEach(xs => {
|
||||||
|
if (typeof xs === "string" && (m = xs.match(/^urn:btpk:(.{64})/))) {
|
||||||
|
if (!result.publicKey) {
|
||||||
|
result.publicKey = [];
|
||||||
|
}
|
||||||
|
(result.publicKey as string[]).push(m[1].toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(result)) {
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
if (v.length === 1) {
|
||||||
|
result[k] = v[0];
|
||||||
|
} else if (v.length === 0) {
|
||||||
|
result[k] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse magnet link", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -3168,6 +3168,18 @@ balanced-match@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
base32-decode@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/base32-decode/-/base32-decode-1.0.0.tgz#2a821d6a664890c872f20aa9aca95a4b4b80e2a7"
|
||||||
|
integrity sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g==
|
||||||
|
|
||||||
|
base32@^0.0.7:
|
||||||
|
version "0.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/base32/-/base32-0.0.7.tgz#9d3c21de7efde73086fa0164d57ffcb1120c68ea"
|
||||||
|
integrity sha512-ire9Jmh+BsUk4Idu0wu6aKeJJr/2j28Mlu0qqJBd1SyOGxW/VRotkfwAv3/KE/entVlNwCefs9bxi7kBeCIxTQ==
|
||||||
|
dependencies:
|
||||||
|
minimist "^1.2.6"
|
||||||
|
|
||||||
base64-js@^1.3.1:
|
base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
|
Loading…
Reference in New Issue
Block a user