v2 start
This commit is contained in:
@ -13,3 +13,24 @@ export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g;
|
||||
* How long profile cache should be considered valid for
|
||||
*/
|
||||
export const ProfileCacheExpire = 1_000 * 60 * 60 * 6;
|
||||
|
||||
/**
|
||||
* Extract file extensions regex
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
export const FileExtensionRegex = /\.([\w]{1,7})$/i;
|
||||
|
||||
/**
|
||||
* Simple lightning invoice regex
|
||||
*/
|
||||
export const InvoiceRegex = /(lnbc\w+)/i;
|
||||
|
||||
/*
|
||||
* Regex to match any base64 string
|
||||
*/
|
||||
export const CashuRegex = /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/i;
|
||||
|
||||
/**
|
||||
* Regex to match any npub/nevent/naddr/nprofile/note
|
||||
*/
|
||||
export const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g;
|
@ -1,5 +1,5 @@
|
||||
import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner } from ".";
|
||||
import { HashtagRegex } from "./const";
|
||||
import { HashtagRegex, MentionNostrEntityRegex } from "./const";
|
||||
import { getPublicKey, unixNow } from "@snort/shared";
|
||||
import { EventExt } from "./event-ext";
|
||||
import { tryParseNostrLink } from "./nostr-link";
|
||||
@ -43,7 +43,7 @@ export class EventBuilder {
|
||||
*/
|
||||
processContent() {
|
||||
if (this.#content) {
|
||||
this.#content = this.#content.replace(/@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g, m =>
|
||||
this.#content = this.#content.replace(MentionNostrEntityRegex, m =>
|
||||
this.#replaceMention(m)
|
||||
);
|
||||
|
||||
|
@ -20,6 +20,7 @@ export * from "./nostr-link";
|
||||
export * from "./profile-cache";
|
||||
export * from "./zaps";
|
||||
export * from "./signer";
|
||||
export * from "./text";
|
||||
|
||||
export * from "./impl/nip4";
|
||||
export * from "./impl/nip44";
|
||||
|
204
packages/system/src/text.ts
Normal file
204
packages/system/src/text.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
import { CashuRegex, FileExtensionRegex, HashtagRegex, InvoiceRegex, MentionNostrEntityRegex } from "./const";
|
||||
import { validateNostrLink } from "./nostr-link";
|
||||
import { splitByUrl } from "./utils";
|
||||
|
||||
export interface ParsedFragment {
|
||||
type: "text" | "link" | "mention" | "invoice" | "media" | "cashu" | "hashtag" | "custom_emoji"
|
||||
content: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export type Fragment = string | ParsedFragment;
|
||||
|
||||
export interface TextFragment {
|
||||
body: React.ReactNode[];
|
||||
tags: Array<Array<string>>;
|
||||
}
|
||||
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return splitByUrl(f).map(a => {
|
||||
const validateLink = () => {
|
||||
const normalizedStr = a.toLowerCase();
|
||||
|
||||
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
|
||||
return validateNostrLink(normalizedStr);
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedStr.startsWith("http:") ||
|
||||
normalizedStr.startsWith("https:") ||
|
||||
normalizedStr.startsWith("magnet:")
|
||||
);
|
||||
};
|
||||
|
||||
if (validateLink()) {
|
||||
const url = new URL(a);
|
||||
const extension = url.pathname.match(FileExtensionRegex);
|
||||
|
||||
if (extension && extension.length > 1) {
|
||||
const mediaType = (() => {
|
||||
switch (extension[1]) {
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "jfif":
|
||||
case "png":
|
||||
case "bmp":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "wav":
|
||||
case "mp3":
|
||||
case "ogg":
|
||||
return "audio";
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "m4v":
|
||||
case "webm":
|
||||
case "m3u8":
|
||||
return "video";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
return {
|
||||
type: "media",
|
||||
content: a,
|
||||
mimeType: `${mediaType}/${extension[1]}`
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return {
|
||||
type: "link",
|
||||
content: a
|
||||
} as ParsedFragment;
|
||||
}
|
||||
}
|
||||
return a;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractMentions(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MentionNostrEntityRegex).map(i => {
|
||||
if (MentionNostrEntityRegex.test(i)) {
|
||||
return {
|
||||
type: "mention",
|
||||
content: i
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractCashuTokens(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string" && f.includes("cashuA")) {
|
||||
return f.split(CashuRegex).map(a => {
|
||||
return {
|
||||
type: "cashu",
|
||||
content: a
|
||||
} as ParsedFragment
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractInvoices(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(InvoiceRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("lnbc")) {
|
||||
return {
|
||||
type: "invoice",
|
||||
content: i
|
||||
} as ParsedFragment
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractHashtags(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(HashtagRegex).map(i => {
|
||||
if (i.toLowerCase().startsWith("#")) {
|
||||
return {
|
||||
type: "hashtag",
|
||||
content: i.substring(1)
|
||||
} as ParsedFragment;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/:(\w+):/g).map(i => {
|
||||
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
|
||||
if (t) {
|
||||
return {
|
||||
type: "custom_emoji",
|
||||
content: t[2]
|
||||
} as ParsedFragment
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
export function transformText(body: string, tags: Array<Array<string>>) {
|
||||
let fragments = extractLinks([body]);
|
||||
fragments = extractMentions(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractCashuTokens(fragments);
|
||||
fragments = extractCustomEmoji(fragments, tags);
|
||||
fragments = fragments.map(a => {
|
||||
if (typeof a === "string") {
|
||||
if (a.trim().length > 0) {
|
||||
return { type: "text", content: a } as ParsedFragment;
|
||||
}
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
}).filter(a => a).map(a => unwrap(a));
|
||||
return fragments as Array<ParsedFragment>;
|
||||
}
|
@ -27,19 +27,24 @@ export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | Req
|
||||
}
|
||||
|
||||
export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
|
||||
return (
|
||||
a.keys === b.keys &&
|
||||
a.since === b.since &&
|
||||
a.until === b.until &&
|
||||
a.limit === b.limit &&
|
||||
a.search === b.search &&
|
||||
a.ids === b.ids &&
|
||||
a.kinds === b.kinds &&
|
||||
a.authors === b.authors &&
|
||||
a["#e"] === b["#e"] &&
|
||||
a["#p"] === b["#p"] &&
|
||||
a["#t"] === b["#t"] &&
|
||||
a["#d"] === b["#d"] &&
|
||||
a["#r"] === b["#r"]
|
||||
);
|
||||
return a.keys === b.keys
|
||||
&& a.since === b.since
|
||||
&& a.until === b.until
|
||||
&& a.limit === b.limit
|
||||
&& a.search === b.search
|
||||
&& a.ids === b.ids
|
||||
&& a.kinds === b.kinds
|
||||
&& a.authors === b.authors
|
||||
&& a["#e"] === b["#e"]
|
||||
&& a["#p"] === b["#p"]
|
||||
&& a["#t"] === b["#t"]
|
||||
&& a["#d"] === b["#d"]
|
||||
&& a["#r"] === b["#r"];
|
||||
}
|
||||
|
||||
export function splitByUrl(str: string) {
|
||||
const urlRegex =
|
||||
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
|
||||
|
||||
return str.split(urlRegex);
|
||||
}
|
||||
|
Reference in New Issue
Block a user