import type * as net from "node:net"; import type { Event } from "@nostr/tools"; import { exists } from "jsr:@std/fs/exists"; import type { ImapCommandParameters } from "./imapParsing.ts"; import type { ImapSession } from "./imapServer.ts"; export async function isMessageRead( session: ImapSession, eventId: string, ): Promise { if (!session.npub) { return false; } const readMessages = JSON.parse( await getFileOrDefault("readMessages.json", "{}"), ); const readMessagesForUser = readMessages[session.npub]; if (!readMessagesForUser) { return false; } return readMessagesForUser.includes(eventId); } export async function markMessageAsRead( session: ImapSession, eventId: string, positive = true, ) { if (!session.npub) { return; } const readMessages = JSON.parse( await getFileOrDefault("readMessages.json", "{}"), ); readMessages[session.npub] = readMessages[session.npub] || []; if (positive) { readMessages[session.npub].push(eventId); } else { readMessages[session.npub] = readMessages[session.npub].filter( (id: string) => id !== eventId, ); } await Deno.writeTextFile( "data/readMessages.json", JSON.stringify(readMessages, null, 2), ); } export async function isMessageFlagged( session: ImapSession, eventId: string, ): Promise { if (!session.npub) { return false; } const flaggedMessages = JSON.parse( await getFileOrDefault("flaggedMessages.json", "{}"), ); const flaggedMessagesForUser = flaggedMessages[session.npub]; if (!flaggedMessagesForUser) { return false; } return flaggedMessagesForUser.includes(eventId); } export async function markMessageAsFlagged( session: ImapSession, eventId: string, positive = true, ) { if (!session.npub) { return; } const flaggedMessages = JSON.parse( await getFileOrDefault("flaggedMessages.json", "{}"), ); flaggedMessages[session.npub] = flaggedMessages[session.npub] || []; if (positive) { flaggedMessages[session.npub].push(eventId); } else { flaggedMessages[session.npub] = flaggedMessages[session.npub].filter( (id: string) => id !== eventId, ); } await Deno.writeTextFile( "data/flaggedMessages.json", JSON.stringify(flaggedMessages, null, 2), ); } export async function getFlags( session: ImapSession, eventId: string, ): Promise { const isRead = await isMessageRead(session, eventId); const isFlagged = await isMessageFlagged(session, eventId); return [isRead ? "\\Seen" : "", isFlagged ? "\\Flagged" : ""].filter(Boolean); } export function parseUid(uid: ImapCommandParameters): [number, number] { if (typeof uid !== "string") { throw new Error("Invalid UID"); } const [start, end] = uid.split(":"); if (end === undefined) { return [Number.parseInt(start), Number.parseInt(start)]; } if (start === "*") { return [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; } if (end === "*") { return [Number.parseInt(start), Number.POSITIVE_INFINITY]; } return [Number.parseInt(start), Number.parseInt(end)]; } export function getByteLength(text: string): number { return new Blob([text]).size; } export function parseBasicNostrMarkdown(text: string): string { let generatedHtml = text.replaceAll("<", "<"); generatedHtml = generatedHtml.replaceAll(">", ">"); generatedHtml = generatedHtml.replace(/\*\*(.*?)\*\*/g, "$1"); generatedHtml = generatedHtml.replace(/\*(.*?)\*/g, "$1"); generatedHtml = generatedHtml.replace(/`(.*?)`/g, "$1"); generatedHtml = generatedHtml.replace(/^###\s+(.*?)$/gm, "

$1

"); generatedHtml = generatedHtml.replace(/^##\s+(.*?)$/gm, "

$1

"); generatedHtml = generatedHtml.replace(/^#\s+(.*?)$/gm, "

$1

"); generatedHtml = generatedHtml.replace( /!\[(.*?)\]\((https?:\/\/[^\s)]+\.(jpg|jpeg|png|gif|webp))\)/g, '$1', ); generatedHtml = generatedHtml.replace( /(https?:\/\/[^\s)]+)/g, '$1', ); generatedHtml = generatedHtml.replace( /nostr:(npub1[a-z0-9]+)/g, '$1', ); generatedHtml = generatedHtml.replace( /nostr:(note1[a-z0-9]+)/g, '$1', ); generatedHtml = generatedHtml.replace( /nostr:(event1[a-z0-9]+)/g, '$1', ); generatedHtml = generatedHtml.replace(/\n\n/g, "

"); return generatedHtml; } const eventIdToUidMap: string[] = JSON.parse( await getFileOrDefault("event-id-to-uid-map.json", "[]"), ); export function findEventIdForUid(uid: number): string { return eventIdToUidMap[uid]; } export function findUidForEvent(event: Event): number { const eventId = event.id; const uid = eventIdToUidMap.findIndex((id) => id === eventId); if (uid !== -1) { return uid; } eventIdToUidMap.push(eventId); Deno.writeFileSync( "event-id-to-uid-map.json", new TextEncoder().encode(JSON.stringify(eventIdToUidMap)), ); return eventIdToUidMap.length; } export function sendSocketMessage(socket: net.Socket, message: string) { if (socket.destroyed || socket.closed) { console.error("Socket is destroyed or closed"); return; } socket.write(message); logMessage("out", message); } export function logMessage(direction: string, message: string) { Deno.writeTextFileSync( "data/message-logs.txt", `${ JSON.stringify({ direction, message, }) }\n`, { append: true, }, ); } export async function getFileOrDefault(path: string, defaultValue: string) { if (!await exists("data")) { await Deno.mkdir("data"); } if (!await exists(`data/${path}`)) { await Deno.writeTextFile(`data/${path}`, defaultValue); } return await Deno.readTextFile(`data/${path}`); }