UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit

212 lines (178 loc) 6.92 kB
import { nip19 } from "nostr-tools"; import type { EventPointer, ProfilePointer } from "../user/index.js"; import type { NDKTag } from "./index.js"; export type ContentTag = { tags: NDKTag[]; content: string; }; /** * Merges two arrays of NDKTag, ensuring uniqueness and preferring more specific tags. * * This function consolidates `tags1` and `tags2` by: * - Combining both tag arrays. * - Removing duplicate tags based on containment (one tag containing another). * - Retaining the longer or more detailed tag when overlaps are found. * * @param tags1 - The first array of NDKTag. * @param tags2 - The second array of NDKTag. * @returns A merged array of unique NDKTag. */ export function mergeTags(tags1: NDKTag[], tags2: NDKTag[]): NDKTag[] { const tagMap = new Map<string, NDKTag>(); // Function to generate a key for the hashmap const generateKey = (tag: NDKTag) => tag.join(","); // Function to determine if a tag is contained in another const isContained = (smaller: NDKTag, larger: NDKTag) => { return smaller.every((value, index) => value === larger[index]); }; // Function to process and add a tag const processTag = (tag: NDKTag) => { for (const [key, existingTag] of tagMap) { if (isContained(existingTag, tag) || isContained(tag, existingTag)) { // Replace with the longer or equal-length tag if (tag.length >= existingTag.length) { tagMap.set(key, tag); } return; } } // Add new tag if no containing relationship is found tagMap.set(generateKey(tag), tag); }; // Process all tags tags1.concat(tags2).forEach(processTag); return Array.from(tagMap.values()); } /** * Compares a tag to see if they are the same or if one is preferred * over the other (i.e. it includes more information) and returns * the tags that should be used. * @returns */ export function uniqueTag(a: NDKTag, b: NDKTag): NDKTag[] { const aLength = a.length; const bLength = b.length; const sameLength = aLength === bLength; // If sa un length if (sameLength) { if (a.every((v, i) => v === b[i])) { // and same values (regardless of length), return one return [a]; } // and different values, return both return [a, b]; } if (aLength > bLength && a.every((v, i) => v === b[i])) { // If different length but the longer contains the shorter, return the longer return [a]; } if (bLength > aLength && b.every((v, i) => v === a[i])) { return [b]; } // Otherwise, return both return [a, b]; } const hashtagRegex = /(?<=\s|^)(#[^\s!@#$%^&*()=+./,[{\]};:'"?><]+)/g; /** * Generates a unique list of hashtags as used in the content. If multiple variations * of the same hashtag are used, only the first one will be used (#ndk and #NDK both resolve to the first one that was used in the content) * @param content * @returns */ export function generateHashtags(content: string): string[] { const hashtags = content.match(hashtagRegex); const tagIds = new Set<string>(); const tag = new Set<string>(); if (hashtags) { for (const hashtag of hashtags) { if (tagIds.has(hashtag.slice(1))) continue; tag.add(hashtag.slice(1)); tagIds.add(hashtag.slice(1)); } } return Array.from(tag); } export async function generateContentTags( content: string, tags: NDKTag[] = [] ): Promise<ContentTag> { const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g; const promises: Promise<void>[] = []; const addTagIfNew = (t: NDKTag) => { if (!tags.find((t2) => ["q", t[0]].includes(t2[0]) && t2[1] === t[1])) { tags.push(t); } }; content = content.replace(tagRegex, (tag) => { try { const entity = tag.split(/(@|nostr:)/)[2]; const { type, data } = nip19.decode(entity); let t: NDKTag | undefined; switch (type) { case "npub": t = ["p", data as string]; break; case "nprofile": t = ["p", (data as ProfilePointer).pubkey as string]; break; case "note": promises.push( new Promise(async (resolve) => { addTagIfNew(["q", data, await maybeGetEventRelayUrl(entity)]); resolve(); }) ); break; case "nevent": promises.push( new Promise(async (resolve) => { const { id, author } = data as EventPointer; let { relays } = data as EventPointer; // If the nevent doesn't have a relay specified, try to get one if (!relays || relays.length === 0) { relays = [await maybeGetEventRelayUrl(entity)]; } addTagIfNew(["q", id, relays[0]]); if (author) addTagIfNew(["p", author]); resolve(); }) ); break; case "naddr": promises.push( new Promise(async (resolve) => { const id = [data.kind, data.pubkey, data.identifier].join(":"); let relays = data.relays ?? []; // If the naddr doesn't have a relay specified, try to get one if (relays.length === 0) { relays = [await maybeGetEventRelayUrl(entity)]; } addTagIfNew(["q", id, relays[0]]); addTagIfNew(["p", data.pubkey]); resolve(); }) ); break; default: return tag; } if (t) addTagIfNew(t); return `nostr:${entity}`; } catch (_error) { return tag; } }); await Promise.all(promises); const newTags = generateHashtags(content).map((hashtag) => ["t", hashtag]); tags = mergeTags(tags, newTags); return { content, tags }; } /** * Get the event from the cache, if there is one, so we can get the relay. * @param nip19Id * @returns Relay URL or an empty string */ async function maybeGetEventRelayUrl(_nip19Id: string): Promise<string> { /* TODO */ return ""; }