nostr-mailing-list/utils.ts

218 lines
5.7 KiB
TypeScript

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<boolean> {
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<boolean> {
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<string[]> {
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("<", "&lt;");
generatedHtml = generatedHtml.replaceAll(">", "&gt;");
generatedHtml = generatedHtml.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>");
generatedHtml = generatedHtml.replace(/\*(.*?)\*/g, "<i>$1</i>");
generatedHtml = generatedHtml.replace(/`(.*?)`/g, "<code>$1</code>");
generatedHtml = generatedHtml.replace(/^###\s+(.*?)$/gm, "<h3>$1</h3>");
generatedHtml = generatedHtml.replace(/^##\s+(.*?)$/gm, "<h2>$1</h2>");
generatedHtml = generatedHtml.replace(/^#\s+(.*?)$/gm, "<h1>$1</h1>");
generatedHtml = generatedHtml.replace(
/!\[(.*?)\]\((https?:\/\/[^\s)]+\.(jpg|jpeg|png|gif|webp))\)/g,
'<img src="$2" alt="$1">',
);
generatedHtml = generatedHtml.replace(
/(https?:\/\/[^\s)]+)/g,
'<img src="$1" alt="$1">',
);
generatedHtml = generatedHtml.replace(
/nostr:(npub1[a-z0-9]+)/g,
'<a href="nostr:$1">$1</a>',
);
generatedHtml = generatedHtml.replace(
/nostr:(note1[a-z0-9]+)/g,
'<a href="nostr:$1">$1</a>',
);
generatedHtml = generatedHtml.replace(
/nostr:(event1[a-z0-9]+)/g,
'<a href="nostr:$1">$1</a>',
);
generatedHtml = generatedHtml.replace(/\n\n/g, "<br><br>");
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}`);
}