2024-01-01 17:28:10 +00:00
|
|
|
import { PublicKey } from "../../libs/nostr.ts/key.ts";
|
2023-10-09 21:02:13 +00:00
|
|
|
import { DirectedMessage_Event, Parsed_Event } from "../nostr.ts";
|
2024-01-01 17:28:10 +00:00
|
|
|
import { Nevent, NostrAddress, NostrProfile, NoteID } from "../../libs/nostr.ts/nip19.ts";
|
|
|
|
import { NostrKind } from "../../libs/nostr.ts/nostr.ts";
|
2023-06-30 14:05:57 +00:00
|
|
|
|
|
|
|
export function* parseContent(content: string) {
|
|
|
|
// URLs
|
|
|
|
yield* match(/https?:\/\/[^\s]+/g, content, "url");
|
|
|
|
|
|
|
|
// npubs
|
2023-09-09 15:01:07 +00:00
|
|
|
yield* match(/(nostr:)?npub[0-9a-z]{59}/g, content, "npub");
|
2023-06-30 14:05:57 +00:00
|
|
|
|
2023-09-12 15:50:14 +00:00
|
|
|
//nprofile
|
|
|
|
yield* match(/(nostr:)?nprofile[0-9a-z]+/g, content, "nprofile");
|
|
|
|
|
2023-09-16 22:12:41 +00:00
|
|
|
//naddr
|
|
|
|
yield* match(/(nostr:)?naddr[0-9a-z]+/g, content, "naddr");
|
|
|
|
|
2023-07-05 09:32:02 +00:00
|
|
|
// notes
|
|
|
|
yield* match(/note[0-9a-z]{59}/g, content, "note");
|
|
|
|
|
2023-09-22 15:11:26 +00:00
|
|
|
// nevent
|
|
|
|
yield* match(/(nostr:)?nevent[0-9a-z]+/g, content, "nevent");
|
|
|
|
|
2023-06-30 14:05:57 +00:00
|
|
|
// tags
|
|
|
|
yield* match(/#\[[0-9]+\]/g, content, "tag");
|
|
|
|
}
|
|
|
|
|
|
|
|
function* match(regex: RegExp, content: string, type: ItemType): Generator<ContentItem, void, unknown> {
|
|
|
|
let match;
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value
|
|
|
|
// If the match succeeds, the exec() method returns an array and
|
|
|
|
// updates the lastIndex property of the regular expression object.
|
|
|
|
while ((match = regex.exec(content)) !== null) {
|
|
|
|
const urlStartPosition = match.index;
|
|
|
|
if (urlStartPosition == undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const urlEndPosition = urlStartPosition + match[0].length - 1;
|
2023-07-16 15:04:23 +00:00
|
|
|
if (type == "note") {
|
|
|
|
const noteID = NoteID.FromBech32(content.slice(urlStartPosition, urlEndPosition + 1));
|
|
|
|
if (noteID instanceof Error) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
yield {
|
|
|
|
type: type,
|
|
|
|
noteID: noteID,
|
|
|
|
start: urlStartPosition,
|
|
|
|
end: urlEndPosition,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} else if (type == "npub") {
|
2023-09-09 15:01:07 +00:00
|
|
|
let bech32: string;
|
|
|
|
if (match[0].startsWith("nostr:")) {
|
|
|
|
bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1);
|
|
|
|
} else {
|
|
|
|
bech32 = content.slice(urlStartPosition, urlEndPosition + 1);
|
|
|
|
}
|
|
|
|
const pubkey = PublicKey.FromBech32(bech32);
|
2023-07-16 15:04:23 +00:00
|
|
|
if (pubkey instanceof Error) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
yield {
|
|
|
|
type: type,
|
2023-09-16 20:04:48 +00:00
|
|
|
pubkey: pubkey,
|
2023-07-16 15:04:23 +00:00
|
|
|
start: urlStartPosition,
|
|
|
|
end: urlEndPosition,
|
|
|
|
};
|
|
|
|
}
|
2023-09-12 15:50:14 +00:00
|
|
|
} else if (type == "nprofile") {
|
|
|
|
let bech32: string;
|
|
|
|
if (match[0].startsWith("nostr:")) {
|
|
|
|
bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1);
|
|
|
|
} else {
|
|
|
|
bech32 = content.slice(urlStartPosition, urlEndPosition + 1);
|
|
|
|
}
|
|
|
|
const decoded_nProfile = NostrProfile.decode(bech32);
|
|
|
|
if (decoded_nProfile instanceof Error) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
const pubkey = decoded_nProfile.pubkey;
|
|
|
|
|
|
|
|
yield {
|
|
|
|
type: "npub",
|
2023-09-16 20:04:48 +00:00
|
|
|
pubkey: pubkey,
|
2023-09-12 15:50:14 +00:00
|
|
|
start: urlStartPosition,
|
|
|
|
end: urlEndPosition,
|
|
|
|
relays: decoded_nProfile.relays,
|
|
|
|
};
|
|
|
|
}
|
2023-09-16 22:12:41 +00:00
|
|
|
} else if (type == "naddr") {
|
|
|
|
let bech32: string;
|
|
|
|
if (match[0].startsWith("nostr:")) {
|
|
|
|
bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1);
|
|
|
|
} else {
|
|
|
|
bech32 = content.slice(urlStartPosition, urlEndPosition + 1);
|
|
|
|
}
|
|
|
|
const decoded_nAddr = NostrAddress.decode(bech32);
|
|
|
|
if (decoded_nAddr instanceof Error) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
yield {
|
|
|
|
type: "naddr",
|
|
|
|
start: urlStartPosition,
|
|
|
|
end: urlEndPosition,
|
|
|
|
addr: decoded_nAddr,
|
|
|
|
};
|
|
|
|
}
|
2023-09-22 15:11:26 +00:00
|
|
|
} else if (type == "nevent") {
|
|
|
|
let bech32: string;
|
|
|
|
if (match[0].startsWith("nostr:")) {
|
|
|
|
bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1);
|
|
|
|
} else {
|
|
|
|
bech32 = content.slice(urlStartPosition, urlEndPosition + 1);
|
|
|
|
}
|
|
|
|
const decoded_nEvent = Nevent.decode(bech32);
|
|
|
|
if (decoded_nEvent instanceof Error) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
yield {
|
|
|
|
type: "nevent",
|
|
|
|
start: urlStartPosition,
|
|
|
|
end: urlEndPosition,
|
|
|
|
event: decoded_nEvent,
|
|
|
|
};
|
|
|
|
}
|
2023-07-16 15:04:23 +00:00
|
|
|
} else {
|
|
|
|
yield {
|
|
|
|
type: type,
|
|
|
|
start: urlStartPosition,
|
|
|
|
end: urlEndPosition,
|
|
|
|
};
|
|
|
|
}
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-16 15:04:23 +00:00
|
|
|
type otherItemType = "url" | "tag";
|
2023-09-22 15:11:26 +00:00
|
|
|
type ItemType = otherItemType | "note" | "npub" | "nprofile" | "naddr" | "nevent";
|
2023-06-30 14:05:57 +00:00
|
|
|
export type ContentItem = {
|
2023-07-16 15:04:23 +00:00
|
|
|
type: otherItemType;
|
|
|
|
start: number;
|
|
|
|
end: number;
|
|
|
|
} | {
|
|
|
|
type: "npub";
|
2023-09-16 20:04:48 +00:00
|
|
|
pubkey: PublicKey;
|
2023-07-16 15:04:23 +00:00
|
|
|
start: number;
|
|
|
|
end: number;
|
2023-09-12 15:50:14 +00:00
|
|
|
relays?: string[];
|
2023-07-16 15:04:23 +00:00
|
|
|
} | {
|
|
|
|
type: "note";
|
|
|
|
noteID: NoteID;
|
2023-06-30 14:05:57 +00:00
|
|
|
start: number;
|
|
|
|
end: number;
|
2023-09-16 22:12:41 +00:00
|
|
|
} | {
|
|
|
|
type: "naddr";
|
|
|
|
start: number;
|
|
|
|
end: number;
|
|
|
|
addr: NostrAddress;
|
2023-09-22 15:11:26 +00:00
|
|
|
} | {
|
|
|
|
type: "nevent";
|
|
|
|
start: number;
|
|
|
|
end: number;
|
|
|
|
event: Nevent;
|
2023-06-30 14:05:57 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Think of ChatMessage as an materialized view of NostrEvent
|
2023-10-09 21:02:13 +00:00
|
|
|
export type ChatMessage = {
|
2023-06-30 14:05:57 +00:00
|
|
|
readonly type: "image" | "text";
|
2024-03-30 07:51:59 +00:00
|
|
|
readonly event: DirectedMessage_Event | Parsed_Event<NostrKind.TEXT_NOTE | NostrKind.Long_Form>;
|
2023-10-09 21:02:13 +00:00
|
|
|
readonly author: PublicKey;
|
|
|
|
readonly created_at: Date;
|
|
|
|
readonly lamport: number | undefined;
|
|
|
|
readonly content: string;
|
|
|
|
};
|
2023-06-30 14:05:57 +00:00
|
|
|
|
|
|
|
export function urlIsImage(url: string) {
|
2023-11-22 06:41:41 +00:00
|
|
|
const trimmed = url.trim().toLocaleLowerCase();
|
2023-06-30 14:05:57 +00:00
|
|
|
const parts = trimmed.split(".");
|
2023-07-03 08:26:30 +00:00
|
|
|
return ["png", "jpg", "jpeg", "gif", "webp"].includes(parts[parts.length - 1]);
|
2023-11-22 06:41:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function urlIsVideo(url: string) {
|
|
|
|
const trimmed = url.trim().toLocaleLowerCase();
|
|
|
|
const parts = trimmed.split(".");
|
|
|
|
return ["mov", "mp4", "wmv", "flv", "avi", "webm", "mkv"].includes(parts[parts.length - 1]);
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function* groupContinuousMessages<T>(
|
|
|
|
seq: Iterable<T>,
|
|
|
|
checker: (previousItem: T, currentItem: T) => boolean,
|
|
|
|
) {
|
|
|
|
let previousItem: T | undefined;
|
|
|
|
let group: T[] = [];
|
|
|
|
for (const currentItem of seq) {
|
|
|
|
if (previousItem == undefined || checker(previousItem, currentItem)) {
|
|
|
|
group.push(currentItem);
|
|
|
|
} else {
|
|
|
|
yield group;
|
|
|
|
group = [currentItem];
|
|
|
|
}
|
|
|
|
previousItem = currentItem;
|
|
|
|
}
|
2023-10-08 15:53:44 +00:00
|
|
|
if (group.length > 0) {
|
|
|
|
yield group;
|
|
|
|
}
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
|
2023-10-07 23:23:53 +00:00
|
|
|
export function sortMessage(messages: ChatMessage[]) {
|
2023-06-30 14:05:57 +00:00
|
|
|
return messages
|
|
|
|
.sort((m1, m2) => {
|
2023-10-07 23:23:53 +00:00
|
|
|
if (m1.lamport && m2.lamport) {
|
|
|
|
if (m1.lamport == m2.lamport) {
|
2024-03-15 13:44:17 +00:00
|
|
|
return m1.created_at.getTime() - m2.created_at.getTime();
|
2023-06-30 14:05:57 +00:00
|
|
|
} else {
|
2024-03-15 13:44:17 +00:00
|
|
|
return m1.lamport - m2.lamport;
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
}
|
2024-03-15 13:44:17 +00:00
|
|
|
return m1.created_at.getTime() - m2.created_at.getTime();
|
2023-06-30 14:05:57 +00:00
|
|
|
});
|
|
|
|
}
|
2023-12-07 13:18:26 +00:00
|
|
|
|
|
|
|
// credit to GPT4
|
|
|
|
export function findUrlInString(text: string): (string | URL)[] {
|
|
|
|
// Regular expression for URLs with various protocols
|
|
|
|
const urlRegex = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]+/g;
|
|
|
|
|
|
|
|
// Split the text into URL and non-URL parts
|
|
|
|
let parts = text.split(urlRegex);
|
|
|
|
|
|
|
|
// Find all URLs using the regex
|
|
|
|
const foundUrls = text.match(urlRegex) || [];
|
|
|
|
|
|
|
|
// Interleave non-URL parts and URL parts
|
|
|
|
let result: (string | URL)[] = [];
|
|
|
|
parts.forEach((part, index) => {
|
|
|
|
if (part !== "") {
|
|
|
|
result.push(part);
|
|
|
|
}
|
|
|
|
if (index < foundUrls.length) {
|
|
|
|
try {
|
|
|
|
result.push(new URL(foundUrls[index]));
|
|
|
|
} catch {
|
|
|
|
result.push(foundUrls[index]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|