clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

457 lines
13 KiB

import { nip19 } from "nostr-tools";
import { getEventHash, prefixNostrAddresses, signEvent } from "./nostrUtils.ts";
import { goto } from "$app/navigation";
import { EVENT_KINDS, TIME_CONSTANTS } from "./search_constants.ts";
import { EXPIRATION_DURATION } from "../consts.ts";
import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
export interface RootEventInfo {
rootId: string;
rootPubkey: string;
rootRelay: string;
rootKind: number;
rootAddress: string;
rootIValue: string;
rootIRelay: string;
isRootA: boolean;
isRootI: boolean;
}
export interface ParentEventInfo {
parentId: string;
parentPubkey: string;
parentRelay: string;
parentKind: number;
parentAddress: string;
}
export interface EventPublishResult {
success: boolean;
relay?: string;
eventId?: string;
error?: string;
}
/**
* Helper function to find a tag by its first element
*/
function findTag(tags: string[][], tagName: string): string[] | undefined {
return tags?.find((t: string[]) => t[0] === tagName);
}
/**
* Helper function to get tag value safely
*/
function getTagValue(
tags: string[][],
tagName: string,
index: number = 1,
): string {
const tag = findTag(tags, tagName);
return tag?.[index] || "";
}
/**
* Helper function to create a tag array
*/
function createTag(name: string, ...values: (string | number)[]): string[] {
return [name, ...values.map((v) => String(v))];
}
/**
* Helper function to add tags to an array
*/
function addTags(tags: string[][], ...newTags: string[][]): void {
tags.push(...newTags);
}
/**
* Extract root event information from parent event tags
*/
export function extractRootEventInfo(parent: NDKEvent): RootEventInfo {
const rootInfo: RootEventInfo = {
rootId: parent.id,
rootPubkey: getPubkeyString(parent.pubkey),
rootRelay: getRelayString(parent.relay),
rootKind: parent.kind || 1,
rootAddress: "",
rootIValue: "",
rootIRelay: "",
isRootA: false,
isRootI: false,
};
if (!parent.tags) return rootInfo;
const rootE = findTag(parent.tags, "E");
const rootA = findTag(parent.tags, "A");
const rootI = findTag(parent.tags, "I");
rootInfo.isRootA = !!rootA;
rootInfo.isRootI = !!rootI;
if (rootE) {
rootInfo.rootId = rootE[1];
rootInfo.rootRelay = getRelayString(rootE[2]);
rootInfo.rootPubkey = getPubkeyString(rootE[3] || rootInfo.rootPubkey);
rootInfo.rootKind = Number(getTagValue(parent.tags, "K")) ||
rootInfo.rootKind;
} else if (rootA) {
rootInfo.rootAddress = rootA[1];
rootInfo.rootRelay = getRelayString(rootA[2]);
rootInfo.rootPubkey = getPubkeyString(
getTagValue(parent.tags, "P") || rootInfo.rootPubkey,
);
rootInfo.rootKind = Number(getTagValue(parent.tags, "K")) ||
rootInfo.rootKind;
} else if (rootI) {
rootInfo.rootIValue = rootI[1];
rootInfo.rootIRelay = getRelayString(rootI[2]);
rootInfo.rootKind = Number(getTagValue(parent.tags, "K")) ||
rootInfo.rootKind;
}
return rootInfo;
}
/**
* Extract parent event information
*/
export function extractParentEventInfo(parent: NDKEvent): ParentEventInfo {
const dTag = getTagValue(parent.tags || [], "d");
const parentAddress = dTag
? `${parent.kind}:${getPubkeyString(parent.pubkey)}:${dTag}`
: "";
return {
parentId: parent.id,
parentPubkey: getPubkeyString(parent.pubkey),
parentRelay: getRelayString(parent.relay),
parentKind: parent.kind || 1,
parentAddress,
};
}
/**
* Build root scope tags for NIP-22 threading
*/
function buildRootScopeTags(
rootInfo: RootEventInfo,
): string[][] {
const tags: string[][] = [];
if (rootInfo.rootAddress) {
const tagType = rootInfo.isRootA ? "A" : rootInfo.isRootI ? "I" : "E";
addTags(
tags,
createTag(
tagType,
rootInfo.rootAddress || rootInfo.rootId,
rootInfo.rootRelay,
),
);
} else if (rootInfo.rootIValue) {
addTags(tags, createTag("I", rootInfo.rootIValue, rootInfo.rootIRelay));
} else {
addTags(tags, createTag("E", rootInfo.rootId, rootInfo.rootRelay));
}
addTags(tags, createTag("K", rootInfo.rootKind));
if (rootInfo.rootPubkey && !rootInfo.rootIValue) {
addTags(tags, createTag("P", rootInfo.rootPubkey, rootInfo.rootRelay));
}
return tags;
}
/**
* Build parent scope tags for NIP-22 threading
*/
function buildParentScopeTags(
parent: NDKEvent,
parentInfo: ParentEventInfo,
rootInfo: RootEventInfo,
): string[][] {
const tags: string[][] = [];
if (parentInfo.parentAddress) {
const tagType = rootInfo.isRootA ? "a" : rootInfo.isRootI ? "i" : "e";
addTags(
tags,
createTag(tagType, parentInfo.parentAddress, parentInfo.parentRelay),
);
}
addTags(
tags,
createTag("e", parent.id, parentInfo.parentRelay),
createTag("k", parentInfo.parentKind),
createTag("p", parentInfo.parentPubkey, parentInfo.parentRelay),
);
return tags;
}
/**
* Build tags for a reply event based on parent and root information
*/
export function buildReplyTags(
parent: NDKEvent,
rootInfo: RootEventInfo,
parentInfo: ParentEventInfo,
kind: number,
): string[][] {
const tags: string[][] = [];
const isParentReplaceable =
parentInfo.parentKind >= EVENT_KINDS.ADDRESSABLE.MIN &&
parentInfo.parentKind < EVENT_KINDS.ADDRESSABLE.MAX;
const isParentComment = parentInfo.parentKind === EVENT_KINDS.COMMENT;
const isReplyToComment = isParentComment && rootInfo.rootId !== parent.id;
if (kind === 1) {
// Kind 1 replies use simple e/p tags
addTags(
tags,
createTag("e", parent.id, parentInfo.parentRelay, "root"),
createTag("p", parentInfo.parentPubkey),
);
// Add address for replaceable events
if (isParentReplaceable) {
const dTag = getTagValue(parent.tags || [], "d");
if (dTag) {
const parentAddress =
`${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`;
addTags(tags, createTag("a", parentAddress, "", "root"));
}
}
} else {
// Kind 1111 (comment) uses NIP-22 threading format
if (isParentReplaceable) {
const dTag = getTagValue(parent.tags || [], "d");
if (dTag) {
const parentAddress =
`${parentInfo.parentKind}:${parentInfo.parentPubkey}:${dTag}`;
if (isReplyToComment) {
// Root scope (uppercase) - use the original article
addTags(
tags,
createTag("A", parentAddress, parentInfo.parentRelay),
createTag("K", rootInfo.rootKind),
createTag("P", rootInfo.rootPubkey, rootInfo.rootRelay),
);
// Parent scope (lowercase) - the comment we're replying to
addTags(
tags,
createTag("e", parent.id, parentInfo.parentRelay),
createTag("k", parentInfo.parentKind),
createTag("p", parentInfo.parentPubkey, parentInfo.parentRelay),
);
} else {
// Top-level comment - root and parent are the same
addTags(
tags,
createTag("A", parentAddress, parentInfo.parentRelay),
createTag("K", rootInfo.rootKind),
createTag("P", rootInfo.rootPubkey, rootInfo.rootRelay),
createTag("a", parentAddress, parentInfo.parentRelay),
createTag("e", parent.id, parentInfo.parentRelay),
createTag("k", parentInfo.parentKind),
createTag("p", parentInfo.parentPubkey, parentInfo.parentRelay),
);
}
} else {
// Fallback to E/e tags if no d-tag found
if (isReplyToComment) {
addTags(
tags,
createTag("E", rootInfo.rootId, rootInfo.rootRelay),
createTag("K", rootInfo.rootKind),
createTag("P", rootInfo.rootPubkey, rootInfo.rootRelay),
createTag("e", parent.id, parentInfo.parentRelay),
createTag("k", parentInfo.parentKind),
createTag("p", parentInfo.parentPubkey, parentInfo.parentRelay),
);
} else {
addTags(
tags,
createTag("E", parent.id, rootInfo.rootRelay),
createTag("K", rootInfo.rootKind),
createTag("P", rootInfo.rootPubkey, rootInfo.rootRelay),
createTag("e", parent.id, parentInfo.parentRelay),
createTag("k", parentInfo.parentKind),
createTag("p", parentInfo.parentPubkey, parentInfo.parentRelay),
);
}
}
} else {
// For regular events, use E/e tags
if (isReplyToComment) {
// Reply to a comment - distinguish root from parent
addTags(tags, ...buildRootScopeTags(rootInfo));
addTags(
tags,
createTag("e", parent.id, parentInfo.parentRelay),
createTag("k", parentInfo.parentKind),
createTag("p", parentInfo.parentPubkey, parentInfo.parentRelay),
);
} else {
// Top-level comment or regular event
addTags(tags, ...buildRootScopeTags(rootInfo));
addTags(tags, ...buildParentScopeTags(parent, parentInfo, rootInfo));
}
}
}
return tags;
}
/**
* Create and sign a Nostr event
*/
export async function createSignedEvent(
content: string,
pubkey: string,
kind: number,
tags: string[][],
// deno-lint-ignore no-explicit-any
): Promise<{ id: string; sig: string; event: any }> {
const prefixedContent = prefixNostrAddresses(content);
// Add expiration tag for kind 24 events (NIP-40)
const finalTags = [...tags];
if (kind === 24) {
const expirationTimestamp =
Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR) +
EXPIRATION_DURATION;
finalTags.push(["expiration", String(expirationTimestamp)]);
}
const eventToSign = {
kind: Number(kind),
created_at: Number(
Math.floor(Date.now() / TIME_CONSTANTS.UNIX_TIMESTAMP_FACTOR),
),
tags: finalTags.map((tag: any) => [
String(tag[0]),
String(tag[1]),
String(tag[2] || ""),
String(tag[3] || ""),
]),
content: String(prefixedContent),
pubkey: pubkey,
};
let sig, id;
if (
typeof window !== "undefined" && globalThis.nostr &&
globalThis.nostr.signEvent
) {
const signed = await globalThis.nostr.signEvent(eventToSign);
sig = signed.sig as string;
id = "id" in signed ? (signed.id as string) : getEventHash(eventToSign);
} else {
id = getEventHash(eventToSign);
sig = await signEvent(eventToSign);
}
return {
id,
sig,
event: {
...eventToSign,
id,
sig,
},
};
}
/**
* Publishes an event to relays using the new relay management system
* @param event The event to publish (can be NDKEvent or plain event object)
* @param relayUrls Array of relay URLs to publish to
* @returns Promise that resolves to array of successful relay URLs
*/
export async function publishEvent(
event: NDKEvent,
relayUrls: string[],
ndk: NDK,
): Promise<string[]> {
const successfulRelays: string[] = [];
if (!ndk) {
throw new Error("NDK instance not available");
}
// Create relay set from URLs
const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk);
try {
// If event is a plain object, create an NDKEvent from it
let ndkEvent: NDKEvent;
if (event.publish && typeof event.publish === "function") {
// It's already an NDKEvent
ndkEvent = event;
} else {
// It's a plain event object, create NDKEvent
ndkEvent = new NDKEvent(ndk, event);
}
// Publish with timeout
await ndkEvent.publish(relaySet).withTimeout(5000);
// For now, assume all relays were successful
// In a more sophisticated implementation, you'd track individual relay responses
successfulRelays.push(...relayUrls);
console.debug("[nostrEventService] Published event successfully:", {
eventId: ndkEvent.id,
relayCount: relayUrls.length,
successfulRelays,
});
} catch (error) {
console.error("[nostrEventService] Failed to publish event:", error);
throw new Error(`Failed to publish event: ${error}`);
}
return successfulRelays;
}
/**
* Navigate to the published event
*/
export function navigateToEvent(eventId: string): void {
try {
// Validate that eventId is a valid hex string
if (!/^[0-9a-fA-F]{64}$/.test(eventId)) {
console.warn("Invalid event ID format:", eventId);
return;
}
const nevent = nip19.neventEncode({ id: eventId });
goto(`/events?id=${nevent}`);
} catch (error) {
console.error("Failed to encode event ID for navigation:", eventId, error);
}
}
// Helper functions to ensure relay and pubkey are always strings
// deno-lint-ignore no-explicit-any
function getRelayString(relay: any): string {
if (!relay) return "";
if (typeof relay === "string") return relay;
if (typeof relay.url === "string") return relay.url;
return "";
}
// deno-lint-ignore no-explicit-any
function getPubkeyString(pubkey: any): string {
if (!pubkey) return "";
if (typeof pubkey === "string") return pubkey;
if (typeof pubkey.hex === "function") return pubkey.hex();
if (typeof pubkey.pubkey === "string") return pubkey.pubkey;
return "";
}