a tool for shared writing and social publishing
1"use server"; 2 3import * as base64 from "base64-js"; 4import { createServerClient } from "@supabase/ssr"; 5import { and, eq } from "drizzle-orm"; 6import { drizzle } from "drizzle-orm/node-postgres"; 7import { email_subscriptions_to_entity } from "drizzle/schema"; 8import postgres from "postgres"; 9import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks"; 10import type { Fact, PermissionToken } from "src/replicache"; 11import type { Attribute } from "src/replicache/attributes"; 12import { Database } from "supabase/database.types"; 13import * as Y from "yjs"; 14import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 15import { pool } from "supabase/pool"; 16 17let supabase = createServerClient<Database>( 18 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 19 process.env.SUPABASE_SERVICE_ROLE_KEY as string, 20 { cookies: {} }, 21); 22const generateCode = () => { 23 // Generate a random 6 digit code 24 let digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 25 const randomDigit = () => digits[Math.floor(Math.random() * digits.length)]; 26 return [ 27 randomDigit(), 28 randomDigit(), 29 randomDigit(), 30 randomDigit(), 31 randomDigit(), 32 randomDigit(), 33 ].join(""); 34}; 35 36export async function subscribeToMailboxWithEmail( 37 entity: string, 38 email: string, 39 token: PermissionToken, 40) { 41 const client = await pool.connect(); 42 const db = drizzle(client); 43 let newCode = generateCode(); 44 let subscription = await db.transaction(async (tx) => { 45 let existingEmail = await db 46 .select() 47 .from(email_subscriptions_to_entity) 48 .where( 49 and( 50 eq(email_subscriptions_to_entity.entity, entity), 51 eq(email_subscriptions_to_entity.email, email), 52 ), 53 ); 54 if (existingEmail[0]) return existingEmail[0]; 55 if (existingEmail.length === 0) { 56 let newSubscription = await tx 57 .insert(email_subscriptions_to_entity) 58 .values({ 59 token: token.id, 60 entity, 61 email, 62 confirmation_code: newCode, 63 }) 64 .returning(); 65 return newSubscription[0]; 66 } 67 }); 68 if (!subscription) return; 69 70 let res = await fetch("https://api.postmarkapp.com/email", { 71 method: "POST", 72 headers: { 73 "Content-Type": "application/json", 74 "X-Postmark-Server-Token": process.env.POSTMARK_API_KEY!, 75 }, 76 body: JSON.stringify({ 77 From: "Leaflet Subscriptions <subscriptions@leaflet.pub>", 78 Subject: `Your confirmation code is ${subscription.confirmation_code}`, 79 To: email, 80 TextBody: `Paste this code to confirm your subscription to a mailbox in ${await getPageTitle(token.root_entity)}: 81 82${subscription.confirmation_code} 83 `, 84 }), 85 }); 86 client.release(); 87 return subscription; 88} 89 90async function getPageTitle(root_entity: string) { 91 let { data } = await supabase.rpc("get_facts", { 92 root: root_entity, 93 }); 94 let initialFacts = (data as unknown as Fact<Attribute>[]) || []; 95 let firstPage = initialFacts.find((f) => f.attribute === "root/page") as 96 | Fact<"root/page"> 97 | undefined; 98 let root = firstPage?.data.value || root_entity; 99 let blocks = getBlocksWithTypeLocal(initialFacts, root); 100 let title = blocks.filter( 101 (f) => f.type === "text" || f.type === "heading", 102 )[0]; 103 let text = initialFacts.find( 104 (f) => f.entity === title?.value && f.attribute === "block/text", 105 ) as Fact<"block/text"> | undefined; 106 if (!text) return "Untitled Leaflet"; 107 let doc = new Y.Doc(); 108 const update = base64.toByteArray(text.data.value); 109 Y.applyUpdate(doc, update); 110 let nodes = doc.getXmlElement("prosemirror").toArray(); 111 return YJSFragmentToString(nodes[0]) || "Untitled Leaflet"; 112}