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}