a tool for shared writing and social publishing
1import {
2 LinkPreviewBody,
3 LinkPreviewImageResult,
4 LinkPreviewMetadataResult,
5} from "app/api/link_previews/route";
6import { Replicache } from "replicache";
7import type { ReplicacheMutators } from "src/replicache";
8import { AtpAgent } from "@atproto/api";
9import { v7 } from "uuid";
10
11export async function addLinkBlock(
12 url: string,
13 entityID: string,
14 rep?: Replicache<ReplicacheMutators> | null,
15) {
16 if (!rep) return;
17
18 let res = await fetch("/api/link_previews", {
19 headers: { "Content-Type": "application/json" },
20 method: "POST",
21 body: JSON.stringify({ url, type: "meta" } as LinkPreviewBody),
22 });
23 if (res.status !== 200) {
24 await rep?.mutate.assertFact([
25 {
26 entity: entityID,
27 attribute: "link/url",
28 data: {
29 type: "string",
30 value: url,
31 },
32 },
33 {
34 entity: entityID,
35 attribute: "block/type",
36 data: { type: "block-type-union", value: "link" },
37 },
38 ]);
39 return;
40 }
41 let data = await (res.json() as LinkPreviewMetadataResult);
42 if (!data.success) {
43 await rep?.mutate.assertFact([
44 {
45 entity: entityID,
46 attribute: "link/url",
47 data: {
48 type: "string",
49 value: url,
50 },
51 },
52 {
53 entity: entityID,
54 attribute: "block/type",
55 data: { type: "block-type-union", value: "link" },
56 },
57 ]);
58 return;
59 }
60
61 if (data.data.links?.player?.[0]) {
62 let embed = data.data.links?.player?.[0];
63 await rep.mutate.assertFact([
64 {
65 entity: entityID,
66 attribute: "block/type",
67 data: { type: "block-type-union", value: "embed" },
68 },
69 {
70 entity: entityID,
71 attribute: "embed/url",
72 data: {
73 type: "string",
74 value: embed.href,
75 },
76 },
77 {
78 entity: entityID,
79 attribute: "embed/height",
80 data: {
81 type: "number",
82 value: embed.media?.height || 300,
83 },
84 },
85 ]);
86 return;
87 }
88 await rep?.mutate.assertFact([
89 {
90 entity: entityID,
91 attribute: "link/url",
92 data: {
93 type: "string",
94 value: url,
95 },
96 },
97 {
98 entity: entityID,
99 attribute: "block/type",
100 data: { type: "block-type-union", value: "link" },
101 },
102 {
103 entity: entityID,
104 attribute: "link/title",
105 data: {
106 type: "string",
107 value: data.data.meta?.title || "",
108 },
109 },
110 {
111 entity: entityID,
112 attribute: "link/description",
113 data: {
114 type: "string",
115 value: data.data.meta?.description || "",
116 },
117 },
118 ]);
119 let imageRes = await fetch("/api/link_previews", {
120 headers: { "Content-Type": "application/json" },
121 method: "POST",
122 body: JSON.stringify({ url, type: "image" } as LinkPreviewBody),
123 });
124
125 let image_data = await (imageRes.json() as LinkPreviewImageResult);
126
127 await rep?.mutate.assertFact({
128 entity: entityID,
129 attribute: "link/preview",
130 data: {
131 fallback: "",
132 type: "image",
133 src: image_data.url,
134 width: image_data.width,
135 height: image_data.height,
136 },
137 });
138}
139
140export async function addBlueskyPostBlock(
141 url: string,
142 entityID: string,
143 rep: Replicache<ReplicacheMutators>,
144) {
145 //construct bsky post uri from url
146 let urlParts = url?.split("/");
147 let userDidOrHandle = urlParts ? urlParts[4] : ""; // "schlage.town" or "did:plc:jjsc5rflv3cpv6hgtqhn2dcm"
148 let collection = "app.bsky.feed.post";
149 let postId = urlParts ? urlParts[6] : "";
150 let uri = `at://${userDidOrHandle}/${collection}/${postId}`;
151
152 let post = await getBlueskyPost(uri);
153 if (!post || post === undefined) return false;
154
155 await rep.mutate.assertFact({
156 entity: entityID,
157 attribute: "block/type",
158 data: { type: "block-type-union", value: "bluesky-post" },
159 });
160 await rep?.mutate.assertFact({
161 entity: entityID,
162 attribute: "block/bluesky-post",
163 data: {
164 type: "bluesky-post",
165 //TODO: this is a hack to get rid of a nested Array buffer which cannot be frozen, which replicache does on write.
166 value: JSON.parse(JSON.stringify(post.data.thread)),
167 },
168 });
169 return true;
170}
171async function getBlueskyPost(uri: string) {
172 const agent = new AtpAgent({ service: "https://public.api.bsky.app" });
173 try {
174 let blueskyPost = await agent
175 .getPostThread({
176 uri: uri,
177 depth: 0,
178 parentHeight: 0,
179 })
180 .then((res) => {
181 return res;
182 });
183 return blueskyPost;
184 } catch (error) {
185 let rect = document;
186 return;
187 }
188}